Add MultilingualManager and its tests.

This commit is contained in:
Jacek Tomaszewski 2012-11-11 15:57:44 +01:00
parent 3868e461e8
commit 0949e95441
8 changed files with 397 additions and 7 deletions

146
modeltranslation/manager.py Normal file
View file

@ -0,0 +1,146 @@
"""
The idea of MultilingualManager is taken from
django-linguo by Zach Mathew
https://github.com/zmathew/django-linguo
"""
from django.db import models
from django.db.models.fields.related import RelatedField
from django.utils.translation import get_language
from django.utils.tree import Node
from modeltranslation.utils import build_localized_fieldname
from modeltranslation import settings
_registry = {}
def get_translatable_fields_for_model(model):
from modeltranslation import translator
if model not in _registry:
try:
_registry[model] = dict(
translator.translator.get_options_for_model(model).localized_fieldnames)
except translator.NotRegistered:
_registry[model] = None
return _registry[model]
def rewrite_lookup_key(model, lookup_key):
translatable_fields = get_translatable_fields_for_model(model)
if translatable_fields is not None:
pieces = lookup_key.split('__')
# If we are doing a lookup on a translatable field,
# we want to rewrite it to the actual field name
# For example, we want to rewrite "name__startswith" to "name_fr__startswith"
if pieces[0] in translatable_fields:
lookup_key = build_localized_fieldname(pieces[0], get_language())
remaining_lookup = '__'.join(pieces[1:])
if remaining_lookup:
lookup_key = '%s__%s' % (lookup_key, remaining_lookup)
pieces = lookup_key.split('__')
if len(pieces) > 1:
# Check if we are doing a lookup to a related trans model
fields_to_trans_models = get_fields_to_translatable_models(model)
for field_to_trans, transmodel in fields_to_trans_models:
if pieces[0] == field_to_trans:
sub_lookup = '__'.join(pieces[1:])
if sub_lookup:
sub_lookup = rewrite_lookup_key(transmodel, sub_lookup)
lookup_key = '%s__%s' % (pieces[0], sub_lookup)
break
return lookup_key
def get_fields_to_translatable_models(model):
results = []
for field_name in model._meta.get_all_field_names():
field_object, modelclass, direct, m2m = model._meta.get_field_by_name(field_name)
if direct and isinstance(field_object, RelatedField):
if get_translatable_fields_for_model(field_object.related.parent_model) is not None:
results.append((field_name, field_object.related.parent_model))
return results
class MultilingualQuerySet(models.query.QuerySet):
def __init__(self, *args, **kwargs):
super(MultilingualQuerySet, self).__init__(*args, **kwargs)
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
# it can be rewritten. Otherwise sql.compiler will grab it directly from _meta
ordering = []
for key in self.model._meta.ordering:
ordering.append(rewrite_lookup_key(self.model, key))
self.query.add_ordering(*ordering)
# This method was not present in django-linguo
def _rewrite_q(self, q):
"Rewrite field names inside Q call."
if isinstance(q, tuple) and len(q) == 2:
return rewrite_lookup_key(self.model, q[0]), q[1]
if isinstance(q, Node):
q.children = map(self._rewrite_q, q.children)
return q
# This method was not present in django-linguo
def _rewrite_f(self, q):
"Rewrite field names inside F call."
if isinstance(q, models.F):
q.name = rewrite_lookup_key(self.model, q.name)
return q
if isinstance(q, Node):
q.children = map(self._rewrite_f, q.children)
return q
def _filter_or_exclude(self, negate, *args, **kwargs):
args = map(self._rewrite_q, args)
for key, val in kwargs.items():
new_key = rewrite_lookup_key(self.model, key)
del kwargs[key]
kwargs[new_key] = self._rewrite_f(val)
return super(MultilingualQuerySet, self)._filter_or_exclude(negate, *args, **kwargs)
def order_by(self, *field_names):
new_args = []
for key in field_names:
new_args.append(rewrite_lookup_key(self.model, key))
return super(MultilingualQuerySet, self).order_by(*new_args)
def update(self, **kwargs):
for key, val in kwargs.items():
new_key = rewrite_lookup_key(self.model, key)
del kwargs[key]
kwargs[new_key] = self._rewrite_f(val)
return super(MultilingualQuerySet, self).update(**kwargs)
update.alters_data = True
# 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 new_key in translatable_fields[key]:
kwargs.setdefault(new_key, val)
else:
# If not use populate feature, then perform normal rewriting
for key, val in kwargs.items():
new_key = rewrite_lookup_key(self.model, key)
del kwargs[key]
kwargs[new_key] = val
return super(MultilingualQuerySet, self).create(**kwargs)
class MultilingualManager(models.Manager):
use_for_related_fields = True
def get_query_set(self):
return MultilingualQuerySet(self.model)

