diff --git a/modeltrans/fields.py b/modeltrans/fields.py index 04da6f3..6de8a28 100644 --- a/modeltrans/fields.py +++ b/modeltrans/fields.py @@ -1,7 +1,7 @@ from django.core.exceptions import ImproperlyConfigured -from django.db.models import F, JSONField, fields +from django.db.models import F, JSONField, Value, fields from django.db.models.fields.json import KeyTextTransform -from django.db.models.functions import Cast, Coalesce +from django.db.models.functions import Cast, Coalesce, NullIf from django.utils.translation import gettext from .conf import get_default_language, get_fallback_chain, get_modeltrans_setting @@ -247,6 +247,14 @@ def as_expression(self, bare_lookup, fallback=True): # and now, add the list of fallback languages to the lookup list for fallback_language in fallback_chain: lookups.append(self._localized_lookup(fallback_language, bare_lookup)) + + # An empty translation in `i18n` must be skipped, just like `__get__()` does, so + # `NULLIF(, '')` is used to make `COALESCE` continue to the next language + # in the chain. The lookup for the default language is a plain reference to the + # original field (a string), which is the last resort and is left as it is. + lookups = [ + lookup if isinstance(lookup, str) else NullIf(lookup, Value("")) for lookup in lookups + ] return Coalesce(*lookups, output_field=self.output_field()) diff --git a/tests/test_querysets.py b/tests/test_querysets.py index 05f1fac..acc22b8 100644 --- a/tests/test_querysets.py +++ b/tests/test_querysets.py @@ -771,3 +771,35 @@ def test_values_kwarg_lower(self): def test_values_spanning_relation(self): qs = Blog.objects.all().order_by("title_nl").values_list("title_nl", "category__name_nl") self.assertEqual(list(qs), [(None, None), ("Kikker", "Amfibiƫn"), ("Valk", "Vogels")]) + + +class FallbackEmptyStringTest(TestCase): + """ + An empty translation in `i18n` must be skipped while building the fallback + expression, just like `TranslatedVirtualField.__get__()` skips it. + """ + + def setUp(self): + Blog.objects.bulk_create( + [Blog(title="Falcon", title_nl=""), Blog(title="Frog", title_nl="Kikker")] + ) + + def test_values_i18n(self): + with override("nl"): + self.assertEqual( + set(Blog.objects.values_list("title_i18n", flat=True)), {"Falcon", "Kikker"} + ) + + def test_filter_i18n(self): + with override("nl"): + self.assertEqual( + {m.title for m in Blog.objects.filter(title_i18n="Falcon")}, {"Falcon"} + ) + + def test_values_i18n_custom_fallback(self): + Challenge.objects.create(default_language="nl", title="Hurray", i18n={"title_nl": ""}) + + with override("de"): + self.assertEqual( + set(Challenge.objects.values_list("title_i18n", flat=True)), {"Hurray"} + )