diff --git a/docs/modeltranslation/usage.rst b/docs/modeltranslation/usage.rst index cb3df69..e65c70e 100644 --- a/docs/modeltranslation/usage.rst +++ b/docs/modeltranslation/usage.rst @@ -123,7 +123,7 @@ These manager methods perform rewriting: - ``order_by()`` - ``update()`` - ``only()``, ``defer()`` -- ``values()``, ``values_list()`` +- ``values()``, ``values_list()``, with :ref:`fallback ` mechanism - ``dates()`` - ``select_related()`` - ``create()``, with optional auto-population_ feature @@ -214,18 +214,25 @@ Falling back ------------ Modeltranslation provides a mechanism to control behaviour of data access in case of empty -translation values. This mechanism affects field access. +translation values. This mechanism affects field access, as well as ``values()`` +and ``values_list()`` manager methods. Consider the ``News`` example: a creator of some news hasn't specified its German title and content, but only English ones. Then if a German visitor is viewing the site, we would rather show him English title/content of the news than display empty strings. This is called *fallback*. :: - News.title_en = 'English title' - News.title_de = '' - print News.title + news.title_en = 'English title' + news.title_de = '' + print news.title # If current active language is German, it should display the title_de field value (''). # But if fallback is enabled, it would display 'English title' instead. + # Similarly for manager + news.save() + print News.objects.filter(pk=news.pk).values_list('title', flat=True)[0] + # As above: if current active language is German and fallback to English is enabled, + # it would display 'English title'. + There are several ways of controlling fallback, described below. .. _fallback_lang: diff --git a/modeltranslation/manager.py b/modeltranslation/manager.py index 03392c0..7f9e216 100644 --- a/modeltranslation/manager.py +++ b/modeltranslation/manager.py @@ -23,7 +23,7 @@ except ImportError: from modeltranslation import settings from modeltranslation.fields import TranslationField from modeltranslation.utils import (build_localized_fieldname, get_language, - auto_populate) + auto_populate, resolution_order) def get_translatable_fields_for_model(model): @@ -56,6 +56,24 @@ def rewrite_lookup_key(model, lookup_key): return '__'.join(pieces) +def append_fallback(model, fields): + """ + If translated field is encountered, add also all its fallback fields. + Returns tuple: (set_of_new_fields_to_use, set_of_translated_field_names) + """ + fields = set(fields) + trans = set() + from modeltranslation.translator import translator + opts = translator.get_options_for_model(model) + for key, _ in opts.fields.items(): + if key in fields: + langs = resolution_order(get_language(), getattr(model, key).fallback_languages) + fields = fields.union(build_localized_fieldname(key, lang) for lang in langs) + fields.remove(key) + trans.add(key) + return fields, trans + + def append_translated(model, fields): "If translated field is encountered, add also all its translation fields." fields = set(fields) @@ -343,24 +361,22 @@ class MultilingualQuerySet(models.query.QuerySet): if not fields: # Emulate original queryset behaviour: get all fields that are not translation fields fields = self._get_original_fields() - new_args = [] - for key in fields: - new_args.append(rewrite_lookup_key(self.model, key)) - vqs = super(MultilingualQuerySet, self).values(*new_args) - vqs.field_names = list(fields) - return vqs + return self._clone(klass=FallbackValuesQuerySet, setup=True, _fields=fields) # This method was not present in django-linguo def values_list(self, *fields, **kwargs): if not self._rewrite: return super(MultilingualQuerySet, self).values_list(*fields, **kwargs) + flat = kwargs.pop('flat', False) + if kwargs: + raise TypeError('Unexpected keyword arguments to values_list: %s' % (list(kwargs),)) + if flat and len(fields) > 1: + raise TypeError("'flat' is not valid when values_list is " + "called with more than one field.") if not fields: # Emulate original queryset behaviour: get all fields that are not translation fields fields = self._get_original_fields() - new_args = [] - for key in fields: - new_args.append(rewrite_lookup_key(self.model, key)) - return super(MultilingualQuerySet, self).values_list(*new_args, **kwargs) + return self._clone(klass=FallbackValuesListQuerySet, setup=True, flat=flat, _fields=fields) # This method was not present in django-linguo def dates(self, field_name, *args, **kwargs): @@ -370,6 +386,58 @@ class MultilingualQuerySet(models.query.QuerySet): return super(MultilingualQuerySet, self).dates(new_key, *args, **kwargs) +class FallbackValuesQuerySet(models.query.ValuesQuerySet, MultilingualQuerySet): + def _setup_query(self): + original = self._fields + new_fields, self.translation_fields = append_fallback(self.model, original) + self._fields = list(new_fields) + self.fields_to_del = new_fields - set(original) + super(FallbackValuesQuerySet, self)._setup_query() + + class X(object): + # This stupid class is needed as object use __slots__ and has no __dict__. + pass + + def iterator(self): + instance = self.X() + for row in super(FallbackValuesQuerySet, self).iterator(): + instance.__dict__.update(row) + for key in self.translation_fields: + row[key] = getattr(self.model, key).__get__(instance, None) + for key in self.fields_to_del: + del row[key] + yield row + + def _clone(self, klass=None, setup=False, **kwargs): + c = super(FallbackValuesQuerySet, self)._clone(klass, **kwargs) + c.fields_to_del = self.fields_to_del + c.translation_fields = self.translation_fields + if setup and hasattr(c, '_setup_query'): + c._setup_query() + return c + + +class FallbackValuesListQuerySet(FallbackValuesQuerySet): + def iterator(self): + for row in super(FallbackValuesListQuerySet, self).iterator(): + if self.flat and len(self.original_fields) == 1: + yield row[self.original_fields[0]] + else: + yield tuple(row[f] for f in self.original_fields) + + def _setup_query(self): + self.original_fields = self._fields + super(FallbackValuesListQuerySet, self)._setup_query() + + def _clone(self, *args, **kwargs): + clone = super(FallbackValuesListQuerySet, self)._clone(*args, **kwargs) + clone.original_fields = self.original_fields + if not hasattr(clone, "flat"): + # Only assign flat if the clone didn't already get it from kwargs + clone.flat = self.flat + return clone + + def get_queryset(obj): if hasattr(obj, 'get_queryset'): return obj.get_queryset() diff --git a/modeltranslation/tests/tests.py b/modeltranslation/tests/tests.py index 1426f2a..aaa7c99 100644 --- a/modeltranslation/tests/tests.py +++ b/modeltranslation/tests/tests.py @@ -2449,6 +2449,38 @@ class TestManager(ModeltranslationTestBase): self.assertEqual(titles_for_en, ('most', 'more_en', 'more_de', 'least')) self.assertEqual(titles_for_de, ('most', 'more_de', 'more_en', 'least')) + def assert_fallback(self, method, expected1, *args, **kwargs): + transform = kwargs.pop('transform', lambda x: x) + expected2 = kwargs.pop('expected_de', expected1) + with default_fallback(): + # Fallback is ('de',) + obj = method(*args, **kwargs)[0] + with override('de'): + obj2 = method(*args, **kwargs)[0] + self.assertEqual(transform(obj), expected1) + self.assertEqual(transform(obj2), expected2) + + def test_values_fallback(self): + manager = models.ManagerTestModel.objects + manager.create(title_en='', title_de='de') + self.assertEqual('en', get_language()) + + self.assert_fallback(manager.values, 'de', 'title', transform=lambda x: x['title']) + self.assert_fallback(manager.values_list, 'de', 'title', flat=True) + self.assert_fallback(manager.values_list, ('de', '', 'de'), 'title', 'title_en', 'title_de') + + # Settings are taken into account - fallback can be disabled + with override_settings(MODELTRANSLATION_ENABLE_FALLBACKS=False): + self.assert_fallback(manager.values, '', 'title', expected_de='de', + transform=lambda x: x['title']) + + # Test fallback values + manager = models.FallbackModel.objects + manager.create() + + self.assert_fallback(manager.values, 'fallback', 'title', transform=lambda x: x['title']) + self.assert_fallback(manager.values_list, ('fallback', 'fallback'), 'title', 'text') + def test_values(self): manager = models.ManagerTestModel.objects id1 = manager.create(title_en='en', title_de='de').pk