Skip to content

Commit

Permalink
Add fallback to values and values_list (close #258).
Browse files Browse the repository at this point in the history
  • Loading branch information
zlorf committed Jul 29, 2014
1 parent 42c949d commit 19c2a90
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 16 deletions.
17 changes: 12 additions & 5 deletions docs/modeltranslation/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
90 changes: 79 additions & 11 deletions modeltranslation/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
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):
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -343,24 +361,22 @@ def values(self, *fields):
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):
Expand All @@ -370,6 +386,58 @@ def dates(self, field_name, *args, **kwargs):
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()
Expand Down
32 changes: 32 additions & 0 deletions modeltranslation/tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -2449,6 +2449,38 @@ def test_order_by_meta(self):
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
Expand Down

0 comments on commit 19c2a90

Please sign in to comment.