Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions modeltrans/fields.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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(<lookup>, '')` 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())


Expand Down
32 changes: 32 additions & 0 deletions tests/test_querysets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
)