Skip to content

Commit

Permalink
Added per-language configureable fallback setting
Browse files Browse the repository at this point in the history
  • Loading branch information
jieter committed Sep 3, 2017
1 parent 24635c3 commit fd9b02a
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 13 deletions.
44 changes: 35 additions & 9 deletions modeltrans/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,25 @@ def __get__(self, instance, instance_type=None):

field_name = build_localized_fieldname(self.original_name, language)

# fallback if key does not exist, or contains the empty string (only for <original_field>_i18n fields)
if self.language is None and (field_name not in instance.i18n or not instance.i18n[field_name]):
return getattr(instance, self.original_name)
def has_field(field_name):
return field_name in instance.i18n and instance.i18n[field_name]

# in two cases, just return the value:
# - if this is an explicit field (<name>_<lang>)
# - if this is a implicit field (<name>_i18n) AND the value exists and is not Falsy
if self.language is not None or has_field(field_name):
return instance.i18n.get(field_name)

# this is the _i18n version of the field, and the current language is not available,
# so we walk the fallback chain:
for fallback_language in get_fallback_chain(language):
field_name = build_localized_fieldname(self.original_name, fallback_language)

return instance.i18n.get(field_name)
if has_field(field_name):
return instance.i18n.get(field_name)

# finally, return the original field if all else fails.
return getattr(instance, self.original_name)

def __set__(self, instance, value):
if instance.i18n is None:
Expand Down Expand Up @@ -156,6 +170,13 @@ def output_field(self):

return Field()

def _localized_lookup(self, language):
if language == DEFAULT_LANGUAGE:
return self.original_name

name = build_localized_fieldname(self.original_name, language)
return RawSQL('{}.i18n->>%s'.format(self.model._meta.db_table), (name, ))

def sql_lookup(self, fallback=True):
'''
Compose the sql lookup to get the value for this virtual field in a query.
Expand All @@ -165,13 +186,18 @@ def sql_lookup(self, fallback=True):
if language == DEFAULT_LANGUAGE:
return self.original_name

name = build_localized_fieldname(self.original_name, language)

i18n_lookup = RawSQL('{}.i18n->>%s'.format(self.model._meta.db_table), (name, ))

if fallback:
return Coalesce(i18n_lookup, self.original_name, output_field=self.output_field())
fallback_chain = get_fallback_chain(language)
# first, add the current language to the list of lookups
lookups = [self._localized_lookup(language)]
# 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)
)
return Coalesce(*lookups, output_field=self.output_field())
else:
i18n_lookup = self._localized_lookup(language)
return Cast(i18n_lookup, self.output_field())


Expand Down
5 changes: 3 additions & 2 deletions modeltrans/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ class MultilingualQuerySet(models.query.QuerySet):
When adding the `modeltrans.fields.TranslationField` to a model, MultilingualManager is automatically
mixed in to the manager class of that model.
'''

def add_i18n_annotation(self, field, annotation_name=None, fallback=True):
'''
Private method to add an annotation to the query to extract the translated
Expand Down Expand Up @@ -203,8 +204,8 @@ def _filter_or_exclude(self, negate, *args, **kwargs):
Examples:
- `title_nl__contains='foo'` will add an annotation for `title_nl`
- `title_nl='bar'` will add an annotation for `title_nl`
- `title_i18n='foo'` will add an annotation for `title_<language>`
where `<language>` is the current active language.
- `title_i18n='foo'` will add an annotation for a coalesce of the
current active language, and all items of the fallback chain.
- `Q(title_nl__contains='foo') will add an annotation for `title_nl`
In all cases, the field part of the field lookup will be changed to use
Expand Down
42 changes: 40 additions & 2 deletions tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from django.core.exceptions import ValidationError
from django.db import DataError, transaction
from django.test import TestCase
from django.test import TestCase, override_settings
from django.utils.translation import override

from tests.app.models import Blog, NullableTextModel, TextModel
Expand Down Expand Up @@ -202,14 +202,52 @@ def test_textfield(self):
)
self.assertEquals(m.description_nl, long_str)

@override_settings(
MODELTRANS_AVAILABLE_LANGUAGES=('fr', 'fy', 'nl'),
MODELTRANS_FALLBACK={
'default': ('en', ),
'fy': ('nl', 'en')
}
)
def test_fallback_chain(self):
'''
Testing the fallback chain setting for model
'''
b = Blog.objects.create(title='Buzzard', i18n={
'title_fy': 'Mûzefalk',
'title_nl': 'Buizerd',
'title_fr': 'Buse'
})

with override('nl'):
self.assertEquals(b.title_i18n, 'Buizerd')
with override('fr'):
self.assertEquals(b.title_i18n, 'Buse')
with override('fy'):
self.assertEquals(b.title_i18n, 'Mûzefalk')

b = Blog.objects.create(title='Buzzard', i18n={
'title_nl': 'Buizerd',
'title_fr': 'Buse'
})
with override('fy'):
self.assertEquals(b.title_i18n, 'Buizerd')

b = Blog.objects.create(title='Buzzard', i18n={
'title_fr': 'Buse'
})
with override('fy'):
self.assertEquals(b.title_i18n, 'Buzzard')
with override('fr'):
self.assertEquals(b.title_i18n, 'Buse')


class RefreshFromDbTest(TestCase):
def test_refresh_from_db(self):
b = Blog.objects.create(title='Falcon', i18n={
'title_nl': 'Valk',
'title_de': 'Falk'
})

Blog.objects.filter(title='Falcon').update(title='Falcon II')

b.refresh_from_db()
Expand Down

0 comments on commit fd9b02a

Please sign in to comment.