Merge branch 'develop' into fix-calendar-end-date-issue

This commit is contained in:
mergify[bot] 2026-04-08 08:15:21 +00:00 committed by GitHub
commit ba254318a0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 356 additions and 330 deletions

View file

@ -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:

View file

@ -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: &quot;Amazon Ember&quot;, 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 its 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: &quot;Amazon Ember&quot;, 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 its 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

View file

@ -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)

View file

@ -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:

View file

@ -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)

View file

@ -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>

View file

@ -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

View file

@ -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(

View file

@ -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 }}" />

View file

@ -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

View file

@ -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);
}
}

View file

@ -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,

View file

@ -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(

View file

@ -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,

View file

@ -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()

View file

@ -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)