Merge branch 'develop' into qb-fixes

This commit is contained in:
Aradhya Tripathi 2022-10-27 20:29:32 +05:30 committed by GitHub
commit 862d7428a6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 142 additions and 117 deletions

View file

@ -41,7 +41,7 @@ jobs:
- name: Get release
id: get_release
uses: bruceadams/get-release@v1.2.3
uses: bruceadams/get-release@v1.3.1
- name: Upload built Assets to Release
uses: actions/upload-release-asset@v1.0.2

View file

@ -89,7 +89,7 @@ def _(msg: str, lang: str | None = None, context: str | None = None) -> str:
_('Change')
_('Change', context='Coins')
"""
from frappe.translate import get_full_dict
from frappe.translate import get_all_translations
from frappe.utils import is_html, strip_html_tags
if not hasattr(local, "lang"):
@ -107,14 +107,15 @@ def _(msg: str, lang: str | None = None, context: str | None = None) -> str:
msg = as_unicode(msg).strip()
translated_string = ""
all_translations = get_all_translations(lang)
if context:
string_key = f"{msg}:{context}"
translated_string = get_full_dict(lang).get(string_key)
translated_string = all_translations.get(string_key)
if not translated_string:
translated_string = get_full_dict(lang).get(msg)
translated_string = all_translations.get(msg)
# return lang_full_dict according to lang passed parameter
return translated_string or non_translated_string
@ -222,7 +223,6 @@ def init(site: str, sites_path: str = ".", new_site: bool = False) -> None:
local.conf = _dict(get_site_config())
local.lang = local.conf.lang or "en"
local.lang_full_dict = None
local.module_app = None
local.app_modules = None

View file

@ -3,6 +3,7 @@
import frappe
from frappe import _
from frappe.tests.utils import FrappeTestCase
from frappe.translate import clear_cache
class TestTranslation(FrappeTestCase):
@ -11,20 +12,17 @@ class TestTranslation(FrappeTestCase):
def tearDown(self):
frappe.local.lang = "en"
clear_translation_cache()
clear_cache()
def test_doctype(self):
translation_data = get_translation_data()
for key, val in translation_data.items():
frappe.local.lang = key
clear_translation_cache()
translation = create_translation(key, val)
self.assertEqual(_(val[0]), val[1])
frappe.delete_doc("Translation", translation.name)
clear_translation_cache()
self.assertEqual(_(val[0]), val[0])
def test_parent_language(self):
@ -55,6 +53,10 @@ class TestTranslation(FrappeTestCase):
clear_translation_cache()
self.assertTrue(_(data[1][0]), data[1][1])
def test_multi_language_translations(self):
source = "User"
self.assertNotEqual(_(source, lang="de"), _(source, lang="es"))
def test_html_content_data_translation(self):
source = """
<span style="color: rgb(51, 51, 51); font-family: &quot;Amazon Ember&quot;, Arial, sans-serif; font-size:
@ -113,5 +115,4 @@ def create_translation(key, val):
def clear_translation_cache():
frappe.local.lang_full_dict = None
frappe.cache().delete_key("lang_full_dict", shared=True)
frappe.cache().delete_key("translations_from_apps", shared=True)

View file

@ -5,7 +5,7 @@ import json
import frappe
from frappe.model.document import Document
from frappe.translate import get_translator_url
from frappe.translate import MERGED_TRANSLATION_KEY, USER_TRANSLATION_KEY, get_translator_url
from frappe.utils import is_html, strip_html_tags
@ -89,4 +89,5 @@ def create_translations(translation_map, language):
def clear_user_translation_cache(lang):
frappe.cache().hdel("lang_user_translations", lang)
frappe.cache().hdel(USER_TRANSLATION_KEY, lang)
frappe.cache().hdel(MERGED_TRANSLATION_KEY, lang)

View file

@ -17,6 +17,16 @@ frappe.ui.form.on("User", {
}
},
time_zone: function (frm) {
if (frm.doc.time_zone && frm.doc.time_zone.startsWith("Etc")) {
frm.set_df_property(
"time_zone",
"description",
__("Note: Etc timezones have their signs reversed.")
);
}
},
role_profile_name: function (frm) {
if (frm.doc.role_profile_name) {
frappe.call({
@ -259,6 +269,7 @@ frappe.ui.form.on("User", {
}
frm.dirty();
}
frm.trigger("time_zone");
},
validate: function (frm) {
if (frm.roles_editor) {

View file

@ -17,15 +17,16 @@ expected_settings_10_3_later = {
}
def get_mariadb_versions():
def get_mariadb_variables():
return frappe._dict(frappe.db.sql("show variables"))
def get_mariadb_version(version_string: str = ""):
# MariaDB classifies their versions as Major (1st and 2nd number), and Minor (3rd number)
# Example: Version 10.3.13 is Major Version = 10.3, Minor Version = 13
mariadb_variables = frappe._dict(frappe.db.sql("""show variables"""))
version_string = mariadb_variables.get("version").split("-")[0]
versions = {}
versions["major"] = version_string.split(".")[0] + "." + version_string.split(".")[1]
versions["minor"] = version_string.split(".")[2]
return versions
version_string = version_string or get_mariadb_variables().get("version")
version = version_string.split("-")[0]
return version.rsplit(".", 1)
def setup_database(force, source_sql, verbose, no_mariadb_socket=False):
@ -108,13 +109,13 @@ def import_db_from_sql(source_sql=None, verbose=False):
def check_database_settings():
versions = get_mariadb_versions()
if versions["major"] <= "10.2":
mariadb_variables = get_mariadb_variables()
versions = get_mariadb_version(mariadb_variables.get("version"))
if versions[0] <= "10.2":
expected_variables = expected_settings_10_2_earlier
else:
expected_variables = expected_settings_10_3_later
mariadb_variables = frappe._dict(frappe.db.sql("show variables"))
# Check each expected value vs. actuals:
result = True
for key, expected_value in expected_variables.items():
@ -124,18 +125,16 @@ def check_database_settings():
% (key, expected_value, mariadb_variables.get(key))
)
result = False
if not result:
print(
(
"=" * 80 + "\n"
"Creation of your site - {x} failed because MariaDB is not properly {sep}"
"{sep2}Creation of your site - {site} failed because MariaDB is not properly {sep}"
"configured. If using version 10.2.x or earlier, make sure you use the {sep}"
"the Barracuda storage engine. {sep}{sep}"
"Please verify the settings above in MariaDB's my.cnf. Restart MariaDB. And {sep}"
"then run `bench new-site {x}` again.{sep2}"
""
"=" * 80
).format(x=frappe.local.site, sep2="\n" * 2, sep="\n")
"the Barracuda storage engine.{sep2}"
"Please verify the above settings in MariaDB's my.cnf. Restart MariaDB.{sep}"
"And then run `bench new-site {site}` again.{sep2}"
).format(site=frappe.local.site, sep2="\n\n", sep="\n")
)
return result

View file

@ -17,10 +17,10 @@ frappe.listview_settings["ToDo"] = {
return doc.reference_name;
},
get_label: function () {
return __("Open");
return __("Open", null, "Access");
},
get_description: function (doc) {
return __("Open {0}", [`${doc.reference_type} ${doc.reference_name}`]);
return __("Open {0}", [`${__(doc.reference_type)}: ${doc.reference_name}`]);
},
action: function (doc) {
frappe.set_route("Form", doc.reference_type, doc.reference_name);

View file

@ -604,6 +604,9 @@ class Email:
fname = get_random_filename(content_type=content_type)
else:
fname = get_random_filename(content_type=content_type)
# Don't clobber existing filename
while fname in self.cid_map:
fname = get_random_filename(content_type=content_type)
self.attachments.append(
{

View file

@ -12,8 +12,8 @@ import { useRoute } from "vue-router"
let route = useRoute();
watch(route, () => {
frappe.router.current_route = frappe.router.parse();
watch(route, async () => {
frappe.router.current_route = await frappe.router.parse();
frappe.breadcrumbs.update();
frappe.recorder.route = route;
});

View file

@ -38,16 +38,17 @@ $("body").on("click", "a", function (e) {
return false;
};
const href = e.currentTarget.getAttribute("href");
const target_element = e.currentTarget;
const href = target_element.getAttribute("href");
const is_on_same_host = target_element.hostname === window.location.hostname;
// click handled, but not by href
if (
e.currentTarget.getAttribute("onclick") || // has a handler
target_element.getAttribute("onclick") || // has a handler
e.ctrlKey ||
e.metaKey || // open in a new tab
href === "#"
href === "#" // hash is home
) {
// hash is home
return;
}
@ -57,20 +58,20 @@ $("body").on("click", "a", function (e) {
if (href && href.startsWith("#")) {
// target startswith "#", this is a v1 style route, so remake it.
return override(e.currentTarget.hash);
return override(target_element.hash);
}
if (frappe.router.is_app_route(e.currentTarget.pathname)) {
if (is_on_same_host && frappe.router.is_app_route(target_element.pathname)) {
// target has "/app, this is a v2 style route.
if (e.currentTarget.search) {
if (target_element.search) {
frappe.route_options = {};
let params = new URLSearchParams(e.currentTarget.search);
let params = new URLSearchParams(target_element.search);
for (const [key, value] of params) {
frappe.route_options[key] = value;
}
}
return override(e.currentTarget.pathname + e.currentTarget.hash);
return override(target_element.pathname + target_element.hash);
}
});
@ -88,6 +89,7 @@ frappe.router = {
"dashboard",
"image",
"inbox",
"map",
],
list_views_route: {
list: "List",
@ -100,6 +102,7 @@ frappe.router = {
image: "Image",
inbox: "Inbox",
file: "Home",
map: "Map",
},
layout_mapped: {},

View file

@ -135,7 +135,6 @@ class TestSearch(FrappeTestCase):
def test_link_search_in_foreign_language(self):
try:
frappe.local.lang = "fr"
frappe.local.lang_full_dict = None # discard translation cache
search_widget(doctype="DocType", txt="pay", page_length=20)
output = frappe.response["values"]

View file

@ -108,7 +108,6 @@ def _restore_thread_locals(flags):
frappe.local.conf = frappe._dict(frappe.get_site_config())
frappe.local.cache = {}
frappe.local.lang = "en"
frappe.local.lang_full_dict = None
frappe.local.preload_assets = {"style": [], "script": []}

View file

@ -53,6 +53,12 @@ REPORT_TRANSLATE_PATTERN = re.compile('"([^:,^"]*):')
CSV_STRIP_WHITESPACE_PATTERN = re.compile(r"{\s?([0-9]+)\s?}")
# Cache keys
MERGED_TRANSLATION_KEY = "merged_translations"
APP_TRANSLATION_KEY = "translations_from_apps"
USER_TRANSLATION_KEY = "lang_user_translations"
def get_language(lang_list: list = None) -> str:
"""Set `frappe.local.lang` from HTTP headers at beginning of request
@ -215,7 +221,7 @@ def get_dict(fortype: str, name: str | None = None) -> dict[str, str]:
def get_messages_for_boot():
"""Return all message translations that are required on boot."""
messages = get_full_dict(frappe.local.lang)
messages = get_all_translations(frappe.local.lang)
messages.update(get_dict_from_hooks("boot", None))
return messages
@ -241,9 +247,9 @@ def make_dict_from_messages(messages, full_dict=None, load_user_translation=True
out = {}
if full_dict is None:
if load_user_translation:
full_dict = get_full_dict(frappe.local.lang)
full_dict = get_all_translations(frappe.local.lang)
else:
full_dict = load_lang(frappe.local.lang)
full_dict = get_translations_from_apps(frappe.local.lang)
for m in messages:
if m[1] in full_dict:
@ -266,31 +272,29 @@ def get_lang_js(fortype: str, name: str) -> str:
return f"\n\n$.extend(frappe._messages, {json.dumps(get_dict(fortype, name))})"
def get_full_dict(lang: str) -> dict[str, str]:
"""Load and return the entire translations dictionary for a language from :meth:`frape.cache`
def get_all_translations(lang: str) -> dict[str, str]:
"""Load and return the entire translations dictionary for a language from apps + user translations.
:param lang: Language Code, e.g. `hi`
"""
if not lang:
return {}
# found in local, return!
if getattr(frappe.local, "lang_full_dict", None) is not None:
return frappe.local.lang_full_dict
def _merge_translations():
all_translations = get_translations_from_apps(lang).copy()
try:
# get user specific translation data
user_translations = get_user_translations(lang)
all_translations.update(user_translations)
except Exception:
pass
frappe.local.lang_full_dict = load_lang(lang)
return all_translations
try:
# get user specific translation data
user_translations = get_user_translations(lang)
frappe.local.lang_full_dict.update(user_translations)
except Exception:
pass
return frappe.local.lang_full_dict
return frappe.cache().hget(MERGED_TRANSLATION_KEY, lang, generator=_merge_translations)
def load_lang(lang, apps=None):
def get_translations_from_apps(lang, apps=None):
"""Combine all translations from `.csv` files in all `apps`.
For derivative languages (es-GT), take translations from the
base language (es) and then update translations from the child (es-GT)"""
@ -298,22 +302,20 @@ def load_lang(lang, apps=None):
if lang == "en":
return {}
out = frappe.cache().hget("lang_full_dict", lang, shared=True)
if not out:
out = {}
def _get_from_disk():
translations = {}
for app in apps or frappe.get_all_apps(True):
path = os.path.join(frappe.get_pymodule_path(app), "translations", lang + ".csv")
out.update(get_translation_dict_from_file(path, lang, app) or {})
translations.update(get_translation_dict_from_file(path, lang, app) or {})
if "-" in lang:
parent = lang.split("-")[0]
parent_out = load_lang(parent)
parent_out.update(out)
out = parent_out
parent_translations = get_translations_from_apps(parent)
parent_translations.update(translations)
return parent_translations
frappe.cache().hset("lang_full_dict", lang, out, shared=True)
return translations
return out or {}
return frappe.cache().hget(APP_TRANSLATION_KEY, lang, shared=True, generator=_get_from_disk)
def get_translation_dict_from_file(path, lang, app, throw=False) -> dict[str, str]:
@ -342,23 +344,22 @@ def get_translation_dict_from_file(path, lang, app, throw=False) -> dict[str, st
def get_user_translations(lang):
if not frappe.db:
frappe.connect()
out = frappe.cache().hget("lang_user_translations", lang)
if out is None:
out = {}
user_translations = frappe.get_all(
def _read_from_db():
user_translations = {}
translations = frappe.get_all(
"Translation", fields=["source_text", "translated_text", "context"], filters={"language": lang}
)
for translation in user_translations:
key = translation.source_text
value = translation.translated_text
if translation.context:
key += ":" + translation.context
out[key] = value
for t in translations:
key = t.source_text
value = t.translated_text
if t.context:
key += ":" + t.context
user_translations[key] = value
return user_translations
frappe.cache().hset("lang_user_translations", lang, out)
return out
return frappe.cache().hget(USER_TRANSLATION_KEY, lang, generator=_read_from_db)
def clear_cache():
@ -368,9 +369,10 @@ def clear_cache():
# clear translations saved in boot cache
cache.delete_key("bootinfo")
cache.delete_key("lang_full_dict", shared=True)
cache.delete_key("translation_assets", shared=True)
cache.delete_key("lang_user_translations")
cache.delete_key(APP_TRANSLATION_KEY, shared=True)
cache.delete_key(USER_TRANSLATION_KEY)
cache.delete_key(MERGED_TRANSLATION_KEY)
def get_messages_for_app(app, deduplicate=True):
@ -1050,7 +1052,7 @@ def get_untranslated(lang, untranslated_file, get_all=False, app="_ALL_APPS"):
# replace \n with ||| so that internal linebreaks don't get split
f.write((escape_newlines(m[1]) + os.linesep).encode("utf-8"))
else:
full_dict = get_full_dict(lang)
full_dict = get_all_translations(lang)
for m in messages:
if not full_dict.get(m[1]):
@ -1073,7 +1075,7 @@ def update_translations(lang, untranslated_file, translated_file, app="_ALL_APPS
:param untranslated_file: File path with the messages in English.
:param translated_file: File path with messages in language to be updated."""
clear_cache()
full_dict = get_full_dict(lang)
full_dict = get_all_translations(lang)
def restore_newlines(s):
return (
@ -1110,7 +1112,7 @@ def update_translations(lang, untranslated_file, translated_file, app="_ALL_APPS
def import_translations(lang, path):
"""Import translations from file in standard format"""
clear_cache()
full_dict = get_full_dict(lang)
full_dict = get_all_translations(lang)
full_dict.update(get_translation_dict_from_file(path, lang, "import"))
for app in frappe.get_all_apps(True):
@ -1140,7 +1142,9 @@ def write_translations_file(app, lang, full_dict=None, app_messages=None):
tpath = frappe.get_pymodule_path(app, "translations")
frappe.create_folder(tpath)
write_csv_file(os.path.join(tpath, lang + ".csv"), app_messages, full_dict or get_full_dict(lang))
write_csv_file(
os.path.join(tpath, lang + ".csv"), app_messages, full_dict or get_all_translations(lang)
)
def send_translations(translation_dict):
@ -1302,3 +1306,8 @@ def get_translated_doctypes():
"Property Setter", {"property": "translated_doctype", "value": "1"}, pluck="doc_type"
)
return unique(dts + custom_dts)
# Backward compatibility
get_full_dict = get_all_translations
load_lang = get_translations_from_apps

View file

@ -2593,6 +2593,7 @@ Tree,Baum,
Trigger Method,Trigger-Methode,
Trigger Name,Name des Auslösers,
"Trigger on valid methods like ""before_insert"", ""after_update"", etc (will depend on the DocType selected)","Trigger auf gültige Methoden wie &quot;before_insert&quot;, &quot;after_update&quot; usw. (hängt von der DocType ausgewählt)",
Try a naming Series, Nummernkreis testen,
Try to avoid repeated words and characters,"Versuchen Sie, wiederholte Wörter und Zeichen zu vermeiden",
Try to use a longer keyboard pattern with more turns,"Versuchen Sie, eine längere Tastaturmuster mit mehr Windungen zu verwenden",
Two Factor Authentication,Zwei-Faktor-Authentifizierung,

1 A4 A4
2593 Trigger Method Trigger-Methode
2594 Trigger Name Name des Auslösers
2595 Trigger on valid methods like "before_insert", "after_update", etc (will depend on the DocType selected) Trigger auf gültige Methoden wie &quot;before_insert&quot;, &quot;after_update&quot; usw. (hängt von der DocType ausgewählt)
2596 Try a naming Series Nummernkreis testen
2597 Try to avoid repeated words and characters Versuchen Sie, wiederholte Wörter und Zeichen zu vermeiden
2598 Try to use a longer keyboard pattern with more turns Versuchen Sie, eine längere Tastaturmuster mit mehr Windungen zu verwenden
2599 Two Factor Authentication Zwei-Faktor-Authentifizierung

View file

@ -98,7 +98,6 @@ def get_context(context):
"""Build context to render the `web_form.html` template"""
context.in_edit_mode = False
context.in_view_mode = False
self.set_web_form_module()
if frappe.form_dict.is_list:
context.template = "website/doctype/web_form/templates/web_list.html"
@ -284,13 +283,14 @@ def get_context(context):
def add_custom_context_and_script(self, context):
"""Update context from module if standard and append script"""
if self.web_form_module:
new_context = self.web_form_module.get_context(context)
if self.is_standard:
web_form_module = get_web_form_module(self)
new_context = web_form_module.get_context(context)
if new_context:
context.update(new_context)
js_path = os.path.join(os.path.dirname(self.web_form_module.__file__), scrub(self.name) + ".js")
js_path = os.path.join(os.path.dirname(web_form_module.__file__), scrub(self.name) + ".js")
if os.path.exists(js_path):
script = frappe.render_template(open(js_path).read(), context)
@ -300,9 +300,7 @@ def get_context(context):
context.script = script
css_path = os.path.join(
os.path.dirname(self.web_form_module.__file__), scrub(self.name) + ".css"
)
css_path = os.path.join(os.path.dirname(web_form_module.__file__), scrub(self.name) + ".css")
if os.path.exists(css_path):
style = open(css_path).read()
@ -322,14 +320,6 @@ def get_context(context):
return parents
def set_web_form_module(self):
"""Get custom web form module if exists"""
self.web_form_module = self.get_web_form_module()
def get_web_form_module(self):
if self.is_standard:
return get_doc_module(self.module, self.doctype, self.name)
def validate_mandatory(self, doc):
"""Validate mandatory web form fields"""
missing = []
@ -368,6 +358,11 @@ def get_context(context):
return False
def get_web_form_module(doc):
if doc.is_standard:
return get_doc_module(doc.module, doc.doctype, doc.name)
@frappe.whitelist(allow_guest=True)
@rate_limit(key="web_form", limit=5, seconds=60, methods=["POST"])
def accept(web_form, data, docname=None):

View file

@ -19,6 +19,7 @@ frappe.ui.form.on("Website Theme", {
} else {
frm.enable_save();
}
frm.set_df_property("custom_scss", "max_lines", 45);
},
set_default_theme_button_and_indicator(frm) {

View file

@ -7,10 +7,10 @@
"document_type": "Setup",
"engine": "InnoDB",
"field_order": [
"bootstrap_theme_section",
"theme",
"module",
"custom",
"bootstrap_theme_section",
"google_font",
"font_size",
"font_properties",
@ -73,8 +73,8 @@
{
"collapsible": 1,
"fieldname": "custom_js_section",
"fieldtype": "Section Break",
"label": "Custom JS"
"fieldtype": "Tab Break",
"label": "Script"
},
{
"fieldname": "js",
@ -84,7 +84,7 @@
},
{
"fieldname": "bootstrap_theme_section",
"fieldtype": "Section Break",
"fieldtype": "Tab Break",
"label": "Theme Configuration"
},
{
@ -123,7 +123,7 @@
},
{
"fieldname": "stylesheet_section",
"fieldtype": "Section Break",
"fieldtype": "Tab Break",
"label": "Stylesheet"
},
{
@ -181,10 +181,11 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-01-18 17:43:39.804765",
"modified": "2022-10-25 22:15:53.601571",
"modified_by": "Administrator",
"module": "Website",
"name": "Website Theme",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
@ -206,5 +207,6 @@
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View file

@ -163,6 +163,7 @@ def prepare_filters(doctype, controller, kwargs):
def get_list_context(context, doctype, web_form_name=None):
from frappe.modules import load_doctype_module
from frappe.website.doctype.web_form.web_form import get_web_form_module
list_context = context or frappe._dict()
meta = frappe.get_meta(doctype)
@ -193,7 +194,7 @@ def get_list_context(context, doctype, web_form_name=None):
# get context from web form module
if web_form_name:
web_form = frappe.get_doc("Web Form", web_form_name)
list_context = update_context_from_module(web_form.get_web_form_module(), list_context)
list_context = update_context_from_module(get_web_form_module(web_form), list_context)
# get path from '/templates/' folder of the doctype
if not meta.custom and not list_context.row_template:

View file

@ -13,7 +13,7 @@ dependencies = [
"Click~=7.1.2",
"GitPython~=3.1.14",
"Jinja2~=3.1.2",
"Pillow~=9.1.1",
"Pillow~=9.2.0",
"PyJWT~=2.4.0",
"PyMySQL~=1.0.2",
"PyPDF2~=2.1.0",