View file

@ -32,3 +32,6 @@ ENABLE_REGISTRATIONS = getattr(
# Modeltranslation specific debug setting
DEBUG = getattr(
settings, 'MODELTRANSLATION_DEBUG', settings.DEBUG)
AUTO_POPULATE = getattr(
settings, 'MODELTRANSLATION_AUTO_POPULATE', False)

View file

@ -16,6 +16,7 @@ from django.contrib.admin.sites import AdminSite
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.core.files.base import ContentFile
from django.db.models import Q, F
from django.db.models.loading import AppCache
from django.test import TestCase
from django.utils.datastructures import SortedDict
@ -28,7 +29,7 @@ from modeltranslation.admin import (TranslationAdmin,
from modeltranslation.tests.models import (
AbstractModelB, MultitableModelA, DataModel, FallbackModel, FallbackModel2,
FileFieldsModel, OtherFieldsModel, TestModel, MultitableBModelA, MultitableModelC,
MultitableDTestModel)
MultitableDTestModel, ManagerTestModel, CustomManagerTestModel)
from modeltranslation.tests.translation import FallbackModel2TranslationOptions
from modeltranslation.tests.test_settings import TEST_SETTINGS
@ -36,6 +37,10 @@ try:
from django.test.utils import override_settings
except ImportError:
from modeltranslation.tests.utils import override_settings
try:
from django.utils.translation import override
except ImportError:
from modeltranslation.tests.utils import override # NOQA
# None of the following tests really depend on the content of the request,
# so we'll just pass in None.
@ -151,7 +156,7 @@ class ModeltranslationTest(ModeltranslationTestBase):
self.failUnless(translator.translator)
# Check that all models are registered for translation
self.failUnlessEqual(len(translator.translator._registry), 10)
self.failUnlessEqual(len(translator.translator._registry), 12)
# Try to unregister a model that is not registered
self.assertRaises(translator.NotRegistered,
@ -215,7 +220,7 @@ class ModeltranslationTest(ModeltranslationTestBase):
def test_titleonly(self):
title1_de = "title de"
n = TestModel.objects.create(title=title1_de)
n = TestModel(title=title1_de)
self.failUnlessEqual(n.title, title1_de)
# Because the original field "title" was specified in the constructor
# it is directly passed into the instance's __dict__ and the descriptor
@ -352,7 +357,7 @@ class OtherFieldsTest(ModeltranslationTestBase):
self.assertEqual('de', get_language())
self.assertEqual(7, inst.int)
self.assertEqual(7, inst.int_de)
self.assertEqual(42, inst.int_en) # default value is honored
self.assertEqual(42, inst.int_en) # default value is honored
inst.int += 2
inst.save()
@ -1121,3 +1126,161 @@ class TranslationAdminTest(ModeltranslationTestBase):
ma_fieldsets = ma.inlines[0](
TestModel, self.site).get_fieldsets(request, self.test_obj)
self.assertEqual(ma_fieldsets, fieldsets)
class TestManager(ModeltranslationTestBase):
def setUp(self):
# In this test case the default language is en, not de.
trans_real.activate('en')
def test_filter_update(self):
"""Test if filtering and updating is language-aware."""
n = ManagerTestModel(title='')
n.title_en = 'en'
n.title_de = 'de'
n.save()
m = ManagerTestModel(title='')
m.title_en = 'title en'
m.title_de = 'de'
m.save()
self.assertEqual('en', get_language())
self.assertEqual(0, ManagerTestModel.objects.filter(title='de').count())
self.assertEqual(1, ManagerTestModel.objects.filter(title='en').count())
# Spanning works
self.assertEqual(2, ManagerTestModel.objects.filter(title__contains='en').count())
with override('de'):
self.assertEqual(2, ManagerTestModel.objects.filter(title='de').count())
self.assertEqual(0, ManagerTestModel.objects.filter(title='en').count())
# Spanning works
self.assertEqual(2, ManagerTestModel.objects.filter(title__endswith='e').count())
# Still possible to use explicit language version
self.assertEqual(1, ManagerTestModel.objects.filter(title_en='en').count())
self.assertEqual(2, ManagerTestModel.objects.filter(title_en__contains='en').count())
ManagerTestModel.objects.update(title='new')
self.assertEqual(2, ManagerTestModel.objects.filter(title='new').count())
n = ManagerTestModel.objects.get(pk=n.pk)
m = ManagerTestModel.objects.get(pk=m.pk)
self.assertEqual('en', n.title_en)
self.assertEqual('new', n.title_de)
self.assertEqual('title en', m.title_en)
self.assertEqual('new', m.title_de)
def test_q(self):
"""Test if Q queries are rewritten."""
n = ManagerTestModel(title='')
n.title_en = 'en'
n.title_de = 'de'
n.save()
self.assertEqual('en', get_language())
self.assertEqual(0, ManagerTestModel.objects.filter(Q(title='de') | Q(pk=42)).count())
self.assertEqual(1, ManagerTestModel.objects.filter(Q(title='en') | Q(pk=42)).count())
with override('de'):
self.assertEqual(1, ManagerTestModel.objects.filter(Q(title='de') | Q(pk=42)).count())
self.assertEqual(0, ManagerTestModel.objects.filter(Q(title='en') | Q(pk=42)).count())
def test_f(self):
"""Test if F queries are rewritten."""
n = ManagerTestModel.objects.create(visits_en=1, visits_de=2)
self.assertEqual('en', get_language())
ManagerTestModel.objects.update(visits=F('visits') + 10)
n = ManagerTestModel.objects.all()[0]
self.assertEqual(n.visits_en, 11)
self.assertEqual(n.visits_de, 2)
with override('de'):
ManagerTestModel.objects.update(visits=F('visits') + 20)
n = ManagerTestModel.objects.all()[0]
self.assertEqual(n.visits_en, 11)
self.assertEqual(n.visits_de, 22)
def test_custom_manager(self):
"""Test if user-defined manager is still working"""
n = CustomManagerTestModel(title='')
n.title_en = 'enigma'
n.title_de = 'foo'
n.save()
m = CustomManagerTestModel(title='')
m.title_en = 'enigma'
m.title_de = 'bar'
m.save()
# Custom method
self.assertEqual('bar', CustomManagerTestModel.objects.foo())
# Ensure that get_query_set is working - filter objects to those with 'a' in title
self.assertEqual('en', get_language())
self.assertEqual(2, CustomManagerTestModel.objects.count())
with override('de'):
self.assertEqual(1, CustomManagerTestModel.objects.count())
def test_creation(self):
"""Test if field are rewritten in create."""
self.assertEqual('en', get_language())
n = ManagerTestModel.objects.create(title='foo')
self.assertEqual('foo', n.title_en)
self.assertEqual(None, n.title_de)
self.assertEqual('foo', n.title)
# The same result
n = ManagerTestModel.objects.create(title_en='foo')
self.assertEqual('foo', n.title_en)
self.assertEqual(None, n.title_de)
self.assertEqual('foo', n.title)
def test_creation_population(self):
"""Test if language fields are populated with default value on creation."""
n = ManagerTestModel.objects.create(title='foo', _populate=True)
self.assertEqual('foo', n.title_en)
self.assertEqual('foo', n.title_de)
self.assertEqual('foo', n.title)
# You can specify some language...
n = ManagerTestModel.objects.create(title='foo', title_de='bar', _populate=True)
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 = 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 = 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)
self.assertEqual(True, mt_settings.AUTO_POPULATE)
n = 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 = ManagerTestModel.objects.create(title='foo', _populate=False)
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)

View file

@ -69,3 +69,21 @@ class AbstractModelB(AbstractModelA):
class DataModel(models.Model):
data = models.TextField(blank=True, null=True)
class ManagerTestModel(models.Model):
title = models.CharField(ugettext_lazy('title'), max_length=255)
visits = models.IntegerField(ugettext_lazy('visits'), default=0)
class CustomManager(models.Manager):
def get_query_set(self):
return super(CustomManager, self).get_query_set().filter(title__contains='a')
def foo(self):
return 'bar'
class CustomManagerTestModel(models.Model):
title = models.CharField(ugettext_lazy('title'), max_length=255)
objects = CustomManager()

View file

@ -26,3 +26,5 @@ LANGUAGE_CODE = 'de'
DEFAULT_LANGUAGE = 'de'
USE_I18N = True
MODELTRANSLATION_AUTO_POPULATE = False

View file

@ -5,7 +5,8 @@ from modeltranslation.translator import translator, TranslationOptions
from modeltranslation.tests.models import (
TestModel, FallbackModel, FallbackModel2,
FileFieldsModel, OtherFieldsModel, AbstractModelA, AbstractModelB,
MultitableModelA, MultitableBModelA, MultitableModelC)
MultitableModelA, MultitableBModelA, MultitableModelC,
ManagerTestModel, CustomManagerTestModel)
class TestTranslationOptions(TranslationOptions):
@ -59,3 +60,13 @@ translator.register(AbstractModelA, AbstractModelATranslationOptions)
class AbstractModelBTranslationOptions(TranslationOptions):
fields = ('titleb',)
translator.register(AbstractModelB, AbstractModelBTranslationOptions)
class ManagerTestModelTranslationOptions(TranslationOptions):
fields = ('title', 'visits')
translator.register(ManagerTestModel, ManagerTestModelTranslationOptions)
class CustomManagerTestModelTranslationOptions(TranslationOptions):
fields = ('title',)
translator.register(CustomManagerTestModel, CustomManagerTestModelTranslationOptions)

