Merge branch 'develop' into fix-calendar-end-date-issue
This commit is contained in:
commit
ba254318a0
17 changed files with 356 additions and 330 deletions
|
|
@ -159,11 +159,14 @@ def main(
|
|||
discover_all_tests(apps, runner)
|
||||
|
||||
results = []
|
||||
global unittest_runner
|
||||
for app, category, suite in runner.iterRun():
|
||||
click.secho(
|
||||
f"\nRunning {suite.countTestCases()} {category} tests for {app}", fg="cyan", bold=True
|
||||
)
|
||||
results.append([app, category, runner.run(suite)])
|
||||
main_runner = unittest_runner if junit_xml_output and unittest_runner else runner
|
||||
res = main_runner.run(suite)
|
||||
results.append([app, category, res])
|
||||
|
||||
success = all(r.wasSuccessful() for _, _, r in results)
|
||||
if not success:
|
||||
|
|
|
|||
|
|
@ -16,16 +16,20 @@ class TestTranslation(IntegrationTestCase):
|
|||
clear_cache()
|
||||
|
||||
def test_doctype(self):
|
||||
translation_data = get_translation_data()
|
||||
for lang, (source_string, new_translation) in translation_data.items():
|
||||
doctype = "Translation"
|
||||
meta = frappe.get_meta(doctype)
|
||||
source_string = meta.get_label("translated_text")
|
||||
|
||||
for lang in ["de", "bs", "zh", "hr", "en", "sv"]:
|
||||
frappe.local.lang = lang
|
||||
original_translation = _(source_string)
|
||||
original_translation = _(source_string, context=doctype)
|
||||
new_translation = f"{original_translation} Customized"
|
||||
|
||||
docname = create_translation(lang, source_string, new_translation)
|
||||
self.assertEqual(_(source_string), new_translation)
|
||||
docname = create_translation(lang, source_string, new_translation, context=doctype)
|
||||
self.assertEqual(_(source_string, context=doctype), new_translation)
|
||||
|
||||
frappe.delete_doc("Translation", docname)
|
||||
self.assertEqual(_(source_string), original_translation)
|
||||
frappe.delete_doc(doctype, docname)
|
||||
self.assertEqual(_(source_string, context=doctype), original_translation)
|
||||
|
||||
def test_parent_language(self):
|
||||
data = {
|
||||
|
|
@ -60,37 +64,54 @@ class TestTranslation(IntegrationTestCase):
|
|||
source = "User"
|
||||
self.assertNotEqual(_(source, lang="de"), _(source, lang="es"))
|
||||
|
||||
def test_html_content_data_translation(self):
|
||||
# ruff: noqa: RUF001
|
||||
def test_html_content_translation(self):
|
||||
source = """
|
||||
<span style="color: rgb(51, 51, 51); font-family: "Amazon Ember", Arial, sans-serif; font-size:
|
||||
small;">MacBook Air lasts up to an incredible 12 hours between charges. So from your morning coffee to
|
||||
your evening commute, you can work unplugged. When it’s time to kick back and relax,
|
||||
you can get up to 12 hours of iTunes movie playback. And with up to 30 days of standby time,
|
||||
you can go away for weeks and pick up where you left off.Whatever the task,
|
||||
fifth-generation Intel Core i5 and i7 processors with Intel HD Graphics 6000 are up to it.</span><br>
|
||||
"""
|
||||
|
||||
To add dynamic subject, use jinja tags like
|
||||
<div><pre><code>{{ doc.name }} Billed</code></pre></div>
|
||||
""".strip()
|
||||
target = """
|
||||
MacBook Air dura hasta 12 horas increíbles entre cargas. Por lo tanto,
|
||||
desde el café de la mañana hasta el viaje nocturno, puede trabajar desconectado.
|
||||
Cuando es hora de descansar y relajarse, puede obtener hasta 12 horas de reproducción de películas de iTunes.
|
||||
Y con hasta 30 días de tiempo de espera, puede irse por semanas y continuar donde lo dejó. Sea cual sea la tarea,
|
||||
los procesadores Intel Core i5 e i7 de quinta generación con Intel HD Graphics 6000 son capaces de hacerlo.
|
||||
"""
|
||||
Um einen dynamischen Betreff hinzuzufügen, verwenden Sie Jinja-Tags wie
|
||||
<div><pre><code>{{ doc.name }} Abgerechnet</code></pre></div>
|
||||
""".strip()
|
||||
|
||||
create_translation("es", source, target)
|
||||
frappe.local.lang = "de"
|
||||
|
||||
source = """
|
||||
<span style="font-family: "Amazon Ember", Arial, sans-serif; font-size:
|
||||
small; color: rgb(51, 51, 51);">MacBook Air lasts up to an incredible 12 hours between charges. So from your morning coffee to
|
||||
your evening commute, you can work unplugged. When it’s time to kick back and relax,
|
||||
you can get up to 12 hours of iTunes movie playback. And with up to 30 days of standby time,
|
||||
you can go away for weeks and pick up where you left off.Whatever the task,
|
||||
fifth-generation Intel Core i5 and i7 processors with Intel HD Graphics 6000 are up to it.</span><br>
|
||||
"""
|
||||
self.assertEqual(_(source), source)
|
||||
|
||||
self.assertTrue(_(source), target)
|
||||
create_translation("de", source, target)
|
||||
|
||||
self.assertEqual(_(source), target)
|
||||
|
||||
def test_translated_html_is_sanitized(self):
|
||||
source = "Translation with HTML"
|
||||
target = """
|
||||
<span style="color:red" onclick="alert('xss')">Hallo</span>
|
||||
<script>alert("xss")</script>
|
||||
<iframe src="https://example.com"></iframe>
|
||||
<div>Ok</div>
|
||||
""".strip()
|
||||
|
||||
docname = create_translation("de", source, target)
|
||||
translated_text = frappe.db.get_value("Translation", docname, "translated_text")
|
||||
|
||||
self.assertIn('<span style="color:red">Hallo</span>', translated_text)
|
||||
self.assertIn("<div>Ok</div>", translated_text)
|
||||
self.assertNotIn("onclick", translated_text)
|
||||
self.assertNotIn("<script", translated_text)
|
||||
self.assertNotIn('alert("xss")', translated_text)
|
||||
self.assertNotIn("<iframe", translated_text)
|
||||
self.assertNotIn("example.com", translated_text)
|
||||
|
||||
frappe.local.lang = "de"
|
||||
self.assertEqual(_(source), translated_text)
|
||||
|
||||
def test_plain_text_translation_with_angle_brackets_is_unchanged(self):
|
||||
source = "Comparison"
|
||||
target = "1 < 2 and 3 > 2"
|
||||
|
||||
docname = create_translation("de", source, target)
|
||||
|
||||
self.assertEqual(frappe.db.get_value("Translation", docname, "translated_text"), target)
|
||||
|
||||
def test_html_message_translations(self):
|
||||
"""Test fallback for messages w/ HTML Tags"""
|
||||
|
|
@ -100,27 +121,12 @@ class TestTranslation(IntegrationTestCase):
|
|||
self.assertEqual(_(message, lang="zh"), translated_message)
|
||||
|
||||
|
||||
def get_translation_data():
|
||||
html_source_data = """<font color="#848484" face="arial, tahoma, verdana, sans-serif">
|
||||
<span style="font-size: 11px; line-height: 16.9px;">Test Data</span></font>"""
|
||||
html_translated_data = """<font color="#848484" face="arial, tahoma, verdana, sans-serif">
|
||||
<span style="font-size: 11px; line-height: 16.9px;"> testituloksia </span></font>"""
|
||||
|
||||
return {
|
||||
"hr": ["Test data", "Testdaten"],
|
||||
"ms": ["Test Data", "ujian Data"],
|
||||
"et": ["Test Data", "testandmed"],
|
||||
"es": ["Test Data", "datos de prueba"],
|
||||
"en": ["Quotation", "Tax Invoice"],
|
||||
"fi": [html_source_data, html_translated_data],
|
||||
}
|
||||
|
||||
|
||||
def create_translation(lang, source_string, new_translation) -> str:
|
||||
def create_translation(lang, source_string, new_translation, context=None) -> str:
|
||||
doc = frappe.new_doc("Translation")
|
||||
doc.language = lang
|
||||
doc.source_text = source_string
|
||||
doc.translated_text = new_translation
|
||||
doc.context = context
|
||||
doc.save()
|
||||
|
||||
return doc.name
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
# Copyright (c) 2015, Frappe Technologies and contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.translate import MERGED_TRANSLATION_KEY, USER_TRANSLATION_KEY, change_translation_version
|
||||
from frappe.utils import is_html, strip_html_tags
|
||||
from frappe.utils import sanitize_html
|
||||
|
||||
|
||||
class Translation(Document):
|
||||
|
|
@ -28,11 +26,7 @@ class Translation(Document):
|
|||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
if is_html(self.source_text):
|
||||
self.remove_html_from_source()
|
||||
|
||||
def remove_html_from_source(self):
|
||||
self.source_text = strip_html_tags(self.source_text).strip()
|
||||
self.translated_text = sanitize_html(self.translated_text)
|
||||
|
||||
def on_update(self):
|
||||
clear_user_translation_cache(self.language)
|
||||
|
|
|
|||
|
|
@ -199,7 +199,6 @@ def get_diff(old, new, for_child=False, compare_cancelled=False):
|
|||
field_meta.options,
|
||||
{"name": ("in", (old_value, new_value))},
|
||||
["name", title_field],
|
||||
ignore_ifnull=True,
|
||||
)
|
||||
for r in result:
|
||||
if r[0] == old_value:
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import frappe
|
|||
from frappe.database.utils import NestedSetHierarchy
|
||||
from frappe.model.db_query import get_timespan_date_range
|
||||
from frappe.query_builder import Field
|
||||
from frappe.query_builder.functions import Coalesce
|
||||
from frappe.utils import cstr
|
||||
|
||||
|
||||
|
|
@ -51,7 +50,7 @@ def func_in(key: Field, value: list | tuple) -> frappe.qb:
|
|||
|
||||
value = ["" if v is None else v for v in value]
|
||||
if "" in value:
|
||||
return Coalesce(key, "").isin(value)
|
||||
return key.isin(value) | key.isnull()
|
||||
return key.isin(value)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -26,9 +26,9 @@
|
|||
<div class="flex" style="gap:16px; align-items: center;">
|
||||
<div class="desktop-notifications">
|
||||
<div class="dropdown dropdown-notifications">
|
||||
<button class="btn-reset nav-link text-muted" data-toggle="dropdown" >
|
||||
<button class="btn-reset nav-link text-muted" data-toggle="dropdown" aria-label="{{ _("Notifications") }}" aria-haspopup="true">
|
||||
<svg
|
||||
class="icon icon-md"
|
||||
class="icon icon-md" aria-hidden="true"
|
||||
>
|
||||
<use href="#icon-bell"></use>
|
||||
</svg>
|
||||
|
|
@ -50,8 +50,8 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="desktop-avatar">
|
||||
</div>
|
||||
<button class="desktop-avatar btn-reset" aria-label="{{ _('User Menu') }}">
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</header>
|
||||
|
|
|
|||
|
|
@ -492,7 +492,7 @@ class EmailAccount(Document):
|
|||
|
||||
@classmethod
|
||||
def create_dummy(cls):
|
||||
return cls.from_record({"sender": "notifications@example.com"})
|
||||
return cls.from_record({"name": "Notifications", "email_id": "notifications@example.com"})
|
||||
|
||||
@classmethod
|
||||
@cache_email_account("outgoing_email_account")
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1766,7 +1766,7 @@ class Document(BaseDocument):
|
|||
_("Table {0} cannot be empty").format(label), raise_exception or frappe.EmptyTableError
|
||||
)
|
||||
|
||||
def round_floats_in(self, doc, fieldnames=None):
|
||||
def round_floats_in(self, doc, fieldnames=None, do_not_round_fields=None):
|
||||
"""Round floats for all `Currency`, `Float`, `Percent` fields for the given doc.
|
||||
|
||||
:param doc: Document whose numeric properties are to be rounded.
|
||||
|
|
@ -1780,6 +1780,9 @@ class Document(BaseDocument):
|
|||
# PERF: flt internally has to resolve this if we don't specify it.
|
||||
rounding_method = frappe.get_system_settings("rounding_method")
|
||||
for fieldname in fieldnames:
|
||||
if do_not_round_fields and fieldname in do_not_round_fields:
|
||||
continue
|
||||
|
||||
doc.set(
|
||||
fieldname,
|
||||
flt(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<a class="desktop-icon" data-id="{{ icon.label}}" data-logo="{{ icon.logo_url }}" data-icon="{{ icon.icon }}" data-type="{{ icon.type }}" style="text-decoration:none">
|
||||
<a href="#" class="desktop-icon" data-id="{{ icon.label}}" data-logo="{{ icon.logo_url }}" data-icon="{{ icon.icon }}" data-type="{{ icon.type }}" style="text-decoration:none">
|
||||
{% if(frappe.utils.get_desktop_icon(icon.label, frappe.boot.desktop_icon_style ) && icon.icon_type != "Folder") %}
|
||||
<div class="icon-container">
|
||||
<img class="app-icon" src="{{ frappe.utils.get_desktop_icon(icon.label, frappe.boot.desktop_icon_style) }}" alt="{{ icon.label }}" />
|
||||
|
|
|
|||
|
|
@ -195,7 +195,7 @@ frappe.ui.Sidebar = class Sidebar {
|
|||
}
|
||||
|
||||
this.remove_onboarding_wrapper();
|
||||
if (module_name) {
|
||||
if (module_name && !frappe.is_mobile()) {
|
||||
if (
|
||||
this?.onboarding_widget[module_name] &&
|
||||
this.onboarding_widget[module_name].hide_panel
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ export default class Widget {
|
|||
constructor(opts) {
|
||||
Object.assign(this, opts);
|
||||
this.make();
|
||||
this.apply_hidden_state();
|
||||
}
|
||||
|
||||
refresh() {
|
||||
|
|
@ -197,4 +198,10 @@ export default class Widget {
|
|||
set_footer() {
|
||||
//
|
||||
}
|
||||
|
||||
apply_hidden_state() {
|
||||
const is_hidden = Boolean(this.hidden);
|
||||
const show_for_customize = is_hidden && this.in_customize_mode;
|
||||
this.widget.toggleClass("hidden", is_hidden && !show_for_customize);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,14 +19,19 @@ a {
|
|||
a,
|
||||
a:hover,
|
||||
a:active,
|
||||
a:focus,
|
||||
.btn,
|
||||
.btn:hover,
|
||||
.btn:active,
|
||||
.btn:focus {
|
||||
.btn:active {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
a:focus-visible,
|
||||
.btn:focus-visible,
|
||||
.btn-reset:focus-visible {
|
||||
box-shadow: var(--focus-default);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
a.grey,
|
||||
.sidebar-section a,
|
||||
.control-value a,
|
||||
|
|
|
|||
|
|
@ -525,15 +525,17 @@ class TestOperatorIn(IntegrationTestCase):
|
|||
query = func_in(note.name, [None, "user1"])
|
||||
sql_str = str(query).lower()
|
||||
|
||||
self.assertIn("coalesce", sql_str)
|
||||
self.assertNotIn("coalesce", sql_str)
|
||||
self.assertIn("is null", sql_str)
|
||||
self.assertIn("''", sql_str)
|
||||
|
||||
def test_func_in_with_empty_string_uses_coalesce(self):
|
||||
def test_func_in_with_empty_string_uses_or_is_null(self):
|
||||
note = frappe.qb.DocType("Note")
|
||||
query = func_in(note.name, ["", "user1"])
|
||||
sql_str = str(query).lower()
|
||||
|
||||
self.assertIn("coalesce", sql_str)
|
||||
self.assertNotIn("coalesce", sql_str)
|
||||
self.assertIn("is null", sql_str)
|
||||
self.assertIn("''", sql_str)
|
||||
|
||||
def test_func_in_with_mixed_none_and_values(self):
|
||||
|
|
@ -541,7 +543,8 @@ class TestOperatorIn(IntegrationTestCase):
|
|||
query = func_in(note.name, ["val1", None, "val2"])
|
||||
sql_str = str(query).lower()
|
||||
|
||||
self.assertIn("coalesce", sql_str)
|
||||
self.assertNotIn("coalesce", sql_str)
|
||||
self.assertIn("is null", sql_str)
|
||||
|
||||
def test_in_filter_matches_null_and_empty_columns(self):
|
||||
test_doctype = new_doctype(
|
||||
|
|
|
|||
|
|
@ -174,7 +174,7 @@ def sanitize_html(html, linkify=False, always_sanitize=False, disallowed_tags=No
|
|||
|
||||
attributes = {"*": acceptable_attributes, "svg": svg_attributes}
|
||||
|
||||
# returns html with escaped tags, escaped orphan >, <, etc.
|
||||
# returns sanitized HTML with unsafe tags and attributes removed
|
||||
escaped_html = nh3.clean(
|
||||
html,
|
||||
tags=tags,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ def _(msg: str, lang: str | None = None, context: str | None = None) -> str:
|
|||
_('Change', context='Coins')
|
||||
"""
|
||||
from frappe.translate import get_all_translations
|
||||
from frappe.utils import is_html, strip_html_tags
|
||||
|
||||
if not hasattr(frappe.local, "lang"):
|
||||
frappe.local.lang = lang or "en"
|
||||
|
|
@ -20,9 +19,6 @@ def _(msg: str, lang: str | None = None, context: str | None = None) -> str:
|
|||
|
||||
non_translated_string = msg
|
||||
|
||||
if is_html(msg):
|
||||
msg = strip_html_tags(msg)
|
||||
|
||||
# msg should always be unicode
|
||||
msg = frappe.as_unicode(msg).strip()
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ from frappe.model.document import Document
|
|||
from frappe.utils.data import quoted
|
||||
from frappe.www.list import get_list_context, get_list_data
|
||||
|
||||
no_cache = 1
|
||||
|
||||
|
||||
def get_context(context, **dict_params):
|
||||
frappe.local.form_dict.update(dict_params)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue