mirror of
https://github.com/Hopiu/django-modeltranslation.git
synced 2026-05-24 04:03:45 +00:00
Merge branch 'populate-modes-cleaned'
This commit is contained in:
commit
9e41538035
9 changed files with 254 additions and 43 deletions
|
|
@ -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``
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
10
modeltranslation/tests/fixtures/fixture.json
vendored
Normal file
10
modeltranslation/tests/fixtures/fixture.json
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
[
|
||||
{
|
||||
"pk": 1,
|
||||
"model": "tests.TestModel",
|
||||
"fields": {
|
||||
"title": "foo",
|
||||
"text": "bar"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -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',)
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ translator.register(RichTextPage)
|
|||
########## Manager testing
|
||||
|
||||
class ManagerTestModelTranslationOptions(TranslationOptions):
|
||||
fields = ('title', 'visits')
|
||||
fields = ('title', 'visits', 'description')
|
||||
translator.register(ManagerTestModel, ManagerTestModelTranslationOptions)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue