Add OneToOneField support.

This commit is contained in:
Jacek Tomaszewski 2013-10-12 13:58:57 +02:00
parent 81c9d55c1e
commit 0e76f1ec7d
7 changed files with 235 additions and 24 deletions

View file

@ -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.

View file

@ -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|
=============================== === === ===

View file

@ -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

View file

@ -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()

View file

@ -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):

View file

@ -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):

View file

@ -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).