From f168b5573702749717480ec1f16ac5b8883bebe0 Mon Sep 17 00:00:00 2001 From: wrwrwr Date: Tue, 5 Feb 2013 15:15:33 +0100 Subject: [PATCH 1/4] Apply population using a pre_save signal. It can be used with ``get_or_create`` (without resaving) and, more importantly, fixture loading. Extend population by providing two new modes: ``default`` that sets just the default translation (only if its not given; ensuring a fallback is available; alike ``update_translation_fields``) and ``required`` that further limits population by only filling default translation of non-nullable fields (minimum to keep some level of consistency). Add a ``populate(mode)`` toggle on multilingual query set / manager and an ``auto_populate`` context manager for fixture loading. --- modeltranslation/manager.py | 55 ++++++++---- modeltranslation/tests/__init__.py | 94 +++++++++++++++++--- modeltranslation/tests/fixtures/fixture.json | 10 +++ modeltranslation/tests/models.py | 1 + modeltranslation/tests/translation.py | 2 +- modeltranslation/translator.py | 60 ++++++++++++- modeltranslation/utils.py | 29 ++++++ 7 files changed, 219 insertions(+), 32 deletions(-) create mode 100644 modeltranslation/tests/fixtures/fixture.json diff --git a/modeltranslation/manager.py b/modeltranslation/manager.py index 871e8e8..7ed60ee 100644 --- a/modeltranslation/manager.py +++ b/modeltranslation/manager.py @@ -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,32 @@ class MultilingualQuerySet(models.query.QuerySet): return super(MultilingualQuerySet, self).update(**kwargs) update.alters_data = True + # This method was not present in django-linguo + def _populate_mode(self, **kwargs): + # Populate can be set using a global setting, a manager method, + # or given as an argument to ``create`` or ``get_or_create``. + populate = kwargs.pop('_populate', self._populate) + if populate is None: + populate = settings.AUTO_POPULATE + return kwargs, 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`` keyword. + """ + kwargs, mode = self._populate_mode(**kwargs) + with auto_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`` keyword. + """ + kwargs, mode = self._populate_mode(**kwargs) + with auto_populate(mode): + return super(MultilingualQuerySet, self).get_or_create(**kwargs) class MultilingualManager(models.Manager): @@ -195,6 +217,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: diff --git a/modeltranslation/tests/__init__.py b/modeltranslation/tests/__init__.py index 2497afa..7fd9b8d 100644 --- a/modeltranslation/tests/__init__.py +++ b/modeltranslation/tests/__init__.py @@ -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='') @@ -1876,14 +1882,15 @@ class TestManager(ModeltranslationTestBase): 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) - self.assertEqual('bar', n.title_en) - self.assertEqual('foo', n.title_de) - self.assertEqual('bar', n.title) # points to en - with override('de'): - self.assertEqual('foo', n.title) # points to de - self.assertEqual('en', get_language()) + # TODO: This kind of behaviour seems hardly doable for fixtures. +# self.assertEqual('en', get_language()) +# n = models.ManagerTestModel.objects.create(title='foo', title_en='bar', _populate=True) +# self.assertEqual('bar', n.title_en) +# # self.assertEqual('foo', n.title_de) +# self.assertEqual('bar', n.title) # points to en +# with override('de'): +# self.assertEqual('foo', n.title) # points to de +# self.assertEqual('en', get_language()) # This feature (for backward-compatibility) require _populate keyword... n = models.ManagerTestModel.objects.create(title='foo') @@ -1891,9 +1898,45 @@ class TestManager(ModeltranslationTestBase): self.assertEqual(None, n.title_de) self.assertEqual('foo', n.title) + # Populate ``default`` fills just the default translation. + # TODO: Having more languages would make these tests more meaningful. + qs = models.ManagerTestModel.objects + m = qs.create(title='foo', description='bar', _populate='default') + 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.create(title='foo', description='bar', _populate='default') + 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.create(title='foo', description='bar', _populate='required') + 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.create(title='foo', description='bar', _populate='required') + 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 may be used as a manager toggle. + m = qs.populate(False).create(title='foo') + self.assertEqual('foo', m.title_en) + self.assertEqual(None, m.title_de) + m = qs.populate(True).create(title='foo') + self.assertEqual('foo', m.title_en) + self.assertEqual('foo', m.title_de) + # ... 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) @@ -1906,6 +1949,29 @@ class TestManager(ModeltranslationTestBase): self.assertEqual(None, n.title_de) self.assertEqual('foo', n.title) - # Restore previous state - reload(mt_settings) - self.assertEqual(False, mt_settings.AUTO_POPULATE) + def test_get_or_create_population(self): + """ + Populate may be used with ``get_or_create``. + """ + qs = models.ManagerTestModel.objects + m1, created1 = qs.get_or_create(title='aaa', _populate=True) + m2, created2 = qs.get_or_create(title='aaa', _populate=True) + 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) diff --git a/modeltranslation/tests/fixtures/fixture.json b/modeltranslation/tests/fixtures/fixture.json new file mode 100644 index 0000000..83462f3 --- /dev/null +++ b/modeltranslation/tests/fixtures/fixture.json @@ -0,0 +1,10 @@ +[ + { + "pk": 1, + "model": "tests.TestModel", + "fields": { + "title": "foo", + "text": "bar" + } + } +] diff --git a/modeltranslation/tests/models.py b/modeltranslation/tests/models.py index d963a6f..b3f73ad 100644 --- a/modeltranslation/tests/models.py +++ b/modeltranslation/tests/models.py @@ -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',) diff --git a/modeltranslation/tests/translation.py b/modeltranslation/tests/translation.py index 40a00fc..397ac29 100644 --- a/modeltranslation/tests/translation.py +++ b/modeltranslation/tests/translation.py @@ -109,7 +109,7 @@ translator.register(RichTextPage) ########## Manager testing class ManagerTestModelTranslationOptions(TranslationOptions): - fields = ('title', 'visits') + fields = ('title', 'visits', 'description') translator.register(ManagerTestModel, ManagerTestModelTranslationOptions) diff --git a/modeltranslation/translator.py b/modeltranslation/translator.py index 4f4f809..8b1b304 100644 --- a/modeltranslation/translator.py +++ b/modeltranslation/translator.py @@ -2,7 +2,9 @@ from django.conf import settings from django.db.models import Manager from django.db.models.base import ModelBase +from django.db.models.signals import pre_save +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 @@ -174,6 +176,59 @@ def delete_cache_fields(model): pass +def populate_translation_fields(sender, instance, **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. Callback for registered models + ``pre_save`` signal. + + 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 field, translation_fields in opts.fields.iteritems(): + if populate == 'all': + # Set the value for every language. + for trans in translation_fields: + if getattr(instance, trans.name) is None: + setattr(instance, trans.name, getattr(instance, field)) + elif populate == 'default': + # Set the value just for the default language. + default = build_localized_fieldname(field, mt_settings.DEFAULT_LANGUAGE) + if getattr(instance, default) is None: + setattr(instance, default, getattr(instance, field)) + elif populate == 'required': + # Set the default language only if the field + # field was non-nullable. + default = build_localized_fieldname(field, mt_settings.DEFAULT_LANGUAGE) + if (getattr(instance, default) is None and + not sender._meta.get_field(field).null): + setattr(instance, default, getattr(instance, field)) + else: + raise AttributeError("Unknown population mode '%s'." % populate) + + class Translator(object): """ A Translator object encapsulates an instance of a translator. Models are @@ -248,8 +303,8 @@ class Translator(object): fallback_languages=model_fallback_languages) setattr(model, field_name, descriptor) - #signals.pre_init.connect(translated_model_initializing, sender=model, - #weak=False) + # Populate translation fields before the model is saved. + pre_save.connect(populate_translation_fields, sender=model) def unregister(self, model_or_iterable): """ @@ -276,6 +331,7 @@ class Translator(object): 'You need to unregister descendant "%s" before' ' unregistering its base "%s"' % (desc.__name__, model.__name__)) + pre_save.disconnect(populate_translation_fields, sender=model) del self._registry[desc] def get_registered_models(self, abstract=True): diff --git a/modeltranslation/utils.py b/modeltranslation/utils.py index c7756d1..d3049a4 100644 --- a/modeltranslation/utils.py +++ b/modeltranslation/utils.py @@ -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 From 176f4aeeb1c03321e94dc1e1009affe0a1ef24d1 Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Tue, 19 Feb 2013 00:09:40 +0100 Subject: [PATCH 2/4] Refactor auto-populating: move code to constructor. Constructor is better place than pre_save signal since it has access to all passed arguments (field names). Also reenabled broken test. --- modeltranslation/tests/__init__.py | 17 +++++------ modeltranslation/translator.py | 49 +++++++++++++----------------- 2 files changed, 29 insertions(+), 37 deletions(-) diff --git a/modeltranslation/tests/__init__.py b/modeltranslation/tests/__init__.py index 7fd9b8d..49e664c 100644 --- a/modeltranslation/tests/__init__.py +++ b/modeltranslation/tests/__init__.py @@ -1882,15 +1882,14 @@ class TestManager(ModeltranslationTestBase): self.assertEqual('foo', n.title) # ... but remember that still original attribute points to current language - # TODO: This kind of behaviour seems hardly doable for fixtures. -# self.assertEqual('en', get_language()) -# n = models.ManagerTestModel.objects.create(title='foo', title_en='bar', _populate=True) -# self.assertEqual('bar', n.title_en) -# # self.assertEqual('foo', n.title_de) -# self.assertEqual('bar', n.title) # points to en -# with override('de'): -# self.assertEqual('foo', n.title) # points to de -# self.assertEqual('en', get_language()) + self.assertEqual('en', get_language()) + n = models.ManagerTestModel.objects.create(title='foo', title_en='bar', _populate=True) + self.assertEqual('bar', n.title_en) + self.assertEqual('foo', n.title_de) + self.assertEqual('bar', n.title) # points to en + with override('de'): + self.assertEqual('foo', n.title) # points to de + self.assertEqual('en', get_language()) # This feature (for backward-compatibility) require _populate keyword... n = models.ManagerTestModel.objects.create(title='foo') diff --git a/modeltranslation/translator.py b/modeltranslation/translator.py index 8b1b304..0dd810f 100644 --- a/modeltranslation/translator.py +++ b/modeltranslation/translator.py @@ -2,7 +2,6 @@ from django.conf import settings from django.db.models import Manager from django.db.models.base import ModelBase -from django.db.models.signals import pre_save from modeltranslation import settings as mt_settings from modeltranslation.fields import TranslationFieldDescriptor, create_translation_field @@ -135,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 @@ -176,12 +176,14 @@ def delete_cache_fields(model): pass -def populate_translation_fields(sender, instance, **kwargs): +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. Callback for registered models - ``pre_save`` signal. + 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 @@ -207,26 +209,21 @@ def populate_translation_fields(sender, instance, **kwargs): populate = 'all' opts = translator.get_options_for_model(sender) - for field, translation_fields in opts.fields.iteritems(): - if populate == 'all': - # Set the value for every language. - for trans in translation_fields: - if getattr(instance, trans.name) is None: - setattr(instance, trans.name, getattr(instance, field)) - elif populate == 'default': - # Set the value just for the default language. - default = build_localized_fieldname(field, mt_settings.DEFAULT_LANGUAGE) - if getattr(instance, default) is None: - setattr(instance, default, getattr(instance, field)) - elif populate == 'required': - # Set the default language only if the field - # field was non-nullable. - default = build_localized_fieldname(field, mt_settings.DEFAULT_LANGUAGE) - if (getattr(instance, default) is None and - not sender._meta.get_field(field).null): - setattr(instance, default, getattr(instance, field)) - else: - raise AttributeError("Unknown population mode '%s'." % populate) + 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): @@ -303,9 +300,6 @@ class Translator(object): fallback_languages=model_fallback_languages) setattr(model, field_name, descriptor) - # Populate translation fields before the model is saved. - pre_save.connect(populate_translation_fields, sender=model) - def unregister(self, model_or_iterable): """ Unregisters the given model(s). @@ -331,7 +325,6 @@ class Translator(object): 'You need to unregister descendant "%s" before' ' unregistering its base "%s"' % (desc.__name__, model.__name__)) - pre_save.disconnect(populate_translation_fields, sender=model) del self._registry[desc] def get_registered_models(self, abstract=True): From cbfb1563a2d3c460dc6f05fa8e7a25747b56d412 Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Tue, 19 Feb 2013 18:03:41 +0100 Subject: [PATCH 3/4] Remove ``_populate`` keyword. Other ways of managing population are preferred and more elegant. --- modeltranslation/manager.py | 23 ++++----- modeltranslation/tests/__init__.py | 82 ++++++++++++++---------------- 2 files changed, 47 insertions(+), 58 deletions(-) diff --git a/modeltranslation/manager.py b/modeltranslation/manager.py index 7ed60ee..68339e9 100644 --- a/modeltranslation/manager.py +++ b/modeltranslation/manager.py @@ -184,30 +184,27 @@ class MultilingualQuerySet(models.query.QuerySet): update.alters_data = True # This method was not present in django-linguo - def _populate_mode(self, **kwargs): - # Populate can be set using a global setting, a manager method, - # or given as an argument to ``create`` or ``get_or_create``. - populate = kwargs.pop('_populate', self._populate) - if populate is None: - populate = settings.AUTO_POPULATE - return kwargs, populate + @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): """ - Allows to override population mode with a ``_populate`` keyword. + Allows to override population mode with a ``populate`` method. """ - kwargs, mode = self._populate_mode(**kwargs) - with auto_populate(mode): + 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`` keyword. + Allows to override population mode with a ``populate`` method. """ - kwargs, mode = self._populate_mode(**kwargs) - with auto_populate(mode): + with auto_populate(self._populate_mode): return super(MultilingualQuerySet, self).get_or_create(**kwargs) diff --git a/modeltranslation/tests/__init__.py b/modeltranslation/tests/__init__.py index 49e664c..d9dc880 100644 --- a/modeltranslation/tests/__init__.py +++ b/modeltranslation/tests/__init__.py @@ -1870,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 @@ -1891,49 +1891,12 @@ 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) - # Populate ``default`` fills just the default translation. - # TODO: Having more languages would make these tests more meaningful. - qs = models.ManagerTestModel.objects - m = qs.create(title='foo', description='bar', _populate='default') - 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.create(title='foo', description='bar', _populate='default') - 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.create(title='foo', description='bar', _populate='required') - 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.create(title='foo', description='bar', _populate='required') - 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 may be used as a manager toggle. - m = qs.populate(False).create(title='foo') - self.assertEqual('foo', m.title_en) - self.assertEqual(None, m.title_de) - m = qs.populate(True).create(title='foo') - self.assertEqual('foo', m.title_en) - self.assertEqual('foo', m.title_de) - # ... or MODELTRANSLATION_AUTO_POPULATE setting with reload_override_settings(MODELTRANSLATION_AUTO_POPULATE=True): self.assertEqual(True, mt_settings.AUTO_POPULATE) @@ -1942,19 +1905,48 @@ class TestManager(ModeltranslationTestBase): 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) + # 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.get_or_create(title='aaa', _populate=True) - m2, created2 = qs.get_or_create(title='aaa', _populate=True) + 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) From d31c4f833beef87c903bbd7340663422ce2ff596 Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Tue, 19 Feb 2013 18:04:27 +0100 Subject: [PATCH 4/4] Update docs. --- docs/modeltranslation/installation.rst | 11 +++-- docs/modeltranslation/usage.rst | 56 +++++++++++++++++++++----- 2 files changed, 55 insertions(+), 12 deletions(-) diff --git a/docs/modeltranslation/installation.rst b/docs/modeltranslation/installation.rst index 1aae289..6629781 100644 --- a/docs/modeltranslation/installation.rst +++ b/docs/modeltranslation/installation.rst @@ -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`` diff --git a/docs/modeltranslation/usage.rst b/docs/modeltranslation/usage.rst index a29b153..9310fd3 100644 --- a/docs/modeltranslation/usage.rst +++ b/docs/modeltranslation/usage.rst @@ -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: