Merge branch 'populate-modes-cleaned'

This commit is contained in:
Jacek Tomaszewski 2013-02-19 18:15:47 +01:00
commit 9e41538035
9 changed files with 254 additions and 43 deletions

View file

@ -249,12 +249,17 @@ Default: ``False``
.. versionadded:: 0.5
This setting controls if the :ref:`multilingual_manager` should automatically
populate language field values in its ``create`` method, so that these two
statements can be considered equivalent::
populate language field values in its ``create`` and ``get_or_create`` method, and in model
constructors, so that these two blocks of statements can be considered equivalent::
News.objects.create(title='-- no translation yet --', _populate=True)
News.objects.populate(True).create(title='-- no translation yet --')
with auto_populate(True):
q = News(title='-- no translation yet --')
# same effect with MODELTRANSLATION_AUTO_POPULATE == True:
News.objects.create(title='-- no translation yet --')
q = News(title='-- no translation yet --')
``MODELTRANSLATION_DEBUG``

View file

@ -131,11 +131,13 @@ It can be changed several times inside a query. So ``X.objects.rewrite(False)``
Auto-population
***************
In ``create()`` you can set special parameter ``_populate=True`` to populate all translation
(language) fields with values from translated (original) ones. It can be very convenient when working
with many languages. So::
.. versionchanged:: 0.6
x = News.objects.create(title='bar', _populate=True)
There is special manager method ``populate(mode)`` which can trigger ``create()`` or
``get_or_create()`` to populate all translation (language) fields with values from translated
(original) ones. It can be very convenient when working with many languages. So::
x = News.objects.populate(True).create(title='bar')
is equivalent of::
@ -144,14 +146,50 @@ is equivalent of::
Moreover, some fields can be explicitly assigned different values::
x = News.objects.create(title='-- no translation yet --', title_de='enigma', _populate=True)
x = News.objects.populate(True).create(title='-- no translation yet --', title_de='enigma')
It will result in ``title_de == 'nic'`` and other ``title_?? == '-- no translation yet --'``.
It will result in ``title_de == 'enigma'`` and other ``title_?? == '-- no translation yet --'``.
There is a more convenient way than passing _populate all the time:
There is another way of altering the current population status, an ``auto_populate`` context manager::
from modeltranslation.utils import auto_populate
with auto_populate(True):
x = News.objects.create(title='bar')
Auto-population tooks place also in model constructor, what is extremely useful when loading
non-translated fixtures. Just remember to use the context manager::
with auto_populate(): # True can be ommited
call_command('loaddata', 'fixture.json') # Some fixture loading
z = News(title='bar')
print z.title_en, z.title_de # prints 'bar bar'
There is a more convenient way than calling ``populate`` manager method or entering
``auto_populate`` manager context all the time:
:ref:`settings-modeltranslation_auto_populate` setting.
If ``_populate`` parameter is missing, ``create()`` will look at the setting to determine if
population should be used.
It controls the default population behaviour.
There are 4 different population modes:
``False``
[set by default]
Auto-population turned off
``True`` or ``'all'``
[default argument to population altering methods]
Auto-population turned on, copying translated field value to all other languages
(unless a translation field value is provided)
``'default'``
Auto-population turned on, copying translated field value to default language field
(unless its value is provided)
``'required'``
Acts like ``'default'``, but copy value only if the original field is non-nullable
.. _fallback:

View file

