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
|
- 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 }}
|
||||||
|
|
|
||||||
|
|
@ -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: []
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
|
@ -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 });
|
||||||
|
|
|
||||||
|
|
@ -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" },
|
||||||
{
|
{
|
||||||
|
|
|
||||||
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,
|
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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
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",
|
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 = {
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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 = "";
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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") {
|
||||||
|
|
|
||||||
|
|
@ -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 = [];
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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"]);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
align-items: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-area {
|
|
||||||
margin-top: 0.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label-area {
|
.label-area {
|
||||||
white-space: unset;
|
white-space: unset;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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
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,
|
"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",
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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": [],
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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": [],
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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": []
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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": []
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue