mirror of
https://github.com/Hopiu/django-modeltranslation.git
synced 2026-05-15 16:33:08 +00:00
Add OneToOneField support.
This commit is contained in:
parent
81c9d55c1e
commit
0e76f1ec7d
7 changed files with 235 additions and 24 deletions
|
|
@ -12,7 +12,7 @@
|
|||
(resolves issue #171)
|
||||
ADDED: Only/defer methods to MultilingualManager.
|
||||
(resolves issue #166)
|
||||
ADDED: Support for ForeignKey.
|
||||
ADDED: Support for ForeignKey and OneToOneField.
|
||||
(thanks to Braden MacDonald and Jacek Tomaszewski,
|
||||
resolves issue #161)
|
||||
ADDED: An auto-population option to the loaddata command.
|
||||
|
|
|
|||
|
|
@ -178,7 +178,7 @@ Model Field 0.4 0.5 0.7
|
|||
``TimeField`` |n| |y| |y|
|
||||
``URLField`` |i| |i| |i|
|
||||
``ForeignKey`` |n| |n| |y|
|
||||
``OneToOneField`` |n| |n| |n|
|
||||
``OneToOneField`` |n| |n| |y|
|
||||
``ManyToManyField`` |n| |n| |n|
|
||||
=============================== === === ===
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ SUPPORTED_FIELDS = (
|
|||
fields.files.FileField,
|
||||
fields.files.ImageField,
|
||||
fields.related.ForeignKey,
|
||||
# Above implies also OneToOneField
|
||||
)
|
||||
try:
|
||||
SUPPORTED_FIELDS += (fields.GenericIPAddressField,) # Django 1.4+ only
|
||||
|
|
@ -303,3 +304,16 @@ class TranslatedRelationIdDescriptor(object):
|
|||
if val is not None:
|
||||
return val
|
||||
return None
|
||||
|
||||
|
||||
class LanguageCacheSingleObjectDescriptor(object):
|
||||
"""
|
||||
A Mixin for RelatedObjectDescriptors which use current language in cache lookups.
|
||||
"""
|
||||
accessor = None # needs to be set on instance
|
||||
|
||||
@property
|
||||
def cache_name(self):
|
||||
lang = get_language()
|
||||
cache = build_localized_fieldname(self.accessor, lang)
|
||||
return "_%s_cache" % cache
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ from modeltranslation.utils import (build_css_class, build_localized_fieldname,
|
|||
request = None
|
||||
|
||||
# How many models are registered for tests.
|
||||
TEST_MODELS = 26
|
||||
TEST_MODELS = 27
|
||||
|
||||
|
||||
class reload_override_settings(override_settings):
|
||||
|
|
@ -673,16 +673,22 @@ class FileFieldsTest(ModeltranslationTestBase):
|
|||
|
||||
|
||||
class ForeignKeyFieldsTest(ModeltranslationTestBase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
# 'model' attribute cannot be assigned to class in its definition,
|
||||
# because ``models`` module will be reloaded and hence class would use old model classes.
|
||||
super(ForeignKeyFieldsTest, cls).setUpClass()
|
||||
cls.model = models.ForeignKeyModel
|
||||
|
||||
def test_translated_models(self):
|
||||
field_names = dir(models.ForeignKeyModel())
|
||||
field_names = dir(self.model())
|
||||
self.assertTrue('id' in field_names)
|
||||
for f in ('test', 'test_de', 'test_en', 'optional', 'optional_en', 'optional_de'):
|
||||
self.assertTrue(f in field_names)
|
||||
self.assertTrue('%s_id' % f in field_names)
|
||||
|
||||
def test_db_column_names(self):
|
||||
meta = models.ForeignKeyModel._meta
|
||||
meta = self.model._meta
|
||||
|
||||
# Make sure the correct database columns always get used:
|
||||
attname, col = meta.get_field('test').get_attname_column()
|
||||
|
|
@ -702,7 +708,7 @@ class ForeignKeyFieldsTest(ModeltranslationTestBase):
|
|||
test_inst1.save()
|
||||
test_inst2 = models.TestModel(title_en='title2_en', title_de='title2_de')
|
||||
test_inst2.save()
|
||||
inst = models.ForeignKeyModel()
|
||||
inst = self.model()
|
||||
|
||||
trans_real.activate("de")
|
||||
inst.test = test_inst1
|
||||
|
|
@ -736,7 +742,7 @@ class ForeignKeyFieldsTest(ModeltranslationTestBase):
|
|||
# Check filtering in direct way + lookup spanning
|
||||
inst.test_en = test_inst2
|
||||
inst.save()
|
||||
manager = models.ForeignKeyModel.objects
|
||||
manager = self.model.objects
|
||||
|
||||
trans_real.activate("de")
|
||||
self.assertEqual(manager.filter(test=test_inst1).count(), 1)
|
||||
|
|
@ -764,18 +770,18 @@ class ForeignKeyFieldsTest(ModeltranslationTestBase):
|
|||
test_inst.save()
|
||||
|
||||
# Instantiate many 'ForeignKeyModel' instances:
|
||||
fk_inst_both = models.ForeignKeyModel(title_en='f_title_en', title_de='f_title_de',
|
||||
test_de=test_inst, test_en=test_inst)
|
||||
fk_inst_both = self.model(title_en='f_title_en', title_de='f_title_de',
|
||||
test_de=test_inst, test_en=test_inst)
|
||||
fk_inst_both.save()
|
||||
fk_inst_de = models.ForeignKeyModel(title_en='f_title_en', title_de='f_title_de',
|
||||
test_de_id=test_inst.pk)
|
||||
fk_inst_de = self.model(title_en='f_title_en', title_de='f_title_de',
|
||||
test_de_id=test_inst.pk)
|
||||
fk_inst_de.save()
|
||||
fk_inst_en = models.ForeignKeyModel(title_en='f_title_en', title_de='f_title_de',
|
||||
test_en=test_inst)
|
||||
fk_inst_en = self.model(title_en='f_title_en', title_de='f_title_de',
|
||||
test_en=test_inst)
|
||||
fk_inst_en.save()
|
||||
|
||||
fk_option_de = models.ForeignKeyModel.objects.create(optional_de=test_inst)
|
||||
fk_option_en = models.ForeignKeyModel.objects.create(optional_en=test_inst)
|
||||
fk_option_de = self.model.objects.create(optional_de=test_inst)
|
||||
fk_option_en = self.model.objects.create(optional_en=test_inst)
|
||||
|
||||
# Check that the reverse accessors are created on the model:
|
||||
# Explicit related_name
|
||||
|
|
@ -867,13 +873,13 @@ class ForeignKeyFieldsTest(ModeltranslationTestBase):
|
|||
non_de = models.NonTranslated.objects.create(title='title_de')
|
||||
non_en = models.NonTranslated.objects.create(title='title_en')
|
||||
|
||||
fk_inst_both = models.ForeignKeyModel.objects.create(
|
||||
fk_inst_both = self.model.objects.create(
|
||||
title_en='f_title_en', title_de='f_title_de', non_de=non_de, non_en=non_en)
|
||||
fk_inst_de = models.ForeignKeyModel.objects.create(non_de=non_de)
|
||||
fk_inst_en = models.ForeignKeyModel.objects.create(non_en=non_en)
|
||||
fk_inst_de = self.model.objects.create(non_de=non_de)
|
||||
fk_inst_en = self.model.objects.create(non_en=non_en)
|
||||
|
||||
# Forward relation + spanning
|
||||
manager = models.ForeignKeyModel.objects
|
||||
manager = self.model.objects
|
||||
trans_real.activate("de")
|
||||
self.assertEqual(manager.filter(non=non_de).count(), 2)
|
||||
self.assertEqual(manager.filter(non=non_en).count(), 0)
|
||||
|
|
@ -913,6 +919,167 @@ class ForeignKeyFieldsTest(ModeltranslationTestBase):
|
|||
return self.assertEqual(sorted(qs1, key=pk), sorted(qs2, key=pk))
|
||||
|
||||
|
||||
class OneToOneFieldsTest(ForeignKeyFieldsTest):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
# 'model' attribute cannot be assigned to class in its definition,
|
||||
# because ``models`` module will be reloaded and hence class would use old model classes.
|
||||
super(OneToOneFieldsTest, cls).setUpClass()
|
||||
cls.model = models.OneToOneFieldModel
|
||||
|
||||
def test_uniqueness(self):
|
||||
test_inst1 = models.TestModel(title_en='title1_en', title_de='title1_de')
|
||||
test_inst1.save()
|
||||
inst = self.model()
|
||||
|
||||
trans_real.activate("de")
|
||||
inst.test = test_inst1
|
||||
|
||||
trans_real.activate("en")
|
||||
# That's ok, since test_en is different than test_de
|
||||
inst.test = test_inst1
|
||||
inst.save()
|
||||
|
||||
# But this violates uniqueness constraint
|
||||
inst2 = self.model(test=test_inst1)
|
||||
self.assertRaises(IntegrityError, inst2.save)
|
||||
|
||||
def test_reverse_relations(self):
|
||||
test_inst = models.TestModel(title_en='title_en', title_de='title_de')
|
||||
test_inst.save()
|
||||
|
||||
# Instantiate many 'OneToOneFieldModel' instances:
|
||||
fk_inst_de = self.model(title_en='f_title_en', title_de='f_title_de',
|
||||
test_de_id=test_inst.pk)
|
||||
fk_inst_de.save()
|
||||
fk_inst_en = self.model(title_en='f_title_en', title_de='f_title_de',
|
||||
test_en=test_inst)
|
||||
fk_inst_en.save()
|
||||
|
||||
fk_option_de = self.model.objects.create(optional_de=test_inst)
|
||||
fk_option_en = self.model.objects.create(optional_en=test_inst)
|
||||
|
||||
# Check that the reverse accessors are created on the model:
|
||||
# Explicit related_name
|
||||
testmodel_fields = models.TestModel._meta.get_all_field_names()
|
||||
testmodel_methods = dir(models.TestModel)
|
||||
self.assertIn('test_o2o', testmodel_fields)
|
||||
self.assertIn('test_o2o_de', testmodel_fields)
|
||||
self.assertIn('test_o2o_en', testmodel_fields)
|
||||
self.assertIn('test_o2o', testmodel_methods)
|
||||
self.assertIn('test_o2o_de', testmodel_methods)
|
||||
self.assertIn('test_o2o_en', testmodel_methods)
|
||||
# Implicit related_name
|
||||
self.assertIn('onetoonefieldmodel', testmodel_fields)
|
||||
self.assertIn('onetoonefieldmodel_de', testmodel_fields)
|
||||
self.assertIn('onetoonefieldmodel_en', testmodel_fields)
|
||||
self.assertIn('onetoonefieldmodel', testmodel_methods)
|
||||
self.assertIn('onetoonefieldmodel_de', testmodel_methods)
|
||||
self.assertIn('onetoonefieldmodel_en', testmodel_methods)
|
||||
|
||||
# Check the German reverse accessor:
|
||||
self.assertEqual(fk_inst_de, test_inst.test_o2o_de)
|
||||
|
||||
# Check the English reverse accessor:
|
||||
self.assertEqual(fk_inst_en, test_inst.test_o2o_en)
|
||||
|
||||
# Check the default reverse accessor:
|
||||
trans_real.activate("de")
|
||||
self.assertEqual(fk_inst_de, test_inst.test_o2o)
|
||||
trans_real.activate("en")
|
||||
self.assertEqual(fk_inst_en, test_inst.test_o2o)
|
||||
|
||||
# Check implicit related_name reverse accessor:
|
||||
self.assertEqual(fk_option_en, test_inst.onetoonefieldmodel)
|
||||
|
||||
# Check filtering in reverse way + lookup spanning:
|
||||
manager = models.TestModel.objects
|
||||
trans_real.activate("de")
|
||||
self.assertEqual(manager.filter(test_o2o=fk_inst_de).count(), 1)
|
||||
self.assertEqual(manager.filter(test_o2o__id=fk_inst_de.pk).count(), 1)
|
||||
self.assertEqual(manager.filter(test_o2o=fk_inst_en).count(), 0)
|
||||
self.assertEqual(manager.filter(test_o2o_en=fk_inst_en).count(), 1)
|
||||
self.assertEqual(manager.filter(onetoonefieldmodel=fk_option_de).count(), 1)
|
||||
self.assertEqual(manager.filter(onetoonefieldmodel=fk_option_en).count(), 0)
|
||||
self.assertEqual(manager.filter(onetoonefieldmodel_en=fk_option_en).count(), 1)
|
||||
self.assertEqual(manager.filter(test_o2o__title='f_title_de').distinct().count(), 1)
|
||||
self.assertEqual(manager.filter(test_o2o__title='f_title_en').distinct().count(), 0)
|
||||
self.assertEqual(manager.filter(test_o2o__title_en='f_title_en').distinct().count(), 1)
|
||||
trans_real.activate("en")
|
||||
self.assertEqual(manager.filter(test_o2o=fk_inst_en).count(), 1)
|
||||
self.assertEqual(manager.filter(test_o2o__id=fk_inst_en.pk).count(), 1)
|
||||
self.assertEqual(manager.filter(test_o2o=fk_inst_de).count(), 0)
|
||||
self.assertEqual(manager.filter(test_o2o_de=fk_inst_de).count(), 1)
|
||||
self.assertEqual(manager.filter(onetoonefieldmodel=fk_option_en).count(), 1)
|
||||
self.assertEqual(manager.filter(onetoonefieldmodel=fk_option_de).count(), 0)
|
||||
self.assertEqual(manager.filter(onetoonefieldmodel_de=fk_option_de).count(), 1)
|
||||
self.assertEqual(manager.filter(test_o2o__title='f_title_en').distinct().count(), 1)
|
||||
self.assertEqual(manager.filter(test_o2o__title='f_title_de').distinct().count(), 0)
|
||||
self.assertEqual(manager.filter(test_o2o__title_de='f_title_de').distinct().count(), 1)
|
||||
|
||||
# Check assignment
|
||||
trans_real.activate("de")
|
||||
test_inst2 = models.TestModel(title_en='title_en', title_de='title_de')
|
||||
test_inst2.save()
|
||||
test_inst2.test_o2o = fk_inst_de
|
||||
test_inst2.test_o2o_en = fk_inst_en
|
||||
|
||||
self.assertEqual(fk_inst_de.test.pk, test_inst2.pk)
|
||||
self.assertEqual(fk_inst_de.test_id, test_inst2.pk)
|
||||
self.assertEqual(fk_inst_de.test_de, test_inst2)
|
||||
self.assertEqual(test_inst2.test_o2o_de, test_inst2.test_o2o)
|
||||
self.assertEqual(fk_inst_de, test_inst2.test_o2o)
|
||||
trans_real.activate("en")
|
||||
self.assertEqual(fk_inst_en.test.pk, test_inst2.pk)
|
||||
self.assertEqual(fk_inst_en.test_id, test_inst2.pk)
|
||||
self.assertEqual(fk_inst_en.test_en, test_inst2)
|
||||
self.assertEqual(test_inst2.test_o2o_en, test_inst2.test_o2o)
|
||||
self.assertEqual(fk_inst_en, test_inst2.test_o2o)
|
||||
|
||||
def test_non_translated_relation(self):
|
||||
non_de = models.NonTranslated.objects.create(title='title_de')
|
||||
non_en = models.NonTranslated.objects.create(title='title_en')
|
||||
|
||||
fk_inst_de = self.model.objects.create(
|
||||
title_en='f_title_en', title_de='f_title_de', non_de=non_de)
|
||||
fk_inst_en = self.model.objects.create(
|
||||
title_en='f_title_en2', title_de='f_title_de2', non_en=non_en)
|
||||
|
||||
# Forward relation + spanning
|
||||
manager = self.model.objects
|
||||
trans_real.activate("de")
|
||||
self.assertEqual(manager.filter(non=non_de).count(), 1)
|
||||
self.assertEqual(manager.filter(non=non_en).count(), 0)
|
||||
self.assertEqual(manager.filter(non_en=non_en).count(), 1)
|
||||
self.assertEqual(manager.filter(non__title='title_de').count(), 1)
|
||||
self.assertEqual(manager.filter(non__title='title_en').count(), 0)
|
||||
self.assertEqual(manager.filter(non_en__title='title_en').count(), 1)
|
||||
trans_real.activate("en")
|
||||
self.assertEqual(manager.filter(non=non_en).count(), 1)
|
||||
self.assertEqual(manager.filter(non=non_de).count(), 0)
|
||||
self.assertEqual(manager.filter(non_de=non_de).count(), 1)
|
||||
self.assertEqual(manager.filter(non__title='title_en').count(), 1)
|
||||
self.assertEqual(manager.filter(non__title='title_de').count(), 0)
|
||||
self.assertEqual(manager.filter(non_de__title='title_de').count(), 1)
|
||||
|
||||
# Reverse relation + spanning
|
||||
manager = models.NonTranslated.objects
|
||||
trans_real.activate("de")
|
||||
self.assertEqual(manager.filter(test_o2o=fk_inst_de).count(), 1)
|
||||
self.assertEqual(manager.filter(test_o2o=fk_inst_en).count(), 0)
|
||||
self.assertEqual(manager.filter(test_o2o_en=fk_inst_en).count(), 1)
|
||||
self.assertEqual(manager.filter(test_o2o__title='f_title_de').count(), 1)
|
||||
self.assertEqual(manager.filter(test_o2o__title='f_title_en').count(), 0)
|
||||
self.assertEqual(manager.filter(test_o2o__title_en='f_title_en').count(), 1)
|
||||
trans_real.activate("en")
|
||||
self.assertEqual(manager.filter(test_o2o=fk_inst_en).count(), 1)
|
||||
self.assertEqual(manager.filter(test_o2o=fk_inst_de).count(), 0)
|
||||
self.assertEqual(manager.filter(test_o2o_de=fk_inst_de).count(), 1)
|
||||
self.assertEqual(manager.filter(test_o2o__title='f_title_en2').count(), 1)
|
||||
self.assertEqual(manager.filter(test_o2o__title='f_title_de2').count(), 0)
|
||||
self.assertEqual(manager.filter(test_o2o__title_de='f_title_de2').count(), 1)
|
||||
|
||||
|
||||
class OtherFieldsTest(ModeltranslationTestBase):
|
||||
def test_translated_models(self):
|
||||
inst = models.OtherFieldsModel.objects.create()
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ class FileFieldsModel(models.Model):
|
|||
image = models.ImageField(upload_to='modeltranslation_tests', null=True, blank=True)
|
||||
|
||||
|
||||
########## Foreign Key fields testing
|
||||
########## Foreign Key / OneToOneField testing
|
||||
|
||||
class NonTranslated(models.Model):
|
||||
title = models.CharField(ugettext_lazy('title'), max_length=255)
|
||||
|
|
@ -69,6 +69,14 @@ class ForeignKeyModel(models.Model):
|
|||
non = models.ForeignKey(NonTranslated, blank=True, null=True, related_name="test_fks")
|
||||
|
||||
|
||||
class OneToOneFieldModel(models.Model):
|
||||
title = models.CharField(ugettext_lazy('title'), max_length=255)
|
||||
test = models.OneToOneField(TestModel, null=True, related_name="test_o2o")
|
||||
optional = models.OneToOneField(TestModel, blank=True, null=True)
|
||||
# No hidden option for OneToOne
|
||||
non = models.OneToOneField(NonTranslated, blank=True, null=True, related_name="test_o2o")
|
||||
|
||||
|
||||
########## Custom fields testing
|
||||
|
||||
class OtherFieldsModel(models.Model):
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from modeltranslation.tests.models import (
|
|||
DescriptorModel, AbstractModelA, AbstractModelB, Slugged, MetaData, Displayable, Page,
|
||||
RichText, RichTextPage, MultitableModelA, MultitableModelB, MultitableModelC, ManagerTestModel,
|
||||
CustomManagerTestModel, CustomManager2TestModel, GroupFieldsetsModel, NameModel,
|
||||
ThirdPartyRegisteredModel, ProxyTestModel, UniqueNullableModel)
|
||||
ThirdPartyRegisteredModel, ProxyTestModel, UniqueNullableModel, OneToOneFieldModel)
|
||||
|
||||
|
||||
class TestTranslationOptions(TranslationOptions):
|
||||
|
|
@ -49,13 +49,18 @@ class FileFieldsModelTranslationOptions(TranslationOptions):
|
|||
translator.register(FileFieldsModel, FileFieldsModelTranslationOptions)
|
||||
|
||||
|
||||
########## Foreign Key fields testing
|
||||
########## Foreign Key / OneToOneField testing
|
||||
|
||||
class ForeignKeyModelTranslationOptions(TranslationOptions):
|
||||
fields = ('title', 'test', 'optional', 'hidden', 'non',)
|
||||
translator.register(ForeignKeyModel, ForeignKeyModelTranslationOptions)
|
||||
|
||||
|
||||
class OneToOneFieldModelTranslationOptions(TranslationOptions):
|
||||
fields = ('title', 'test', 'optional', 'non',)
|
||||
translator.register(OneToOneFieldModel, OneToOneFieldModelTranslationOptions)
|
||||
|
||||
|
||||
########## Custom fields testing
|
||||
|
||||
class OtherFieldsModelTranslationOptions(TranslationOptions):
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from django.conf import settings
|
||||
from django.utils.six import with_metaclass
|
||||
from django.db.models import Manager, ForeignKey
|
||||
from django.db.models import Manager, ForeignKey, OneToOneField
|
||||
from django.db.models.base import ModelBase
|
||||
from django.db.models.signals import post_init
|
||||
from django.dispatch import receiver
|
||||
|
||||
from modeltranslation import settings as mt_settings
|
||||
from modeltranslation.fields import (NONE, create_translation_field, TranslationFieldDescriptor,
|
||||
TranslatedRelationIdDescriptor)
|
||||
TranslatedRelationIdDescriptor,
|
||||
LanguageCacheSingleObjectDescriptor)
|
||||
from modeltranslation.manager import MultilingualManager, rewrite_lookup_key
|
||||
from modeltranslation.utils import build_localized_fieldname
|
||||
|
||||
|
|
@ -274,6 +275,17 @@ def populate_translation_fields(sender, kwargs):
|
|||
raise AttributeError("Unknown population mode '%s'." % populate)
|
||||
|
||||
|
||||
def patch_related_object_descriptor_caching(ro_descriptor):
|
||||
"""
|
||||
Patch SingleRelatedObjectDescriptor or ReverseSingleRelatedObjectDescriptor to use
|
||||
language-aware caching.
|
||||
"""
|
||||
class NewSingleObjectDescriptor(LanguageCacheSingleObjectDescriptor, ro_descriptor.__class__):
|
||||
pass
|
||||
ro_descriptor.accessor = ro_descriptor.related.get_accessor_name()
|
||||
ro_descriptor.__class__ = NewSingleObjectDescriptor
|
||||
|
||||
|
||||
class Translator(object):
|
||||
"""
|
||||
A Translator object encapsulates an instance of a translator. Models are
|
||||
|
|
@ -372,6 +384,11 @@ class Translator(object):
|
|||
other_opts.related_fields.append(field.related_query_name())
|
||||
add_manager(field.rel.to) # Add manager in case of non-registered model
|
||||
|
||||
if isinstance(field, OneToOneField):
|
||||
# Fix translated_field caching for SingleRelatedObjectDescriptor
|
||||
sro_descriptor = getattr(field.rel.to, field.related.get_accessor_name())
|
||||
patch_related_object_descriptor_caching(sro_descriptor)
|
||||
|
||||
def unregister(self, model_or_iterable):
|
||||
"""
|
||||
Unregisters the given model(s).
|
||||
|
|
|
|||
Loading…
Reference in a new issue