@ -5,13 +5,15 @@ django-linguo by Zach Mathew
https://github.com/zmathew/django-linguo
"""
from __future__ import with_statement # Python 2.5 compatibility
from django.db import models
from django.db.models.fields.related import RelatedField
from django.db.models.sql.where import Constraint
from django.utils.tree import Node
from modeltranslation import settings
from modeltranslation.utils import build_localized_fieldname, get_language
from modeltranslation.utils import (build_localized_fieldname, get_language,
auto_populate)
def get_translatable_fields_for_model(model):
@ -69,13 +71,13 @@ def get_fields_to_translatable_models(model):
class MultilingualQuerySet(models.query.QuerySet):
_rewrite = True
def __init__(self, *args, **kwargs):
super(MultilingualQuerySet, self).__init__(*args, **kwargs)
self._post_init()
def _post_init(self):
self._rewrite = True
self._populate = None
if self.model and (not self.query.order_by):
if self.model._meta.ordering:
# If we have default ordering specified on the model, set it now so that
@ -88,12 +90,20 @@ class MultilingualQuerySet(models.query.QuerySet):
# This method was not present in django-linguo
def _clone(self, *args, **kwargs):
kwargs.setdefault('_rewrite', self._rewrite)
kwargs.setdefault('_populate', self._populate)
return super(MultilingualQuerySet, self)._clone(*args, **kwargs)
# This method was not present in django-linguo
def rewrite(self, mode=True):
return self._clone(_rewrite=mode)
# This method was not present in django-linguo
def populate(self, mode='all'):
"""
Overrides the translation fields population mode for this query set.
"""
return self._clone(_populate=mode)
def _rewrite_applied_operations(self):
"""
Rewrite fields in already applied filters/ordering.
@ -173,20 +183,29 @@ class MultilingualQuerySet(models.query.QuerySet):
return super(MultilingualQuerySet, self).update(**kwargs)
update.alters_data = True
# This method was not present in django-linguo
@property
def _populate_mode(self):
# Populate can be set using a global setting or a manager method.
if self._populate is None:
return settings.AUTO_POPULATE
return self._populate
# This method was not present in django-linguo
def create(self, **kwargs):
populate = kwargs.pop('_populate', settings.AUTO_POPULATE)
if populate:
translatable_fields = get_translatable_fields_for_model(self.model)
if translatable_fields is not None:
for key, val in kwargs.items():
if key in translatable_fields:
# Try to add value in every language
for translation_field in translatable_fields[key]:
kwargs.setdefault(translation_field.name, val)
# If not use populate feature, then normal rewriting will occur at model's __init__
# That's why it is not performed here - no reason to rewrite twice.
return super(MultilingualQuerySet, self).create(**kwargs)
"""
Allows to override population mode with a ``populate`` method.
"""
with auto_populate(self._populate_mode):
return super(MultilingualQuerySet, self).create(**kwargs)
# This method was not present in django-linguo
def get_or_create(self, **kwargs):
"""
Allows to override population mode with a ``populate`` method.
"""
with auto_populate(self._populate_mode):
return super(MultilingualQuerySet, self).get_or_create(**kwargs)
class MultilingualManager(models.Manager):
@ -195,6 +214,9 @@ class MultilingualManager(models.Manager):
def rewrite(self, *args, **kwargs):
return self.get_query_set().rewrite(*args, **kwargs)
def populate(self, *args, **kwargs):
return self.get_query_set().populate(*args, **kwargs)
def get_query_set(self):
qs = super(MultilingualManager, self).get_query_set()
if qs.__class__ == models.query.QuerySet:

View file

@ -31,7 +31,8 @@ from modeltranslation.tests.translation import (FallbackModel2TranslationOptions
FieldInheritanceCTranslationOptions,
FieldInheritanceETranslationOptions)
from modeltranslation.tests.test_settings import TEST_SETTINGS
from modeltranslation.utils import build_css_class, build_localized_fieldname
from modeltranslation.utils import (build_css_class, build_localized_fieldname,
auto_populate)
try:
from django.test.utils import override_settings
@ -1705,6 +1706,11 @@ class TestManager(ModeltranslationTestBase):
# In this test case the default language is en, not de.
trans_real.activate('en')
def tearDown(self):
# Settings may be loaded by translator, resulting in a different fallback.
trans_real.activate('de')
reload(mt_settings)
def test_filter_update(self):
"""Test if filtering and updating is language-aware."""
n = models.ManagerTestModel(title='')
@ -1864,20 +1870,20 @@ class TestManager(ModeltranslationTestBase):
def test_creation_population(self):
"""Test if language fields are populated with default value on creation."""
n = models.ManagerTestModel.objects.create(title='foo', _populate=True)
n = models.ManagerTestModel.objects.populate(True).create(title='foo')
self.assertEqual('foo', n.title_en)
self.assertEqual('foo', n.title_de)
self.assertEqual('foo', n.title)
# You can specify some language...
n = models.ManagerTestModel.objects.create(title='foo', title_de='bar', _populate=True)
n = models.ManagerTestModel.objects.populate(True).create(title='foo', title_de='bar')
self.assertEqual('foo', n.title_en)
self.assertEqual('bar', n.title_de)
self.assertEqual('foo', n.title)
# ... but remember that still original attribute points to current language
self.assertEqual('en', get_language())
n = models.ManagerTestModel.objects.create(title='foo', title_en='bar', _populate=True)
n = models.ManagerTestModel.objects.populate(True).create(title='foo', title_en='bar')
self.assertEqual('bar', n.title_en)
self.assertEqual('foo', n.title_de)
self.assertEqual('bar', n.title) # points to en
@ -1885,27 +1891,78 @@ class TestManager(ModeltranslationTestBase):
self.assertEqual('foo', n.title) # points to de
self.assertEqual('en', get_language())
# This feature (for backward-compatibility) require _populate keyword...
# This feature (for backward-compatibility) require populate method...
n = models.ManagerTestModel.objects.create(title='foo')
self.assertEqual('foo', n.title_en)
self.assertEqual(None, n.title_de)
self.assertEqual('foo', n.title)
# ... or MODELTRANSLATION_AUTO_POPULATE setting
with override_settings(MODELTRANSLATION_AUTO_POPULATE=True):
reload(mt_settings)
with reload_override_settings(MODELTRANSLATION_AUTO_POPULATE=True):
self.assertEqual(True, mt_settings.AUTO_POPULATE)
n = models.ManagerTestModel.objects.create(title='foo')
self.assertEqual('foo', n.title_en)
self.assertEqual('foo', n.title_de)
self.assertEqual('foo', n.title)
# _populate keyword has highest priority
n = models.ManagerTestModel.objects.create(title='foo', _populate=False)
# populate method has highest priority
n = models.ManagerTestModel.objects.populate(False).create(title='foo')
self.assertEqual('foo', n.title_en)
self.assertEqual(None, n.title_de)
self.assertEqual('foo', n.title)
# Restore previous state
reload(mt_settings)
self.assertEqual(False, mt_settings.AUTO_POPULATE)
# Populate ``default`` fills just the default translation.
# TODO: Having more languages would make these tests more meaningful.
qs = models.ManagerTestModel.objects
m = qs.populate('default').create(title='foo', description='bar')
self.assertEqual('foo', m.title_de)
self.assertEqual('foo', m.title_en)
self.assertEqual('bar', m.description_de)
self.assertEqual('bar', m.description_en)
with override('de'):
m = qs.populate('default').create(title='foo', description='bar')
self.assertEqual('foo', m.title_de)
self.assertEqual(None, m.title_en)
self.assertEqual('bar', m.description_de)
self.assertEqual(None, m.description_en)
# Populate ``required`` fills just non-nullable default translations.
qs = models.ManagerTestModel.objects
m = qs.populate('required').create(title='foo', description='bar')
self.assertEqual('foo', m.title_de)
self.assertEqual('foo', m.title_en)
self.assertEqual(None, m.description_de)
self.assertEqual('bar', m.description_en)
with override('de'):
m = qs.populate('required').create(title='foo', description='bar')
self.assertEqual('foo', m.title_de)
self.assertEqual(None, m.title_en)
self.assertEqual('bar', m.description_de)
self.assertEqual(None, m.description_en)
def test_get_or_create_population(self):
"""
Populate may be used with ``get_or_create``.
"""
qs = models.ManagerTestModel.objects
m1, created1 = qs.populate(True).get_or_create(title='aaa')
m2, created2 = qs.populate(True).get_or_create(title='aaa')
self.assertTrue(created1)
self.assertFalse(created2)
self.assertEqual(m1, m2)
self.assertEqual('aaa', m1.title_en)
self.assertEqual('aaa', m1.title_de)
def test_fixture_population(self):
"""
Test that a fixture with values only for the original fields
does not result in missing default translations for (original)
non-nullable fields.
"""
with auto_populate('required'):
call_command('loaddata', 'fixture.json', verbosity=0, commit=False)
m = models.TestModel.objects.get()
self.assertEqual(m.title_en, 'foo')
self.assertEqual(m.title_de, 'foo')
self.assertEqual(m.text_en, 'bar')
self.assertEqual(m.text_de, None)

View file

@ -0,0 +1,10 @@
[
{
"pk": 1,
"model": "tests.TestModel",
"fields": {
"title": "foo",
"text": "bar"
}
}
]

View file

@ -195,6 +195,7 @@ class NameModel(models.Model):
class ManagerTestModel(models.Model):
title = models.CharField(ugettext_lazy('title'), max_length=255)
visits = models.IntegerField(ugettext_lazy('visits'), default=0)
description = models.CharField(max_length=255, null=True)
class Meta:
ordering = ('-visits',)

View file

@ -109,7 +109,7 @@ translator.register(RichTextPage)
########## Manager testing
class ManagerTestModelTranslationOptions(TranslationOptions):
fields = ('title', 'visits')
fields = ('title', 'visits', 'description')
translator.register(ManagerTestModel, ManagerTestModelTranslationOptions)

View file

@ -3,6 +3,7 @@ from django.conf import settings
from django.db.models import Manager
from django.db.models.base import ModelBase
from modeltranslation import settings as mt_settings
from modeltranslation.fields import TranslationFieldDescriptor, create_translation_field
from modeltranslation.manager import MultilingualManager, rewrite_lookup_key
from modeltranslation.utils import build_localized_fieldname
@ -133,6 +134,7 @@ def patch_constructor(model):
old_init = model.__init__
def new_init(self, *args, **kwargs):
populate_translation_fields(self.__class__, kwargs)
for key, val in kwargs.items():
new_key = rewrite_lookup_key(model, key)
# Old key is intentionally left in case old_init wants to play with it
@ -174,6 +176,56 @@ def delete_cache_fields(model):
pass
def populate_translation_fields(sender, kwargs):
"""
When models are created or loaded from fixtures, replicates values
provided for translatable fields to some / all empty translation fields,
according to the current population mode.
Population is performed only on keys (field names) present in kwargs.
Nothing is returned, but passed kwargs dictionary is altered.
With ``mode`` set to:
-- ``all``: fills all translation fields, skipping just those for
which a translated value is also provided;
-- ``default``: fills only the default translation (unless it is
additionally provided);
-- ``required``: like ``default``, but only if the original field is
non-nullable;
At least the ``required`` mode should be used when loading untranslated
fixtures to keep the database consistent (note that Django management
commands are normally forced to run with hardcoded ``en-us`` language
active). The ``default`` mode is useful if you need to ensure fallback
values are available, and ``all`` if you need to have all translations
defined (for example to make lookups / filtering without resorting to
query fallbacks).
"""
populate = mt_settings.AUTO_POPULATE
if not populate:
return
if populate is True:
# What was meant by ``True`` is now called ``all``.
populate = 'all'
opts = translator.get_options_for_model(sender)
for key, val in kwargs.items():
if key in opts.fields:
if populate == 'all':
# Set the value for every language.
for translation_field in opts.fields[key]:
kwargs.setdefault(translation_field.name, val)
elif populate == 'default':
default = build_localized_fieldname(key, mt_settings.DEFAULT_LANGUAGE)
kwargs.setdefault(default, val)
elif populate == 'required':
default = build_localized_fieldname(key, mt_settings.DEFAULT_LANGUAGE)
if not sender._meta.get_field(key).null:
kwargs.setdefault(default, val)
else:
raise AttributeError("Unknown population mode '%s'." % populate)
class Translator(object):
"""
A Translator object encapsulates an instance of a translator. Models are
@ -248,9 +300,6 @@ class Translator(object):
fallback_languages=model_fallback_languages)
setattr(model, field_name, descriptor)
#signals.pre_init.connect(translated_model_initializing, sender=model,
#weak=False)
def unregister(self, model_or_iterable):
"""
Unregisters the given model(s).

View file

@ -1,4 +1,6 @@
# -*- coding: utf-8 -*-
from contextlib import contextmanager
from django.utils.encoding import force_unicode
from django.utils.translation import get_language as _get_language
from django.utils.functional import lazy
@ -94,3 +96,30 @@ def resolution_order(lang, override=None):
fallback_def = override.get('default', settings.FALLBACK_LANGUAGES['default'])
order = (lang,) + fallback_for_lang + fallback_def
return tuple(unique(order))
@contextmanager
def auto_populate(mode='all'):
"""
Overrides translation fields population mode (population mode decides which
unprovided translations will be filled during model construction / loading).
Example:
with auto_populate('all'):
s = Slugged.objects.create(title='foo')
s.title_en == 'foo' // True
s.title_de == 'foo' // True
This method may be used to ensure consistency loading untranslated fixtures,
with non-default language active:
with auto_populate('required'):
call_command('loaddata', 'fixture.json')
"""
current_population_mode = settings.AUTO_POPULATE
settings.AUTO_POPULATE = mode
try:
yield
finally:
settings.AUTO_POPULATE = current_population_mode