Add fallback to values and values_list (close #258).

This commit is contained in:
Jacek Tomaszewski 2014-07-29 17:34:53 +03:00
parent 42c949ddb5
commit 19c2a90e9f
3 changed files with 123 additions and 16 deletions

View file

@ -123,7 +123,7 @@ These manager methods perform rewriting:
- ``order_by()``
- ``update()``
- ``only()``, ``defer()``
- ``values()``, ``values_list()``
- ``values()``, ``values_list()``, with :ref:`fallback <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:

View file

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

View file

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