View file

@ -1,5 +1,6 @@
"""
This is Django 1.4 override_settings decorator backported for compatibility with Django 1.3.
This are Django 1.4 override_settings decorator and override (language) context manager
backported for compatibility with Django 1.3.
The only difference is that this version does not use settings_changes signal
(because there is no such signal).
@ -8,6 +9,7 @@ from __future__ import with_statement # Python 2.5 compatibility
from django.utils.functional import wraps
from django.conf import settings, UserSettingsHolder
from django.utils.translation import get_language, activate, deactivate, deactivate_all
class override_settings(object):
@ -58,3 +60,22 @@ class override_settings(object):
def disable(self):
settings._wrapped = self.wrapped
class override(object):
def __init__(self, language, deactivate=False):
self.language = language
self.deactivate = deactivate
self.old_language = get_language()
def __enter__(self):
if self.language is not None:
activate(self.language)
else:
deactivate_all()
def __exit__(self, exc_type, exc_value, traceback):
if self.deactivate:
deactivate()
else:
activate(self.old_language)

View file

@ -1,9 +1,11 @@
# -*- coding: utf-8 -*-
from django.conf import settings
from django.db.models import Manager
from django.db.models.base import ModelBase
from modeltranslation.fields import (TranslationFieldDescriptor,
create_translation_field)
from modeltranslation.manager import MultilingualManager
from modeltranslation.utils import build_localized_fieldname
@ -31,7 +33,7 @@ class TranslationOptions(object):
def add_localized_fields(model):
"""
Monkey patchs the original model class to provide additional fields for
Monkey patches the original model class to provide additional fields for
every language. Only do that for fields which are defined in the
translation options of the model.
@ -61,6 +63,26 @@ def add_localized_fields(model):
return localized_fields
def add_manager(model):
"""
Monkey patches the original model to use MultilingualManager instead of
default manager (``objects``).
If model has a custom manager, then merge it with MultilingualManager.
"""
if not hasattr(model, 'objects'):
return
current_manager = model.objects
if isinstance(current_manager, MultilingualManager):
return
if current_manager.__class__ is Manager:
current_manager.__class__ = MultilingualManager
else:
class NewMultilingualManager(current_manager.__class__, MultilingualManager):
pass
current_manager.__class__ = NewMultilingualManager
#def translated_model_initialized(field_names, instance, **kwargs):
#print "translated_model_initialized instance:", \
#instance, ", field:", field_names
@ -152,6 +174,10 @@ class Translator(object):
for related_obj in model._meta.get_all_related_objects():
delete_cache_fields(related_obj.model)
# Set MultilingualManager
add_manager(model)
# Substitute original field with descriptor
model_fallback_values = getattr(
translation_opts, 'fallback_values', None)
for field_name in translation_opts.fields: