Merge branch 'develop' into chrome-pdf
This commit is contained in:
commit
f74671267d
78 changed files with 45849 additions and 10361 deletions
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
|
||||
|
||||
|
||||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ frappe.listview_settings["DocType"] = {
|
|||
fieldtype: "Data",
|
||||
reqd: 1,
|
||||
default: doctype_name,
|
||||
length: 61,
|
||||
},
|
||||
{ fieldtype: "Column Break" },
|
||||
{
|
||||
|
|
|
|||
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 || "");
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -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 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)
|
||||
|
|
|
|||
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"]
|
||||
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
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
|
|
@ -1127,7 +1127,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 = {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -16,25 +16,6 @@ frappe.ui.form.ControlTable = class ControlTable extends frappe.ui.form.Control
|
|||
this.frm.grids[this.frm.grids.length] = 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) => {
|
||||
const table_field = this.df.fieldname;
|
||||
const grid = this.grid;
|
||||
|
|
@ -173,13 +154,4 @@ frappe.ui.form.ControlTable = class ControlTable extends frappe.ui.form.Control
|
|||
check_all_rows() {
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ frappe.form.formatters = {
|
|||
docfield.precision ||
|
||||
cint(frappe.boot.sysdefaults && frappe.boot.sysdefaults.float_precision) ||
|
||||
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) {
|
||||
let rating_html = "";
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ export default class Grid {
|
|||
<p class="text-muted small grid-description"></p>
|
||||
<div class="grid-custom-buttons"></div>
|
||||
<div class="form-grid-container">
|
||||
<div class="form-grid" tabIndex="0">
|
||||
<div class="form-grid">
|
||||
<div class="grid-heading-row"></div>
|
||||
<div class="grid-body">
|
||||
<div class="rows"></div>
|
||||
|
|
@ -939,6 +939,7 @@ export default class Grid {
|
|||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this.grid_rows[idx].toggle_editable_row(true);
|
||||
this.grid_rows[idx].row
|
||||
.find('input[type="Text"],textarea,select')
|
||||
.filter(":visible:first")
|
||||
|
|
@ -1276,4 +1277,14 @@ export default class Grid {
|
|||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1102,7 +1102,17 @@ export default class GridRow {
|
|||
|
||||
this.columns[df.fieldname] = $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;
|
||||
}
|
||||
|
||||
|
|
@ -1200,6 +1210,8 @@ export default class GridRow {
|
|||
// flag list input
|
||||
if (this.columns_list && this.columns_list.slice(-1)[0] === column) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
} 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.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() {
|
||||
return this.doc.idx > 1;
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup {
|
|||
this.is_dialog = true;
|
||||
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) {
|
||||
this.make();
|
||||
}
|
||||
|
|
@ -34,6 +34,7 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup {
|
|||
if (!this.size) this.set_modal_size();
|
||||
|
||||
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");
|
||||
else if (this.size == "large") $(this.wrapper).addClass("modal-lg");
|
||||
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 it
|
||||
this.handle_focus();
|
||||
if (window.location.pathname.startsWith("/app")) {
|
||||
this.handle_focus();
|
||||
}
|
||||
|
||||
if (this.animate) {
|
||||
this.$wrapper.addClass("fade");
|
||||
} else {
|
||||
|
|
@ -278,7 +282,7 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup {
|
|||
|
||||
handle_focus() {
|
||||
const me = this;
|
||||
if (frappe.get_route) {
|
||||
if (frappe.get_route()) {
|
||||
if (frappe.get_route()[0] == "Form") {
|
||||
if (!me.last_focus) me.last_focus = document.activeElement;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -117,6 +117,10 @@ frappe.ui.FieldSelect = class FieldSelect {
|
|||
// main table
|
||||
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) {
|
||||
if (df.is_virtual) {
|
||||
return;
|
||||
}
|
||||
|
||||
let doctype =
|
||||
frappe.get_meta(me.doctype).istable && me.parent_doctype
|
||||
? me.parent_doctype
|
||||
|
|
@ -128,7 +132,7 @@ frappe.ui.FieldSelect = class FieldSelect {
|
|||
|
||||
// child tables
|
||||
$.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]);
|
||||
|
||||
if (table_df.fieldtype === "Table MultiSelect") {
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ frappe.search.AwesomeBar = class AwesomeBar {
|
|||
"input",
|
||||
frappe.utils.debounce(function (e) {
|
||||
var value = e.target.value;
|
||||
value = frappe.utils.xss_sanitise(value);
|
||||
var txt = value.trim().replace(/\s\s+/g, " ");
|
||||
var last_space = txt.lastIndexOf(" ");
|
||||
me.global_results = [];
|
||||
|
|
|
|||
|
|
@ -213,6 +213,7 @@ frappe.views.FileView = class FileView extends frappe.views.ListView {
|
|||
title = d.file_name || d.file_url;
|
||||
}
|
||||
|
||||
title = frappe.utils.escape_html(title);
|
||||
title = title.slice(0, 60);
|
||||
d._title = title;
|
||||
d.icon_class = icon_class;
|
||||
|
|
|
|||
|
|
@ -355,7 +355,7 @@ frappe.views.TreeView = class TreeView {
|
|||
var node = me.tree.get_selected_node();
|
||||
|
||||
if (!(node && node.expandable)) {
|
||||
frappe.msgprint(__("Select a group node first."));
|
||||
frappe.msgprint(__("Select a group {0} first.", [__(me.doctype)]));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -415,8 +415,10 @@ frappe.views.TreeView = class TreeView {
|
|||
{
|
||||
fieldtype: "Check",
|
||||
fieldname: "is_group",
|
||||
label: __("Group Node"),
|
||||
description: __("Further nodes can be only created under 'Group' type nodes"),
|
||||
label: __("Is Group"),
|
||||
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"),
|
||||
action: function () {
|
||||
frappe.set_route("List", me.doctype);
|
||||
frappe.set_route(["List", me.doctype, "List"]);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -103,6 +103,7 @@ export default class WebFormList {
|
|||
label: df.label,
|
||||
fieldname: df.fieldname,
|
||||
fieldtype: df.fieldtype,
|
||||
options: df.options,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,9 +8,6 @@
|
|||
color: var(--text-color);
|
||||
min-height: 150px;
|
||||
background-color: var(--subtle-accent);
|
||||
&:focus-visible {
|
||||
@include grid-focus();
|
||||
}
|
||||
}
|
||||
|
||||
.form-grid.error {
|
||||
|
|
@ -165,7 +162,9 @@
|
|||
display: flex;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.grid-static-col:focus-visible {
|
||||
@include grid-focus();
|
||||
}
|
||||
.grid-static-col,
|
||||
.row-index {
|
||||
// height: 38px;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,10 +49,6 @@
|
|||
align-items: unset;
|
||||
}
|
||||
|
||||
.input-area {
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
|
||||
.label-area {
|
||||
white-space: unset;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ table.user-perm {
|
|||
margin-bottom: var(--margin-sm);
|
||||
label {
|
||||
position: relative;
|
||||
align-items: center;
|
||||
}
|
||||
input[type="checkbox"] {
|
||||
margin-left: 0;
|
||||
|
|
|
|||
|
|
@ -176,6 +176,18 @@ class TestDBUpdate(IntegrationTestCase):
|
|||
|
||||
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):
|
||||
@run_only_if(db_type_is.MARIADB)
|
||||
|
|
|
|||
|
|
@ -1610,6 +1610,19 @@ class TestQuery(IntegrationTestCase):
|
|||
frappe.qb.get_query("User", fields=[{"DROP": "TABLE users"}]).get_sql()
|
||||
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
|
||||
def test_permission_hook_condition(user):
|
||||
|
|
|
|||
|
|
@ -180,6 +180,10 @@ class TestWebsite(IntegrationTestCase):
|
|||
"route_redirects",
|
||||
{"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()
|
||||
|
||||
set_request(method="GET", path="/testfrom")
|
||||
|
|
@ -226,6 +230,11 @@ class TestWebsite(IntegrationTestCase):
|
|||
self.assertEqual(response.status_code, 307)
|
||||
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")
|
||||
frappe.client_cache.delete_value("app_hooks")
|
||||
|
||||
|
|
|
|||
124
frappe/utils/inplacevar.py
Normal file
124
frappe/utils/inplacevar.py
Normal 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)
|
||||
|
|
@ -89,7 +89,6 @@ def install_basic_docs():
|
|||
"thread_notify": 0,
|
||||
"send_me_a_copy": 0,
|
||||
},
|
||||
{"doctype": "Role", "role_name": "Translator"},
|
||||
{
|
||||
"doctype": "Workflow State",
|
||||
"workflow_state_name": "Pending",
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ from typing import TYPE_CHECKING, Any
|
|||
|
||||
import orjson
|
||||
import RestrictedPython.Guards
|
||||
from AccessControl.ZopeGuards import protected_inplacevar
|
||||
from RestrictedPython import PrintCollector, compile_restricted, safe_globals
|
||||
from RestrictedPython.transformer import RestrictingNodeTransformer
|
||||
|
||||
|
|
@ -33,6 +32,7 @@ from frappe.model.rename_doc import rename_doc
|
|||
from frappe.modules import scrub
|
||||
from frappe.utils.background_jobs import enqueue, get_jobs
|
||||
from frappe.utils.caching import site_cache
|
||||
from frappe.utils.inplacevar import protected_inplacevar
|
||||
from frappe.utils.number_format import NumberFormat
|
||||
from frappe.utils.response import json_handler
|
||||
from frappe.website.utils import get_next_link, get_toc
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
"document_type": "Other",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"is_disabled",
|
||||
"page_title",
|
||||
"company_introduction",
|
||||
"sb0",
|
||||
|
|
@ -75,6 +76,12 @@
|
|||
"fieldname": "team_members_subtitle",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Team Members Subtitle"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_disabled",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disabled"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-group",
|
||||
|
|
@ -82,7 +89,7 @@
|
|||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-23 16:01:26.370657",
|
||||
"modified": "2025-08-22 15:56:33.957707",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Website",
|
||||
"name": "About Us Settings",
|
||||
|
|
@ -98,6 +105,7 @@
|
|||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ class AboutUsSettings(Document):
|
|||
company_history_heading: DF.Data | None
|
||||
company_introduction: DF.TextEditor | None
|
||||
footer: DF.TextEditor | None
|
||||
is_disabled: DF.Check
|
||||
page_title: DF.Data | None
|
||||
team_members: DF.Table[AboutUsTeamMember]
|
||||
team_members_heading: DF.Data | None
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"disable_contact_us",
|
||||
"introduction_section",
|
||||
"is_disabled",
|
||||
"forward_to_email",
|
||||
"heading",
|
||||
"introduction",
|
||||
|
|
@ -56,7 +56,6 @@
|
|||
"label": "Query Options"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "address",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Address"
|
||||
|
|
@ -120,16 +119,16 @@
|
|||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "disable_contact_us",
|
||||
"fieldname": "is_disabled",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disable Contact Us Page"
|
||||
"label": "Disabled"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-cog",
|
||||
"idx": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-05-15 07:19:31.401053",
|
||||
"modified": "2025-08-22 12:59:39.463182",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Website",
|
||||
"name": "Contact Us Settings",
|
||||
|
|
@ -145,6 +144,7 @@
|
|||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ class ContactUsSettings(Document):
|
|||
forward_to_email: DF.Data | None
|
||||
heading: DF.Data | None
|
||||
introduction: DF.TextEditor | None
|
||||
is_disabled: DF.Check
|
||||
phone: DF.Data | None
|
||||
pincode: DF.Data | None
|
||||
query_options: DF.SmallText | None
|
||||
|
|
|
|||
|
|
@ -301,6 +301,7 @@ frappe.ui.form.on("Web Form List Column", {
|
|||
if (!df) return;
|
||||
doc.fieldtype = df.fieldtype;
|
||||
doc.label = df.label;
|
||||
doc.options = df.options;
|
||||
frm.refresh_field("list_columns");
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -274,53 +274,69 @@ def get_context(context):
|
|||
|
||||
def load_translations(self, context):
|
||||
messages = [
|
||||
"Sr",
|
||||
"Attach",
|
||||
"Next",
|
||||
"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",
|
||||
"{0} if you are not redirected within {1} seconds",
|
||||
"← Back to upload files",
|
||||
"Are you sure you want to delete this record?",
|
||||
"Are you sure you want to discard the changes?",
|
||||
"Mandatory fields required::Error message in web form",
|
||||
"Invalid values for fields::Error message in web form",
|
||||
"Error:Title of error message in web form",
|
||||
"Page {0} of {1}",
|
||||
"Couldn't save, please check the data you have entered",
|
||||
"Validation Error",
|
||||
"No {0} found",
|
||||
"Create a new {0}",
|
||||
"Attach a web link",
|
||||
"Attach",
|
||||
"Attachments",
|
||||
"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",
|
||||
"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",
|
||||
"Load More",
|
||||
"Message",
|
||||
"Missing Values Required:Error message in web form",
|
||||
"Missing Values Required",
|
||||
"My Device",
|
||||
"New",
|
||||
"Next",
|
||||
"No {0} found",
|
||||
"No comments yet.",
|
||||
"No Images",
|
||||
"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 public",
|
||||
"Sr",
|
||||
"Start a new discussion",
|
||||
"Upload",
|
||||
"Link",
|
||||
"Public",
|
||||
"Private",
|
||||
"Optimize",
|
||||
"Drop files here",
|
||||
("Submit another response", "Button in web form"),
|
||||
("Submit", "Button in web form"),
|
||||
"Submitted",
|
||||
"Take Photo",
|
||||
"No Images",
|
||||
"Thank you for spending your valuable time to fill this form",
|
||||
"Total Images",
|
||||
"Preview",
|
||||
"Submit",
|
||||
"Capture",
|
||||
"Attach a web link",
|
||||
"← Back to upload files",
|
||||
"Updated",
|
||||
"Upload",
|
||||
"Validation Error",
|
||||
("View your response", "Button in web form"),
|
||||
("Yes", "Approve confirmation dialog"),
|
||||
"Your form has been successfully updated",
|
||||
self.title,
|
||||
self.introduction_text,
|
||||
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
|
||||
if any(field.fieldtype == "Table" for field in self.web_form_fields):
|
||||
messages.append("Move")
|
||||
messages.append("Insert Above")
|
||||
messages.append("Insert Below")
|
||||
messages.append("Duplicate")
|
||||
messages.append("Shortcuts")
|
||||
messages.append("Ctrl + Up")
|
||||
messages.append("Ctrl + Down")
|
||||
messages.append("ESC")
|
||||
messages.append("Editing Row")
|
||||
messages.append("Add / Remove Columns")
|
||||
messages.append("Fieldname")
|
||||
messages.append("Column Width")
|
||||
messages.append("Configure Columns")
|
||||
messages.append("Select Fields")
|
||||
messages.append("Select All")
|
||||
messages.append("Update")
|
||||
messages.append("Reset to default")
|
||||
messages.append("No Data")
|
||||
messages.append("Delete")
|
||||
messages.append("Delete All")
|
||||
messages.append("Add Row")
|
||||
messages.append("Add Multiple")
|
||||
messages.append("Download")
|
||||
messages.append("of")
|
||||
messages.append("Upload")
|
||||
messages.append("Last")
|
||||
messages.append("First")
|
||||
messages.append("No.")
|
||||
|
||||
messages.extend(
|
||||
(
|
||||
"Move",
|
||||
"Insert Above",
|
||||
"Insert Below",
|
||||
"Duplicate",
|
||||
"Shortcuts",
|
||||
"Ctrl + Up",
|
||||
"Ctrl + Down",
|
||||
"ESC",
|
||||
"Editing Row",
|
||||
"Add / Remove Columns",
|
||||
"Fieldname",
|
||||
"Column Width",
|
||||
"Configure Columns",
|
||||
"Select Fields",
|
||||
"Select All",
|
||||
"Update",
|
||||
"Reset to default",
|
||||
"No Data",
|
||||
"Delete",
|
||||
"Delete All",
|
||||
"Add Row",
|
||||
"Add Multiple",
|
||||
"Download",
|
||||
"of",
|
||||
"Upload",
|
||||
"Last",
|
||||
"First",
|
||||
"No.",
|
||||
)
|
||||
)
|
||||
# Phone Picker
|
||||
if any(field.fieldtype == "Phone" for field in self.web_form_fields):
|
||||
messages.append("Search for countries...")
|
||||
|
||||
# Dates
|
||||
if any(field.fieldtype == "Date" for field in self.web_form_fields):
|
||||
messages.append("Now")
|
||||
messages.append("Today")
|
||||
messages.append("Date {0} must be in format: {1}")
|
||||
messages.append("{0} to {1}")
|
||||
|
||||
messages.extend(("Now", "Today", "Date {0} must be in format: {1}", "{0} to {1}"))
|
||||
# Time
|
||||
if any(field.fieldtype == "Time" for field in self.web_form_fields):
|
||||
messages.append("Now")
|
||||
|
||||
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):
|
||||
if not self.list_columns:
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@
|
|||
"field_order": [
|
||||
"fieldname",
|
||||
"fieldtype",
|
||||
"label"
|
||||
"label",
|
||||
"options"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
|
|
@ -30,19 +31,26 @@
|
|||
"in_list_view": 1,
|
||||
"label": "Fieldtype",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "options",
|
||||
"fieldtype": "Text",
|
||||
"in_list_view": 1,
|
||||
"label": "Options"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-23 16:04:02.310851",
|
||||
"modified": "2025-09-24 22:28:54.931089",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Website",
|
||||
"name": "Web Form List Column",
|
||||
"naming_rule": "Autoincrement",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ class WebFormListColumn(Document):
|
|||
fieldtype: DF.Data | None
|
||||
label: DF.Data | None
|
||||
name: DF.Int | None
|
||||
options: DF.Text | None
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
|
|
|
|||
|
|
@ -4,8 +4,10 @@
|
|||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"column_break_mzuh",
|
||||
"source",
|
||||
"target",
|
||||
"forward_query_parameters",
|
||||
"redirect_http_status"
|
||||
],
|
||||
"fields": [
|
||||
|
|
@ -29,18 +31,30 @@
|
|||
"fieldtype": "Select",
|
||||
"label": "Redirect HTTP Status",
|
||||
"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,
|
||||
"links": [],
|
||||
"modified": "2024-03-23 16:04:03.818023",
|
||||
"modified": "2025-10-06 11:45:35.866316",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Website",
|
||||
"name": "Website Route Redirect",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "ASC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ class WebsiteRouteRedirect(Document):
|
|||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
forward_query_parameters: DF.Check
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
|
|
|
|||
|
|
@ -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 "/")
|
||||
if redirect_to:
|
||||
if isinstance(redirect_to, dict):
|
||||
frappe.flags.redirect_location = redirect_to["path"]
|
||||
raise frappe.Redirect(redirect_to["status_code"])
|
||||
raise_redirect(
|
||||
redirect_to["path"],
|
||||
redirect_to.get("status_code", 301),
|
||||
redirect_to.get("forward_query_parameters", False),
|
||||
)
|
||||
frappe.flags.redirect_location = redirect_to
|
||||
raise frappe.Redirect
|
||||
|
||||
|
|
@ -128,7 +138,12 @@ def resolve_redirect(path, query_string=None):
|
|||
|
||||
redirects = frappe.get_hooks("website_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 [])
|
||||
]
|
||||
|
||||
|
|
@ -148,12 +163,19 @@ def resolve_redirect(path, query_string=None):
|
|||
|
||||
if 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
|
||||
|
||||
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)
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ from frappe.model.workflow import (
|
|||
send_email_alert,
|
||||
)
|
||||
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.data import get_link_to_form
|
||||
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.completed_by, user)
|
||||
.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)
|
||||
).run()
|
||||
|
||||
|
|
|
|||
|
|
@ -8,5 +8,7 @@ sitemap = 1
|
|||
|
||||
def get_context(context):
|
||||
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
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ sitemap = 1
|
|||
|
||||
def get_context(context):
|
||||
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:
|
||||
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)
|
||||
@rate_limit(limit=1000, seconds=60 * 60)
|
||||
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)
|
||||
|
||||
message = escape_html(message)
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ dependencies = [
|
|||
"PyQRCode~=1.2.1",
|
||||
"PyYAML~=6.0.2",
|
||||
"RestrictedPython~=8.0",
|
||||
"AccessControl~=7.2",
|
||||
"WeasyPrint==66.0",
|
||||
"pydyf==0.11.0",
|
||||
"Werkzeug==3.1.3",
|
||||
|
|
|
|||
|
|
@ -16,8 +16,12 @@ function authenticate_with_frappe(socket, next) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!socket.request.headers.cookie) {
|
||||
next(new Error("No cookie transmitted."));
|
||||
if (!socket.request.headers.cookie && !socket.request.headers.authorization) {
|
||||
next(
|
||||
new Error(
|
||||
"Missing cookie and authorization header. Either one needed for authentication."
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue