Merge pull request #17741 from hrwX/merge_translated_doctypes

refactor: translatable doctypes
This commit is contained in:
mergify[bot] 2022-08-13 12:21:32 +00:00 committed by GitHub
commit 21a45dab1c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 172 additions and 260 deletions

View file

@ -26,15 +26,15 @@ context("Control Link", () => {
});
}
function get_dialog_with_user_link() {
function get_dialog_with_gender_link() {
return cy.dialog({
title: "Link",
fields: [
{
label: "Select User",
label: "Select Gender",
fieldname: "link",
fieldtype: "Link",
options: "User",
options: "Gender",
},
],
});
@ -43,19 +43,6 @@ context("Control Link", () => {
it("should set the valid value", () => {
get_dialog_with_link().as("dialog");
cy.insert_doc(
"Property Setter",
{
doctype: "Property Setter",
doc_type: "User",
property: "translate_link_fields",
property_type: "Check",
doctype_or_field: "DocType",
value: "0",
},
true
);
cy.insert_doc(
"Property Setter",
{
@ -133,19 +120,6 @@ context("Control Link", () => {
});
it("show title field in link", () => {
cy.insert_doc(
"Property Setter",
{
doctype: "Property Setter",
doc_type: "User",
property: "translate_link_fields",
property_type: "Check",
doctype_or_field: "DocType",
value: "0",
},
true
);
cy.insert_doc(
"Property Setter",
{
@ -275,142 +249,54 @@ context("Control Link", () => {
);
});
it("show translated text for link with show_title_field_in_link enabled", () => {
cy.insert_doc(
"Property Setter",
{
doctype: "Property Setter",
doc_type: "ToDo",
property: "translate_link_fields",
property_type: "Check",
doctype_or_field: "DocType",
value: "1",
},
true
);
cy.insert_doc(
"Property Setter",
{
doctype: "Property Setter",
doc_type: "ToDo",
property: "show_title_field_in_link",
property_type: "Check",
doctype_or_field: "DocType",
value: "1",
},
true
);
cy.window()
.its("frappe")
.then((frappe) => {
cy.insert_doc("Translation", {
doctype: "Translation",
language: frappe.boot.lang,
source_text: "this is a test todo for link",
translated_text: "this is a translated test todo for link",
it("show translated text for Gender link field with language de with input in de", () => {
cy.call("frappe.tests.ui_test_helpers.insert_translations").then(() => {
cy.window()
.its("frappe")
.then((frappe) => {
cy.set_value("User", frappe.user.name, { language: "de" });
});
});
cy.clear_cache();
cy.wait(500);
cy.clear_cache();
cy.wait(500);
cy.window()
.its("frappe")
.then((frappe) => {
if (!frappe.boot) {
frappe.boot = {
link_title_doctypes: ["ToDo"],
translatable_doctypes: ["ToDo"],
};
} else {
frappe.boot.link_title_doctypes = ["ToDo"];
frappe.boot.translatable_doctypes = ["ToDo"];
}
});
get_dialog_with_gender_link().as("dialog");
cy.intercept("POST", "/api/method/frappe.desk.search.search_link").as("search_link");
get_dialog_with_link().as("dialog");
cy.intercept("POST", "/api/method/frappe.desk.search.search_link").as("search_link");
cy.get(".frappe-control[data-fieldname=link] input").focus().as("input");
cy.wait("@search_link");
cy.get("@input").type("todo for link", { delay: 100 });
cy.wait("@search_link");
cy.get(".frappe-control[data-fieldname=link] ul").should("be.visible");
cy.get(".frappe-control[data-fieldname=link] input").type("{enter}", { delay: 100 });
cy.get(".frappe-control[data-fieldname=link] input").blur();
cy.get("@dialog").then((dialog) => {
cy.get("@todos").then((todos) => {
cy.get(".frappe-control[data-fieldname=link] input").focus().as("input");
cy.wait("@search_link");
cy.get("@input").type("Sonstiges", { delay: 100 });
cy.wait("@search_link");
cy.get(".frappe-control[data-fieldname=link] ul").should("be.visible");
cy.get(".frappe-control[data-fieldname=link] input").type("{enter}", { delay: 100 });
cy.get(".frappe-control[data-fieldname=link] input").blur();
cy.get("@dialog").then((dialog) => {
let field = dialog.get_field("link");
let value = field.get_value();
let label = field.get_label_value();
expect(value).to.eq(todos[0]);
expect(label).to.eq("this is a translated test todo for link");
expect(value).to.eq("Other");
expect(label).to.eq("Sonstiges");
});
});
});
it("show translated text for link with show_title_field_in_link disabled", () => {
cy.insert_doc(
"Property Setter",
{
doctype: "Property Setter",
doc_type: "User",
property: "translate_link_fields",
property_type: "Check",
doctype_or_field: "DocType",
value: "1",
},
true
);
cy.insert_doc(
"Property Setter",
{
doctype: "Property Setter",
doc_type: "ToDo",
property: "show_title_field_in_link",
property_type: "Check",
doctype_or_field: "DocType",
value: "0",
},
true
);
it("show text for Gender link field with language en", () => {
cy.window()
.its("frappe")
.then((frappe) => {
cy.insert_doc("Translation", {
doctype: "Translation",
language: frappe.boot.lang,
source_text: "test@erpnext.com",
translated_text: "translatedtest@erpnext.com",
});
cy.set_value("User", frappe.user.name, { language: "en" });
});
cy.clear_cache();
cy.wait(500);
cy.window()
.its("frappe")
.then((frappe) => {
if (!frappe.boot) {
frappe.boot = {
translatable_doctypes: ["User"],
};
} else {
frappe.boot.translatable_doctypes = ["User"];
}
});
get_dialog_with_user_link().as("dialog");
get_dialog_with_gender_link().as("dialog");
cy.intercept("POST", "/api/method/frappe.desk.search.search_link").as("search_link");
cy.get(".frappe-control[data-fieldname=link] input").focus().as("input");
cy.wait("@search_link");
cy.get("@input").type("test@erpnext.com", { delay: 100 });
cy.get("@input").type("Non-Conforming", { delay: 100 });
cy.wait("@search_link");
cy.get(".frappe-control[data-fieldname=link] ul").should("be.visible");
cy.get(".frappe-control[data-fieldname=link] input").type("{enter}", { delay: 100 });
@ -420,8 +306,8 @@ context("Control Link", () => {
let value = field.get_value();
let label = field.get_label_value();
expect(value).to.eq("test@erpnext.com");
expect(label).to.eq("translatedtest@erpnext.com");
expect(value).to.eq("Non-Conforming");
expect(label).to.eq("Non-Conforming");
});
});

View file

@ -237,7 +237,7 @@ context("Web Form", () => {
cy.get(".web-form-actions a").contains("Edit").click();
cy.fill_field("last_name", "_Test User");
cy.fill_field("middle_name", "_Test User");
cy.get(".web-form-actions .btn-primary").click();
cy.url().should("include", "/me");
@ -249,7 +249,7 @@ context("Web Form", () => {
cy.get(".web-form-actions a").contains("Edit").click();
cy.fill_field("last_name", "_Test User");
cy.fill_field("middle_name", "_Test User");
cy.get(".btn-next").should("be.visible");
cy.get(".btn-next").click();

View file

@ -19,7 +19,7 @@ from frappe.social.doctype.energy_point_log.energy_point_log import get_energy_p
from frappe.social.doctype.energy_point_settings.energy_point_settings import (
is_energy_point_enabled,
)
from frappe.translate import get_lang_dict, get_messages_for_boot
from frappe.translate import get_lang_dict, get_messages_for_boot, get_translated_doctypes
from frappe.utils import add_user_info, cstr, get_time_zone
from frappe.utils.change_log import get_versions
from frappe.website.doctype.web_page_view.web_page_view import is_tracking_enabled
@ -100,7 +100,7 @@ def get_bootinfo():
bootinfo.desk_settings = get_desk_settings()
bootinfo.app_logo_url = get_app_logo()
bootinfo.link_title_doctypes = get_link_title_doctypes()
bootinfo.translatable_doctypes = get_translatable_doctypes()
bootinfo.translated_doctypes = get_translated_doctypes()
return bootinfo
@ -399,14 +399,6 @@ def set_time_zone(bootinfo):
}
def get_translatable_doctypes():
dts = frappe.get_all("DocType", {"translate_link_fields": 1}, pluck="name")
custom_dts = frappe.get_all(
"Property Setter", {"property": "translate_link_fields", "value": "1"}, pluck="doc_type"
)
return dts + custom_dts
def load_country_doc(bootinfo):
country = frappe.db.get_default("country")
if not country:

View file

@ -17,7 +17,7 @@
}
],
"links": [],
"modified": "2022-08-03 12:20:48.408685",
"modified": "2022-08-05 18:33:28.043370",
"modified_by": "Administrator",
"module": "Contacts",
"name": "Gender",
@ -43,5 +43,6 @@
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
"track_changes": 1,
"translated_doctype": 1
}

View file

@ -18,7 +18,7 @@
}
],
"links": [],
"modified": "2022-08-03 12:20:48.954912",
"modified": "2022-08-05 18:33:28.196387",
"modified_by": "Administrator",
"module": "Contacts",
"name": "Salutation",
@ -56,5 +56,6 @@
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
"track_changes": 1,
"translated_doctype": 1
}

View file

@ -47,7 +47,7 @@
"view_settings",
"title_field",
"show_title_field_in_link",
"translate_link_fields",
"translated_doctype",
"search_fields",
"default_print_format",
"sort_field",
@ -595,7 +595,7 @@
},
{
"default": "0",
"fieldname": "translate_link_fields",
"fieldname": "translated_doctype",
"fieldtype": "Check",
"label": "Translate Link Fields"
}
@ -680,7 +680,7 @@
"link_fieldname": "reference_doctype"
}
],
"modified": "2022-02-28 21:56:52.116915",
"modified": "2022-08-05 18:33:27.315351",
"modified_by": "Administrator",
"module": "Core",
"name": "DocType",
@ -716,5 +716,5 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1,
"translate_link_fields": 1
"translated_doctype": 1
}

View file

@ -148,7 +148,7 @@
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2022-01-12 20:18:18.496230",
"modified": "2022-08-05 18:33:27.694065",
"modified_by": "Administrator",
"module": "Core",
"name": "Role",
@ -171,5 +171,6 @@
"sort_field": "modified",
"sort_order": "ASC",
"states": [],
"track_changes": 1
"track_changes": 1,
"translated_doctype": 1
}

View file

@ -29,7 +29,7 @@
"view_settings_section",
"title_field",
"show_title_field_in_link",
"translate_link_fields",
"translated_doctype",
"image_field",
"default_print_format",
"column_break_29",
@ -315,7 +315,7 @@
},
{
"default": "0",
"fieldname": "translate_link_fields",
"fieldname": "translated_doctype",
"fieldtype": "Check",
"label": "Translate Link Fields"
}
@ -326,7 +326,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2022-05-13 15:36:16.772277",
"modified": "2022-08-04 15:36:16.772277",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customize Form",

View file

@ -585,7 +585,7 @@ doctype_properties = {
"naming_rule": "Data",
"autoname": "Data",
"show_title_field_in_link": "Check",
"translate_link_fields": "Check",
"translated_doctype": "Check",
}
docfield_properties = {

View file

@ -226,7 +226,7 @@ CREATE TABLE `tabDocType` (
`sender_field` varchar(255) DEFAULT NULL,
`show_title_field_in_link` int(1) NOT NULL DEFAULT 0,
`migration_hash` varchar(255) DEFAULT NULL,
`translate_link_fields` int(1) NOT NULL DEFAULT 0,
`translated_doctype` int(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`name`)
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

View file

@ -231,7 +231,7 @@ CREATE TABLE "tabDocType" (
"sender_field" varchar(255) DEFAULT NULL,
"show_title_field_in_link" smallint NOT NULL DEFAULT 0,
"migration_hash" varchar(255) DEFAULT NULL,
"translate_link_fields" smallint NOT NULL DEFAULT 0,
"translated_doctype" smallint NOT NULL DEFAULT 0,
PRIMARY KEY ("name")
) ;

View file

@ -8,6 +8,7 @@ import re
import frappe
from frappe import _, is_whitelisted
from frappe.permissions import has_permission
from frappe.translate import get_translated_doctypes
from frappe.utils import cint, cstr, unique
@ -115,7 +116,10 @@ def search_widget(
raise e
else:
frappe.respond_as_web_page(
title="Invalid Method", html="Method not found", indicator_color="red", http_status_code=404
title="Invalid Method",
html="Method not found",
indicator_color="red",
http_status_code=404,
)
return
except Exception as e:
@ -146,9 +150,22 @@ def search_widget(
filters = []
or_filters = []
translated_search_doctypes = frappe.get_hooks("translated_search_doctypes")
translated_doctypes = frappe.cache().hget(
"translated_doctypes", "doctypes", get_translated_doctypes
)
# build from doctype
if txt:
field_types = [
"Data",
"Text",
"Small Text",
"Long Text",
"Link",
"Select",
"Read Only",
"Text Editor",
]
search_fields = ["name"]
if meta.title_field:
search_fields.append(meta.title_field)
@ -158,13 +175,8 @@ def search_widget(
for f in search_fields:
fmeta = meta.get_field(f.strip())
if (doctype not in translated_search_doctypes) and (
f == "name"
or (
fmeta
and fmeta.fieldtype
in ["Data", "Text", "Small Text", "Long Text", "Link", "Select", "Read Only", "Text Editor"]
)
if (doctype not in translated_doctypes) and (
f == "name" or (fmeta and fmeta.fieldtype in field_types)
):
or_filters.append([doctype, f.strip(), "like", f"%{txt}%"])
@ -188,7 +200,8 @@ def search_widget(
# find relevance as location of search term from the beginning of string `name`. used for sorting results.
formatted_fields.append(
"""locate({_txt}, `tab{doctype}`.`name`) as `_relevance`""".format(
_txt=frappe.db.escape((txt or "").replace("%", "").replace("@", "")), doctype=doctype
_txt=frappe.db.escape((txt or "").replace("%", "").replace("@", "")),
doctype=doctype,
)
)
@ -206,7 +219,7 @@ def search_widget(
else (cint(ignore_user_permissions) and has_permission(doctype, ptype=ptype))
)
if doctype in translated_search_doctypes:
if doctype in translated_doctypes:
page_length = None
values = frappe.get_list(
@ -223,7 +236,7 @@ def search_widget(
strict=False,
)
if doctype in translated_search_doctypes:
if doctype in translated_doctypes:
# Filtering the values array so that query is included in very element
values = (
v

View file

@ -54,7 +54,7 @@
"icon": "fa fa-globe",
"idx": 1,
"links": [],
"modified": "2020-02-24 15:44:31.837133",
"modified": "2022-08-05 18:33:27.880783",
"modified_by": "Administrator",
"module": "Geo",
"name": "Country",
@ -84,5 +84,7 @@
"quick_entry": 1,
"sort_field": "country_name",
"sort_order": "ASC",
"track_changes": 1
"states": [],
"track_changes": 1,
"translated_doctype": 1
}

View file

@ -373,5 +373,3 @@ override_whitelisted_methods = {
"frappe.core.doctype.file.file.move_file": "frappe.core.api.file.move_file",
"frappe.core.doctype.file.file.zip_files": "frappe.core.api.file.zip_files",
}
translated_search_doctypes = ["DocType", "Role", "Country", "Gender", "Salutation"]

View file

@ -87,7 +87,7 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
return this.is_translatable() ? __(value) : value;
}
is_translatable() {
return in_list(frappe.boot?.translatable_doctypes || [], this.get_options());
return in_list(frappe.boot?.translated_doctypes || [], this.get_options());
}
set_link_title(value) {
let doctype = this.get_options();
@ -391,22 +391,6 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
me.$input.val("");
}
});
this.$input.on("focus", function () {
if (!frappe.boot.translated_search_doctypes.includes(me.df.options)) {
me.show_untranslated();
}
});
this.$input.keydown((e) => {
let BACKSPACE = 8;
if (
e.keyCode === BACKSPACE &&
!frappe.boot.translated_search_doctypes.includes(me.df.options)
) {
me.show_untranslated();
}
});
}
show_untranslated() {

View file

@ -184,7 +184,6 @@ def get():
frappe.get_attr(hook)(bootinfo=bootinfo)
bootinfo["lang"] = frappe.translate.get_user_lang()
bootinfo["translated_search_doctypes"] = frappe.get_hooks("translated_search_doctypes")
bootinfo["disable_async"] = frappe.conf.disable_async
bootinfo["setup_complete"] = cint(frappe.get_system_settings("setup_complete"))

View file

@ -333,3 +333,37 @@ def insert_doctype_with_child_table_record(name):
insert_child(doc, "Drag", "08189DIHAA2981", 0, 0.7, 342628, "2022-05-04")
doc.insert()
@frappe.whitelist()
def insert_translations():
translation = [
{
"doctype": "Translation",
"language": "de",
"source_text": "Other",
"translated_text": "Sonstiges",
},
{
"doctype": "Translation",
"language": "de",
"source_text": "Genderqueer",
"translated_text": "Nichtbinär",
},
{
"doctype": "Translation",
"language": "de",
"source_text": "Non-Conforming",
"translated_text": "Nicht konform",
},
{
"doctype": "Translation",
"language": "de",
"source_text": "Prefer not to say",
"translated_text": "Keine Angabe",
},
]
for doc in translation:
if not frappe.db.exists("doc"):
frappe.get_doc(doc).insert()

View file

@ -23,7 +23,7 @@ from pypika.terms import PseudoColumn
import frappe
from frappe.model.utils import InvalidIncludePath, render_include
from frappe.query_builder import DocType, Field
from frappe.utils import cstr, get_bench_path, is_html, strip, strip_html_tags
from frappe.utils import cstr, get_bench_path, is_html, strip, strip_html_tags, unique
TRANSLATE_PATTERN = re.compile(
r"_\(\s*" # starts with literal `_(`, ignore following whitespace/newlines
@ -1294,3 +1294,11 @@ def set_preferred_language_cookie(preferred_language):
def get_preferred_language_cookie():
return frappe.request.cookies.get("preferred_language")
def get_translated_doctypes():
dts = frappe.get_all("DocType", {"translated_doctype": 1}, pluck="name")
custom_dts = frappe.get_all(
"Property Setter", {"property": "translated_doctype", "value": "1"}, pluck="doc_type"
)
return unique(dts + custom_dts)

View file

@ -79,7 +79,8 @@ def is_valid_title(title) -> bool:
def _create_app_boilerplate(dest, hooks, no_git=False):
frappe.create_folder(
os.path.join(dest, hooks.app_name, hooks.app_name, frappe.scrub(hooks.app_title)), with_init=True
os.path.join(dest, hooks.app_name, hooks.app_name, frappe.scrub(hooks.app_title)),
with_init=True,
)
frappe.create_folder(
os.path.join(dest, hooks.app_name, hooks.app_name, "templates"), with_init=True
@ -249,8 +250,8 @@ app_license = "{app_license}"
# add methods and filters to jinja environment
# jinja = {{
# "methods": "{app_name}.utils.jinja_methods",
# "filters": "{app_name}.utils.jinja_filters"
# "methods": "{app_name}.utils.jinja_methods",
# "filters": "{app_name}.utils.jinja_filters"
# }}
# Installation
@ -276,11 +277,11 @@ app_license = "{app_license}"
# Permissions evaluated in scripted ways
# permission_query_conditions = {{
# "Event": "frappe.desk.doctype.event.event.get_permission_query_conditions",
# "Event": "frappe.desk.doctype.event.event.get_permission_query_conditions",
# }}
#
# has_permission = {{
# "Event": "frappe.desk.doctype.event.event.has_permission",
# "Event": "frappe.desk.doctype.event.event.has_permission",
# }}
# DocType Class
@ -288,7 +289,7 @@ app_license = "{app_license}"
# Override standard doctype classes
# override_doctype_class = {{
# "ToDo": "custom_app.overrides.CustomToDo"
# "ToDo": "custom_app.overrides.CustomToDo"
# }}
# Document Events
@ -296,10 +297,10 @@ app_license = "{app_license}"
# Hook on document methods and events
# doc_events = {{
# "*": {{
# "on_update": "method",
# "on_cancel": "method",
# "on_trash": "method"
# "*": {{
# "on_update": "method",
# "on_cancel": "method",
# "on_trash": "method"
# }}
# }}
@ -307,21 +308,21 @@ app_license = "{app_license}"
# ---------------
# scheduler_events = {{
# "all": [
# "{app_name}.tasks.all"
# ],
# "daily": [
# "{app_name}.tasks.daily"
# ],
# "hourly": [
# "{app_name}.tasks.hourly"
# ],
# "weekly": [
# "{app_name}.tasks.weekly"
# ],
# "monthly": [
# "{app_name}.tasks.monthly"
# ],
# "all": [
# "{app_name}.tasks.all"
# ],
# "daily": [
# "{app_name}.tasks.daily"
# ],
# "hourly": [
# "{app_name}.tasks.hourly"
# ],
# "weekly": [
# "{app_name}.tasks.weekly"
# ],
# "monthly": [
# "{app_name}.tasks.monthly"
# ],
# }}
# Testing
@ -333,14 +334,14 @@ app_license = "{app_license}"
# ------------------------------
#
# override_whitelisted_methods = {{
# "frappe.desk.doctype.event.event.get_events": "{app_name}.event.get_events"
# "frappe.desk.doctype.event.event.get_events": "{app_name}.event.get_events"
# }}
#
# each overriding function accepts a `data` argument;
# generated from the base implementation of the doctype dashboard,
# along with any modifications made in other Frappe apps
# override_doctype_dashboards = {{
# "Task": "{app_name}.task.get_dashboard_data"
# "Task": "{app_name}.task.get_dashboard_data"
# }}
# exempt linked doctypes from being automatically cancelled
@ -352,40 +353,32 @@ app_license = "{app_license}"
# --------------------
# user_data_fields = [
# {{
# "doctype": "{{doctype_1}}",
# "filter_by": "{{filter_by}}",
# "redact_fields": ["{{field_1}}", "{{field_2}}"],
# "partial": 1,
# }},
# {{
# "doctype": "{{doctype_2}}",
# "filter_by": "{{filter_by}}",
# "partial": 1,
# }},
# {{
# "doctype": "{{doctype_3}}",
# "strict": False,
# }},
# {{
# "doctype": "{{doctype_4}}"
# }}
# {{
# "doctype": "{{doctype_1}}",
# "filter_by": "{{filter_by}}",
# "redact_fields": ["{{field_1}}", "{{field_2}}"],
# "partial": 1,
# }},
# {{
# "doctype": "{{doctype_2}}",
# "filter_by": "{{filter_by}}",
# "partial": 1,
# }},
# {{
# "doctype": "{{doctype_3}}",
# "strict": False,
# }},
# {{
# "doctype": "{{doctype_4}}"
# }}
# ]
# Authentication and authorization
# --------------------------------
# auth_hooks = [
# "{app_name}.auth.validate"
# "{app_name}.auth.validate"
# ]
# Translation
# --------------------------------
# Make link fields search translated document names for these DocTypes
# Recommended only for DocTypes which have limited documents with untranslated names
# For example: Role, Gender, etc.
# translated_search_doctypes = []
"""
desktop_template = """from frappe import _