From f97695ac5a0abaa04189a8d4e57e2a1fa6486a32 Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Sun, 19 Jan 2014 22:13:43 +0100 Subject: [PATCH 001/170] Connect signal only for registered model, not all. --- modeltranslation/translator.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modeltranslation/translator.py b/modeltranslation/translator.py index 58f018e4..61c0d7fe 100644 --- a/modeltranslation/translator.py +++ b/modeltranslation/translator.py @@ -168,7 +168,6 @@ def new_init(self, *args, **kwargs): model.__init__ = new_init -@receiver(post_init) def delete_mt_init(sender, instance, **kwargs): if hasattr(instance, '_mt_init'): del instance._mt_init @@ -353,6 +352,9 @@ def register(self, model_or_iterable, opts_class=None, **options): # Patch __init__ to rewrite fields patch_constructor(model) + # Connect signal for model + post_init.connect(delete_mt_init, sender=model) + # Patch clean_fields to verify form field clearing patch_clean_fields(model) From fb98c370ec6da81862f53f597b1ffd60467624bd Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Sun, 19 Jan 2014 22:27:33 +0100 Subject: [PATCH 002/170] Make testrunner compatible with Django 1.6. --- modeltranslation/tests/__init__.py | 2713 +--------------------------- modeltranslation/tests/tests.py | 2711 +++++++++++++++++++++++++++ runtests.py | 3 +- 3 files changed, 2714 insertions(+), 2713 deletions(-) create mode 100644 modeltranslation/tests/tests.py diff --git a/modeltranslation/tests/__init__.py b/modeltranslation/tests/__init__.py index ef59e845..a49dea43 100644 --- a/modeltranslation/tests/__init__.py +++ b/modeltranslation/tests/__init__.py @@ -1,2711 +1,2 @@ -# -*- coding: utf-8 -*- -import datetime -from decimal import Decimal -import os -import shutil -import imp - -from django import forms -from django.conf import settings as django_settings -from django.contrib.admin.sites import AdminSite -from django.contrib.auth.models import User -from django.core.exceptions import ValidationError, ImproperlyConfigured -from django.core.files.base import ContentFile -from django.core.files.storage import default_storage -from django.core.management import call_command -from django.db import IntegrityError -from django.db.models import Q, F -from django.db.models.loading import AppCache -from django.test import TestCase, TransactionTestCase -from django.test.utils import override_settings -from django.utils import six -from django.utils.translation import get_language, override, trans_real - -from modeltranslation import admin, settings as mt_settings, translator -from modeltranslation.forms import TranslationModelForm -from modeltranslation.models import autodiscover -from modeltranslation.tests import models -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, - auto_populate, fallbacks) - - -# None of the following tests really depend on the content of the request, -# so we'll just pass in None. -request = None - -# How many models are registered for tests. -TEST_MODELS = 27 - - -class reload_override_settings(override_settings): - """Context manager that not only override settings, but also reload modeltranslation conf.""" - def __enter__(self): - super(reload_override_settings, self).__enter__() - imp.reload(mt_settings) - - def __exit__(self, exc_type, exc_value, traceback): - super(reload_override_settings, self).__exit__(exc_type, exc_value, traceback) - imp.reload(mt_settings) - - -# In this test suite fallback language is turned off. This context manager temporarily turns it on. -def default_fallback(): - return reload_override_settings( - MODELTRANSLATION_FALLBACK_LANGUAGES=(mt_settings.DEFAULT_LANGUAGE,)) - - -@override_settings(**TEST_SETTINGS) -class ModeltranslationTransactionTestBase(TransactionTestCase): - urls = 'modeltranslation.tests.urls' - cache = AppCache() - synced = False - - @classmethod - def setUpClass(cls): - """ - Prepare database: - * Call syncdb to create tables for tests.models (since during - default testrunner's db creation modeltranslation.tests was not in INSTALLED_APPS - """ - super(ModeltranslationTransactionTestBase, cls).setUpClass() - if not ModeltranslationTestBase.synced: - # In order to perform only one syncdb - ModeltranslationTestBase.synced = True - with override_settings(**TEST_SETTINGS): - import sys - - # 1. Reload translation in case USE_I18N was False - from django.utils import translation - imp.reload(translation) - - # 2. Reload MT because LANGUAGES likely changed. - imp.reload(mt_settings) - imp.reload(translator) - imp.reload(admin) - - # 3. Reset test models (because autodiscover have already run, those models - # have translation fields, but for languages previously defined. We want - # to be sure that 'de' and 'en' are available) - del cls.cache.app_models['tests'] - imp.reload(models) - cls.cache.load_app('modeltranslation.tests') - sys.modules.pop('modeltranslation.tests.translation', None) - - # 4. Autodiscover - from modeltranslation import models as aut_models - imp.reload(aut_models) - - # 5. Syncdb (``migrate=False`` in case of south) - from django.db import connections, DEFAULT_DB_ALIAS - call_command('syncdb', verbosity=0, migrate=False, interactive=False, - database=connections[DEFAULT_DB_ALIAS].alias, load_initial_data=False) - - def setUp(self): - self._old_language = get_language() - trans_real.activate('de') - - def tearDown(self): - trans_real.activate(self._old_language) - - -class ModeltranslationTestBase(ModeltranslationTransactionTestBase, TestCase): - pass - - -class TestAutodiscover(ModeltranslationTestBase): - # The way the ``override_settings`` works on ``TestCase`` is wicked; - # it patches ``_pre_setup`` and ``_post_teardown`` methods. - # Because of this, if class B extends class A and both are ``override_settings``'ed, - # class B settings would be overwritten by class A settings (if some keys clash). - # To solve this, override some settings after parents ``_pre_setup`` is called. - def _pre_setup(self): - super(TestAutodiscover, self)._pre_setup() - # Add test_app to INSTALLED_APPS - new_installed_apps = django_settings.INSTALLED_APPS + ('modeltranslation.tests.test_app',) - self.__override = override_settings(INSTALLED_APPS=new_installed_apps) - self.__override.enable() - - def _post_teardown(self): - self.__override.disable() - imp.reload(mt_settings) # restore mt_settings.FALLBACK_LANGUAGES - super(TestAutodiscover, self)._post_teardown() - - @classmethod - def setUpClass(cls): - """Save registry (and restore it after tests).""" - super(TestAutodiscover, cls).setUpClass() - from copy import copy - from modeltranslation.translator import translator - cls.registry_cpy = copy(translator._registry) - - @classmethod - def tearDownClass(cls): - from modeltranslation.translator import translator - translator._registry = cls.registry_cpy - super(TestAutodiscover, cls).tearDownClass() - - def tearDown(self): - import sys - # Rollback model classes - del self.cache.app_models['test_app'] - from .test_app import models - imp.reload(models) - # Delete translation modules from import cache - sys.modules.pop('modeltranslation.tests.test_app.translation', None) - sys.modules.pop('modeltranslation.tests.project_translation', None) - super(TestAutodiscover, self).tearDown() - - def check_news(self): - from .test_app.models import News - fields = dir(News()) - self.assertIn('title', fields) - self.assertIn('title_en', fields) - self.assertIn('title_de', fields) - self.assertIn('visits', fields) - self.assertNotIn('visits_en', fields) - self.assertNotIn('visits_de', fields) - - def check_other(self, present=True): - from .test_app.models import Other - fields = dir(Other()) - self.assertIn('name', fields) - if present: - self.assertIn('name_en', fields) - self.assertIn('name_de', fields) - else: - self.assertNotIn('name_en', fields) - self.assertNotIn('name_de', fields) - - def test_simple(self): - """Check if translation is imported for installed apps.""" - autodiscover() - self.check_news() - self.check_other(present=False) - - @reload_override_settings( - MODELTRANSLATION_TRANSLATION_FILES=('modeltranslation.tests.project_translation',) - ) - def test_global(self): - """Check if translation is imported for global translation file.""" - autodiscover() - self.check_news() - self.check_other() - - @reload_override_settings( - MODELTRANSLATION_TRANSLATION_FILES=('modeltranslation.tests.test_app.translation',) - ) - def test_duplication(self): - """Check if there is no problem with duplicated filenames.""" - autodiscover() - self.check_news() - - -class ModeltranslationTest(ModeltranslationTestBase): - """Basic tests for the modeltranslation application.""" - def test_registration(self): - langs = tuple(l[0] for l in django_settings.LANGUAGES) - self.assertEqual(langs, tuple(mt_settings.AVAILABLE_LANGUAGES)) - self.assertEqual(2, len(langs)) - self.assertTrue('de' in langs) - self.assertTrue('en' in langs) - self.assertTrue(translator.translator) - - # Check that all models are registered for translation - self.assertEqual(len(translator.translator.get_registered_models()), TEST_MODELS) - - # Try to unregister a model that is not registered - self.assertRaises(translator.NotRegistered, - translator.translator.unregister, models.BasePage) - - # Try to get options for a model that is not registered - self.assertRaises(translator.NotRegistered, - translator.translator.get_options_for_model, User) - - # Ensure that a base can't be registered after a subclass. - self.assertRaises(translator.DescendantRegistered, - translator.translator.register, models.BasePage) - - # Or unregistered before it. - self.assertRaises(translator.DescendantRegistered, - translator.translator.unregister, models.Slugged) - - def test_fields(self): - field_names = dir(models.TestModel()) - self.assertTrue('id' in field_names) - self.assertTrue('title' in field_names) - self.assertTrue('title_de' in field_names) - self.assertTrue('title_en' in field_names) - self.assertTrue('text' in field_names) - self.assertTrue('text_de' in field_names) - self.assertTrue('text_en' in field_names) - self.assertTrue('url' in field_names) - self.assertTrue('url_de' in field_names) - self.assertTrue('url_en' in field_names) - self.assertTrue('email' in field_names) - self.assertTrue('email_de' in field_names) - self.assertTrue('email_en' in field_names) - - def test_verbose_name(self): - verbose_name = models.TestModel._meta.get_field('title_de').verbose_name - self.assertEqual(six.text_type(verbose_name), 'title [de]') - - def test_descriptor_introspection(self): - # See Django #8248 - try: - models.TestModel.title - models.TestModel.title.__doc__ - self.assertTrue(True) - except: - self.fail('Descriptor accessed on class should return itself.') - - def test_fields_hashes(self): - opts = models.TestModel._meta - orig = opts.get_field('title') - en = opts.get_field('title_en') - de = opts.get_field('title_de') - # Translation field retain creation_counters - self.assertEqual(orig.creation_counter, en.creation_counter) - self.assertEqual(orig.creation_counter, de.creation_counter) - # But they compare unequal - self.assertNotEqual(orig, en) - self.assertNotEqual(orig, de) - self.assertNotEqual(en, de) - # Their hashes too - self.assertNotEqual(hash(orig), hash(en)) - self.assertNotEqual(hash(orig), hash(de)) - self.assertNotEqual(hash(en), hash(de)) - self.assertEqual(3, len(set([orig, en, de]))) - # TranslationFields can compare equal if they have the same language - de.language = 'en' - self.assertNotEqual(orig, de) - self.assertEqual(en, de) - self.assertEqual(hash(en), hash(de)) - self.assertEqual(2, len(set([orig, en, de]))) - de.language = 'de' - - def test_set_translation(self): - """This test briefly shows main modeltranslation features.""" - self.assertEqual(get_language(), 'de') - title_de = "title de" - title_en = "title en" - - # The original field "title" passed in the constructor is - # populated for the current language field: "title_de". - inst2 = models.TestModel(title=title_de) - self.assertEqual(inst2.title, title_de) - self.assertEqual(inst2.title_en, None) - self.assertEqual(inst2.title_de, title_de) - - # So creating object is language-aware - with override('en'): - inst2 = models.TestModel(title=title_en) - self.assertEqual(inst2.title, title_en) - self.assertEqual(inst2.title_en, title_en) - self.assertEqual(inst2.title_de, None) - - # Value from original field is presented in current language: - inst2 = models.TestModel(title_de=title_de, title_en=title_en) - self.assertEqual(inst2.title, title_de) - with override('en'): - self.assertEqual(inst2.title, title_en) - - # Changes made via original field affect current language field: - inst2.title = 'foo' - self.assertEqual(inst2.title, 'foo') - self.assertEqual(inst2.title_en, title_en) - self.assertEqual(inst2.title_de, 'foo') - with override('en'): - inst2.title = 'bar' - self.assertEqual(inst2.title, 'bar') - self.assertEqual(inst2.title_en, 'bar') - self.assertEqual(inst2.title_de, 'foo') - self.assertEqual(inst2.title, 'foo') - - # When conflict, language field wins with original field - inst2 = models.TestModel(title='foo', title_de=title_de, title_en=title_en) - self.assertEqual(inst2.title, title_de) - self.assertEqual(inst2.title_en, title_en) - self.assertEqual(inst2.title_de, title_de) - - # Creating model and assigning only one language - inst1 = models.TestModel(title_en=title_en) - # Please note: '' and not None, because descriptor falls back to field default value - self.assertEqual(inst1.title, '') - self.assertEqual(inst1.title_en, title_en) - self.assertEqual(inst1.title_de, None) - # Assign current language value - de - inst1.title = title_de - self.assertEqual(inst1.title, title_de) - self.assertEqual(inst1.title_en, title_en) - self.assertEqual(inst1.title_de, title_de) - inst1.save() - - # Check that the translation fields are correctly saved and provide the - # correct value when retrieving them again. - n = models.TestModel.objects.get(title=title_de) - self.assertEqual(n.title, title_de) - self.assertEqual(n.title_en, title_en) - self.assertEqual(n.title_de, title_de) - - # Queries are also language-aware: - self.assertEqual(1, models.TestModel.objects.filter(title=title_de).count()) - with override('en'): - self.assertEqual(0, models.TestModel.objects.filter(title=title_de).count()) - - def test_fallback_language(self): - # Present what happens if current language field is empty - self.assertEqual(get_language(), 'de') - title_de = "title de" - - # Create model with value in de only... - inst2 = models.TestModel(title=title_de) - self.assertEqual(inst2.title, title_de) - self.assertEqual(inst2.title_en, None) - self.assertEqual(inst2.title_de, title_de) - - # In this test environment, fallback language is not set. So return value for en - # will be field's default: '' - with override('en'): - self.assertEqual(inst2.title, '') - self.assertEqual(inst2.title_en, None) # Language field access returns real value - - # However, by default FALLBACK_LANGUAGES is set to DEFAULT_LANGUAGE - with default_fallback(): - - # No change here... - self.assertEqual(inst2.title, title_de) - - # ... but for empty en fall back to de - with override('en'): - self.assertEqual(inst2.title, title_de) - self.assertEqual(inst2.title_en, None) # Still real value - - def test_fallback_values_1(self): - """ - If ``fallback_values`` is set to string, all untranslated fields would - return this string. - """ - title1_de = "title de" - n = models.FallbackModel(title=title1_de) - n.save() - n = models.FallbackModel.objects.get(title=title1_de) - self.assertEqual(n.title, title1_de) - trans_real.activate("en") - self.assertEqual(n.title, "fallback") - - def test_fallback_values_2(self): - """ - If ``fallback_values`` is set to ``dict``, all untranslated fields in - ``dict`` would return this mapped value. Fields not in ``dict`` would - return default translation. - """ - title1_de = "title de" - text1_de = "text in german" - n = models.FallbackModel2(title=title1_de, text=text1_de) - n.save() - n = models.FallbackModel2.objects.get(title=title1_de) - trans_real.activate("en") - self.assertEqual(n.title, '') # Falling back to default field value - self.assertEqual( - n.text, - FallbackModel2TranslationOptions.fallback_values['text']) - - def _compare_instances(self, x, y, field): - self.assertEqual(getattr(x, field), getattr(y, field), - "Constructor diff on field %s." % field) - - def _test_constructor(self, keywords): - n = models.TestModel(**keywords) - m = models.TestModel.objects.create(**keywords) - opts = translator.translator.get_options_for_model(models.TestModel) - for base_field, trans_fields in opts.fields.items(): - self._compare_instances(n, m, base_field) - for lang_field in trans_fields: - self._compare_instances(n, m, lang_field.name) - - def test_constructor(self): - """ - Ensure that model constructor behaves exactly the same as objects.create - """ - # test different arguments compositions - keywords = dict( - # original only - title='title', - # both languages + original - email='q@q.qq', email_de='d@d.dd', email_en='e@e.ee', - # both languages without original - text_en='text en', text_de='text de', - ) - self._test_constructor(keywords) - - keywords = dict( - # only current language - title_de='title', - # only not current language - url_en='http://www.google.com', - # original + current - text='text def', text_de='text de', - # original + not current - email='q@q.qq', email_en='e@e.ee', - ) - self._test_constructor(keywords) - - -class ModeltranslationTransactionTest(ModeltranslationTransactionTestBase): - def test_unique_nullable_field(self): - from django.db import transaction - models.UniqueNullableModel.objects.create() - models.UniqueNullableModel.objects.create() - models.UniqueNullableModel.objects.create(title=None) - models.UniqueNullableModel.objects.create(title=None) - - models.UniqueNullableModel.objects.create(title='') - self.assertRaises(IntegrityError, models.UniqueNullableModel.objects.create, title='') - transaction.rollback() # Postgres - models.UniqueNullableModel.objects.create(title='foo') - self.assertRaises(IntegrityError, models.UniqueNullableModel.objects.create, title='foo') - transaction.rollback() # Postgres - - -class FallbackTests(ModeltranslationTestBase): - test_fallback = { - 'default': ('de',), - 'de': ('en',) - } - - def test_settings(self): - # Initial - self.assertEqual(mt_settings.FALLBACK_LANGUAGES, {'default': ()}) - # Tuple/list - with reload_override_settings(MODELTRANSLATION_FALLBACK_LANGUAGES=('de',)): - self.assertEqual(mt_settings.FALLBACK_LANGUAGES, {'default': ('de',)}) - # Whole dict - with reload_override_settings(MODELTRANSLATION_FALLBACK_LANGUAGES=self.test_fallback): - self.assertEqual(mt_settings.FALLBACK_LANGUAGES, self.test_fallback) - # Improper language raises error - config = {'default': (), 'fr': ('en',)} - with override_settings(MODELTRANSLATION_FALLBACK_LANGUAGES=config): - self.assertRaises(ImproperlyConfigured, lambda: imp.reload(mt_settings)) - imp.reload(mt_settings) - - def test_resolution_order(self): - from modeltranslation.utils import resolution_order - with reload_override_settings(MODELTRANSLATION_FALLBACK_LANGUAGES=self.test_fallback): - self.assertEqual(('en', 'de'), resolution_order('en')) - self.assertEqual(('de', 'en'), resolution_order('de')) - # Overriding - config = {'default': ()} - self.assertEqual(('en',), resolution_order('en', config)) - self.assertEqual(('de', 'en'), resolution_order('de', config)) - # Uniqueness - config = {'de': ('en', 'de')} - self.assertEqual(('en', 'de'), resolution_order('en', config)) - self.assertEqual(('de', 'en'), resolution_order('de', config)) - - # Default fallbacks are always used at the end - # That's it: fallbacks specified for a language don't replace defaults, - # but just are prepended - config = {'default': ('en', 'de'), 'de': ()} - self.assertEqual(('en', 'de'), resolution_order('en', config)) - self.assertEqual(('de', 'en'), resolution_order('de', config)) - # What one may have expected - self.assertNotEqual(('de',), resolution_order('de', config)) - - # To completely override settings, one should override all keys - config = {'default': (), 'de': ()} - self.assertEqual(('en',), resolution_order('en', config)) - self.assertEqual(('de',), resolution_order('de', config)) - - def test_fallback_languages(self): - with reload_override_settings(MODELTRANSLATION_FALLBACK_LANGUAGES=self.test_fallback): - title_de = 'title de' - title_en = 'title en' - n = models.TestModel(title=title_de) - self.assertEqual(n.title_de, title_de) - self.assertEqual(n.title_en, None) - self.assertEqual(n.title, title_de) - trans_real.activate('en') - self.assertEqual(n.title, title_de) # since default fallback is de - - n = models.TestModel(title=title_en) - self.assertEqual(n.title_de, None) - self.assertEqual(n.title_en, title_en) - self.assertEqual(n.title, title_en) - trans_real.activate('de') - self.assertEqual(n.title, title_en) # since fallback for de is en - - n.title_en = None - self.assertEqual(n.title, '') # if all fallbacks fail, return field.get_default() - - def test_fallbacks_toggle(self): - with reload_override_settings(MODELTRANSLATION_FALLBACK_LANGUAGES=self.test_fallback): - m = models.TestModel(title='foo') - with fallbacks(True): - self.assertEqual(m.title_de, 'foo') - self.assertEqual(m.title_en, None) - self.assertEqual(m.title, 'foo') - with override('en'): - self.assertEqual(m.title, 'foo') - with fallbacks(False): - self.assertEqual(m.title_de, 'foo') - self.assertEqual(m.title_en, None) - self.assertEqual(m.title, 'foo') - with override('en'): - self.assertEqual(m.title, '') # '' is the default - - def test_fallback_undefined(self): - """ - Checks if a sensible value is considered undefined and triggers - fallbacks. Tests if the value can be overridden as documented. - """ - with reload_override_settings(MODELTRANSLATION_FALLBACK_LANGUAGES=self.test_fallback): - # Non-nullable CharField falls back on empty strings. - m = models.FallbackModel(title_en='value', title_de='') - with override('en'): - self.assertEqual(m.title, 'value') - with override('de'): - self.assertEqual(m.title, 'value') - - # Nullable CharField does not fall back on empty strings. - m = models.FallbackModel(description_en='value', description_de='') - with override('en'): - self.assertEqual(m.description, 'value') - with override('de'): - self.assertEqual(m.description, '') - - # Nullable CharField does fall back on None. - m = models.FallbackModel(description_en='value', description_de=None) - with override('en'): - self.assertEqual(m.description, 'value') - with override('de'): - self.assertEqual(m.description, 'value') - - # The undefined value may be overridden. - m = models.FallbackModel2(title_en='value', title_de='') - with override('en'): - self.assertEqual(m.title, 'value') - with override('de'): - self.assertEqual(m.title, '') - m = models.FallbackModel2(title_en='value', title_de='no title') - with override('en'): - self.assertEqual(m.title, 'value') - with override('de'): - self.assertEqual(m.title, 'value') - - -class FileFieldsTest(ModeltranslationTestBase): - - def tearDown(self): - if default_storage.exists('modeltranslation_tests'): - # With FileSystemStorage uploading files creates a new directory, - # that's not automatically removed upon their deletion. - tests_dir = default_storage.path('modeltranslation_tests') - if os.path.isdir(tests_dir): - shutil.rmtree(tests_dir) - super(FileFieldsTest, self).tearDown() - - def test_translated_models(self): - field_names = dir(models.FileFieldsModel()) - self.assertTrue('id' in field_names) - self.assertTrue('title' in field_names) - self.assertTrue('title_de' in field_names) - self.assertTrue('title_en' in field_names) - self.assertTrue('file' in field_names) - self.assertTrue('file_de' in field_names) - self.assertTrue('file_en' in field_names) - self.assertTrue('image' in field_names) - self.assertTrue('image_de' in field_names) - self.assertTrue('image_en' in field_names) - - def _file_factory(self, name, content): - try: - return ContentFile(content, name=name) - except TypeError: # In Django 1.3 ContentFile had no name parameter - file = ContentFile(content) - file.name = name - return file - - def test_translated_models_instance(self): - inst = models.FileFieldsModel(title="Testtitle") - - trans_real.activate("en") - inst.title = 'title_en' - inst.file = 'a_en' - inst.file.save('b_en', ContentFile('file in english')) - inst.image = self._file_factory('i_en.jpg', 'image in english') # Direct assign - - trans_real.activate("de") - inst.title = 'title_de' - inst.file = 'a_de' - inst.file.save('b_de', ContentFile('file in german')) - inst.image = self._file_factory('i_de.jpg', 'image in german') - - inst.save() - - trans_real.activate("en") - self.assertEqual(inst.title, 'title_en') - self.assertTrue(inst.file.name.count('b_en') > 0) - self.assertEqual(inst.file.read(), b'file in english') - self.assertTrue(inst.image.name.count('i_en') > 0) - self.assertEqual(inst.image.read(), b'image in english') - - # Check if file was actually created in the global storage. - self.assertTrue(default_storage.exists(inst.file)) - self.assertTrue(inst.file.size > 0) - self.assertTrue(default_storage.exists(inst.image)) - self.assertTrue(inst.image.size > 0) - - trans_real.activate("de") - self.assertEqual(inst.title, 'title_de') - self.assertTrue(inst.file.name.count('b_de') > 0) - self.assertEqual(inst.file.read(), b'file in german') - self.assertTrue(inst.image.name.count('i_de') > 0) - self.assertEqual(inst.image.read(), b'image in german') - - inst.file_en.delete() - inst.image_en.delete() - inst.file_de.delete() - inst.image_de.delete() - - def test_empty_field(self): - from django.db.models.fields.files import FieldFile - inst = models.FileFieldsModel() - self.assertIsInstance(inst.file, FieldFile) - self.assertIsInstance(inst.file2, FieldFile) - inst.save() - inst = models.FileFieldsModel.objects.all()[0] - self.assertIsInstance(inst.file, FieldFile) - self.assertIsInstance(inst.file2, FieldFile) - - def test_fallback(self): - from django.db.models.fields.files import FieldFile - with reload_override_settings(MODELTRANSLATION_FALLBACK_LANGUAGES=('en',)): - self.assertEqual(get_language(), 'de') - inst = models.FileFieldsModel() - inst.file_de = '' - inst.file_en = 'foo' - inst.file2_de = '' - inst.file2_en = 'bar' - self.assertIsInstance(inst.file, FieldFile) - self.assertIsInstance(inst.file2, FieldFile) - self.assertEqual(inst.file.name, 'foo') - self.assertEqual(inst.file2.name, 'bar') - inst.save() - inst = models.FileFieldsModel.objects.all()[0] - self.assertIsInstance(inst.file, FieldFile) - self.assertIsInstance(inst.file2, FieldFile) - self.assertEqual(inst.file.name, 'foo') - self.assertEqual(inst.file2.name, 'bar') - - -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(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 = self.model._meta - - # Make sure the correct database columns always get used: - attname, col = meta.get_field('test').get_attname_column() - self.assertEqual(attname, 'test_id') - self.assertEqual(attname, col) - - attname, col = meta.get_field('test_en').get_attname_column() - self.assertEqual(attname, 'test_en_id') - self.assertEqual(attname, col) - - attname, col = meta.get_field('test_de').get_attname_column() - self.assertEqual(attname, 'test_de_id') - self.assertEqual(attname, col) - - def test_translated_models_instance(self): - test_inst1 = models.TestModel(title_en='title1_en', title_de='title1_de') - test_inst1.save() - test_inst2 = models.TestModel(title_en='title2_en', title_de='title2_de') - test_inst2.save() - inst = self.model() - - trans_real.activate("de") - inst.test = test_inst1 - inst.optional = None - - trans_real.activate("en") - # Test assigning relation by ID: - inst.optional_id = test_inst2.pk - inst.save() - - trans_real.activate("de") - self.assertEqual(inst.test_id, test_inst1.pk) - self.assertEqual(inst.test.title, 'title1_de') - self.assertEqual(inst.test_de_id, test_inst1.pk) - self.assertEqual(inst.test_de.title, 'title1_de') - self.assertEqual(inst.optional, None) - - # Test fallbacks: - trans_real.activate("en") - with default_fallback(): - self.assertEqual(inst.test_id, test_inst1.pk) - self.assertEqual(inst.test.pk, test_inst1.pk) - self.assertEqual(inst.test.title, 'title1_en') - - # Test English: - self.assertEqual(inst.optional_id, test_inst2.pk) - self.assertEqual(inst.optional.title, 'title2_en') - self.assertEqual(inst.optional_en_id, test_inst2.pk) - self.assertEqual(inst.optional_en.title, 'title2_en') - - # Test caching - inst.test_en = test_inst2 - inst.save() - trans_real.activate("de") - self.assertEqual(inst.test, test_inst1) - trans_real.activate("en") - self.assertEqual(inst.test, test_inst2) - - # Check filtering in direct way + lookup spanning - manager = self.model.objects - trans_real.activate("de") - self.assertEqual(manager.filter(test=test_inst1).count(), 1) - self.assertEqual(manager.filter(test_en=test_inst1).count(), 0) - self.assertEqual(manager.filter(test_de=test_inst1).count(), 1) - self.assertEqual(manager.filter(test=test_inst2).count(), 0) - self.assertEqual(manager.filter(test_en=test_inst2).count(), 1) - self.assertEqual(manager.filter(test_de=test_inst2).count(), 0) - self.assertEqual(manager.filter(test__title='title1_de').count(), 1) - self.assertEqual(manager.filter(test__title='title1_en').count(), 0) - self.assertEqual(manager.filter(test__title_en='title1_en').count(), 1) - trans_real.activate("en") - self.assertEqual(manager.filter(test=test_inst1).count(), 0) - self.assertEqual(manager.filter(test_en=test_inst1).count(), 0) - self.assertEqual(manager.filter(test_de=test_inst1).count(), 1) - self.assertEqual(manager.filter(test=test_inst2).count(), 1) - self.assertEqual(manager.filter(test_en=test_inst2).count(), 1) - self.assertEqual(manager.filter(test_de=test_inst2).count(), 0) - self.assertEqual(manager.filter(test__title='title2_en').count(), 1) - self.assertEqual(manager.filter(test__title='title2_de').count(), 0) - self.assertEqual(manager.filter(test__title_de='title2_de').count(), 1) - - def test_reverse_relations(self): - test_inst = models.TestModel(title_en='title_en', title_de='title_de') - test_inst.save() - - # Instantiate many 'ForeignKeyModel' instances: - 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 = 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_fks', testmodel_fields) - self.assertIn('test_fks_de', testmodel_fields) - self.assertIn('test_fks_en', testmodel_fields) - self.assertIn('test_fks', testmodel_methods) - self.assertIn('test_fks_de', testmodel_methods) - self.assertIn('test_fks_en', testmodel_methods) - # Implicit related_name: manager descriptor name != query field name - self.assertIn('foreignkeymodel', testmodel_fields) - self.assertIn('foreignkeymodel_de', testmodel_fields) - self.assertIn('foreignkeymodel_en', testmodel_fields) - self.assertIn('foreignkeymodel_set', testmodel_methods) - self.assertIn('foreignkeymodel_set_de', testmodel_methods) - self.assertIn('foreignkeymodel_set_en', testmodel_methods) - - # Check the German reverse accessor: - self.assertIn(fk_inst_both, test_inst.test_fks_de.all()) - self.assertIn(fk_inst_de, test_inst.test_fks_de.all()) - self.assertNotIn(fk_inst_en, test_inst.test_fks_de.all()) - - # Check the English reverse accessor: - self.assertIn(fk_inst_both, test_inst.test_fks_en.all()) - self.assertIn(fk_inst_en, test_inst.test_fks_en.all()) - self.assertNotIn(fk_inst_de, test_inst.test_fks_en.all()) - - # Check the default reverse accessor: - trans_real.activate("de") - self.assertIn(fk_inst_de, test_inst.test_fks.all()) - self.assertNotIn(fk_inst_en, test_inst.test_fks.all()) - trans_real.activate("en") - self.assertIn(fk_inst_en, test_inst.test_fks.all()) - self.assertNotIn(fk_inst_de, test_inst.test_fks.all()) - - # Check implicit related_name reverse accessor: - self.assertIn(fk_option_en, test_inst.foreignkeymodel_set.all()) - - # Check filtering in reverse way + lookup spanning: - manager = models.TestModel.objects - trans_real.activate("de") - self.assertEqual(manager.filter(test_fks=fk_inst_both).count(), 1) - self.assertEqual(manager.filter(test_fks=fk_inst_de).count(), 1) - self.assertEqual(manager.filter(test_fks__id=fk_inst_de.pk).count(), 1) - self.assertEqual(manager.filter(test_fks=fk_inst_en).count(), 0) - self.assertEqual(manager.filter(test_fks_en=fk_inst_en).count(), 1) - self.assertEqual(manager.filter(foreignkeymodel=fk_option_de).count(), 1) - self.assertEqual(manager.filter(foreignkeymodel=fk_option_en).count(), 0) - self.assertEqual(manager.filter(foreignkeymodel_en=fk_option_en).count(), 1) - self.assertEqual(manager.filter(test_fks__title='f_title_de').distinct().count(), 1) - self.assertEqual(manager.filter(test_fks__title='f_title_en').distinct().count(), 0) - self.assertEqual(manager.filter(test_fks__title_en='f_title_en').distinct().count(), 1) - trans_real.activate("en") - self.assertEqual(manager.filter(test_fks=fk_inst_both).count(), 1) - self.assertEqual(manager.filter(test_fks=fk_inst_en).count(), 1) - self.assertEqual(manager.filter(test_fks__id=fk_inst_en.pk).count(), 1) - self.assertEqual(manager.filter(test_fks=fk_inst_de).count(), 0) - self.assertEqual(manager.filter(test_fks_de=fk_inst_de).count(), 1) - self.assertEqual(manager.filter(foreignkeymodel=fk_option_en).count(), 1) - self.assertEqual(manager.filter(foreignkeymodel=fk_option_de).count(), 0) - self.assertEqual(manager.filter(foreignkeymodel_de=fk_option_de).count(), 1) - self.assertEqual(manager.filter(test_fks__title='f_title_en').distinct().count(), 1) - self.assertEqual(manager.filter(test_fks__title='f_title_de').distinct().count(), 0) - self.assertEqual(manager.filter(test_fks__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_fks = [fk_inst_de, fk_inst_both] - test_inst2.test_fks_en = (fk_inst_en, fk_inst_both) - - self.assertEqual(fk_inst_both.test.pk, test_inst2.pk) - self.assertEqual(fk_inst_both.test_id, test_inst2.pk) - self.assertEqual(fk_inst_both.test_de, test_inst2) - self.assertQuerysetsEqual(test_inst2.test_fks_de.all(), test_inst2.test_fks.all()) - self.assertIn(fk_inst_both, test_inst2.test_fks.all()) - self.assertIn(fk_inst_de, test_inst2.test_fks.all()) - self.assertNotIn(fk_inst_en, test_inst2.test_fks.all()) - trans_real.activate("en") - self.assertQuerysetsEqual(test_inst2.test_fks_en.all(), test_inst2.test_fks.all()) - self.assertIn(fk_inst_both, test_inst2.test_fks.all()) - self.assertIn(fk_inst_en, test_inst2.test_fks.all()) - self.assertNotIn(fk_inst_de, test_inst2.test_fks.all()) - - 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_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 = self.model.objects.create(non_de=non_de) - fk_inst_en = self.model.objects.create(non_en=non_en) - - # Forward relation + spanning - 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) - self.assertEqual(manager.filter(non_en=non_en).count(), 2) - self.assertEqual(manager.filter(non__title='title_de').count(), 2) - self.assertEqual(manager.filter(non__title='title_en').count(), 0) - self.assertEqual(manager.filter(non_en__title='title_en').count(), 2) - trans_real.activate("en") - self.assertEqual(manager.filter(non=non_en).count(), 2) - self.assertEqual(manager.filter(non=non_de).count(), 0) - self.assertEqual(manager.filter(non_de=non_de).count(), 2) - self.assertEqual(manager.filter(non__title='title_en').count(), 2) - self.assertEqual(manager.filter(non__title='title_de').count(), 0) - self.assertEqual(manager.filter(non_de__title='title_de').count(), 2) - - # Reverse relation + spanning - manager = models.NonTranslated.objects - trans_real.activate("de") - self.assertEqual(manager.filter(test_fks=fk_inst_both).count(), 1) - self.assertEqual(manager.filter(test_fks=fk_inst_de).count(), 1) - self.assertEqual(manager.filter(test_fks=fk_inst_en).count(), 0) - self.assertEqual(manager.filter(test_fks_en=fk_inst_en).count(), 1) - self.assertEqual(manager.filter(test_fks__title='f_title_de').count(), 1) - self.assertEqual(manager.filter(test_fks__title='f_title_en').count(), 0) - self.assertEqual(manager.filter(test_fks__title_en='f_title_en').count(), 1) - trans_real.activate("en") - self.assertEqual(manager.filter(test_fks=fk_inst_both).count(), 1) - self.assertEqual(manager.filter(test_fks=fk_inst_en).count(), 1) - self.assertEqual(manager.filter(test_fks=fk_inst_de).count(), 0) - self.assertEqual(manager.filter(test_fks_de=fk_inst_de).count(), 1) - self.assertEqual(manager.filter(test_fks__title='f_title_en').count(), 1) - self.assertEqual(manager.filter(test_fks__title='f_title_de').count(), 0) - self.assertEqual(manager.filter(test_fks__title_de='f_title_de').count(), 1) - - def assertQuerysetsEqual(self, qs1, qs2): - pk = lambda o: o.pk - 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() - field_names = dir(inst) - self.assertTrue('id' in field_names) - self.assertTrue('int' in field_names) - self.assertTrue('int_de' in field_names) - self.assertTrue('int_en' in field_names) - self.assertTrue('boolean' in field_names) - self.assertTrue('boolean_de' in field_names) - self.assertTrue('boolean_en' in field_names) - self.assertTrue('nullboolean' in field_names) - self.assertTrue('nullboolean_de' in field_names) - self.assertTrue('nullboolean_en' in field_names) - self.assertTrue('csi' in field_names) - self.assertTrue('csi_de' in field_names) - self.assertTrue('csi_en' in field_names) - self.assertTrue('ip' in field_names) - self.assertTrue('ip_de' in field_names) - self.assertTrue('ip_en' in field_names) -# self.assertTrue('genericip' in field_names) -# self.assertTrue('genericip_de' in field_names) -# self.assertTrue('genericip_en' in field_names) - self.assertTrue('float' in field_names) - self.assertTrue('float_de' in field_names) - self.assertTrue('float_en' in field_names) - self.assertTrue('decimal' in field_names) - self.assertTrue('decimal_de' in field_names) - self.assertTrue('decimal_en' in field_names) - inst.delete() - - def test_translated_models_integer_instance(self): - inst = models.OtherFieldsModel() - inst.int = 7 - 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 - - inst.int += 2 - inst.save() - self.assertEqual(9, inst.int) - self.assertEqual(9, inst.int_de) - self.assertEqual(42, inst.int_en) - - trans_real.activate('en') - inst.int -= 1 - self.assertEqual(41, inst.int) - self.assertEqual(9, inst.int_de) - self.assertEqual(41, inst.int_en) - - # this field has validator - let's try to make it below 0! - inst.int -= 50 - self.assertRaises(ValidationError, inst.full_clean) - - def test_translated_models_boolean_instance(self): - inst = models.OtherFieldsModel() - inst.boolean = True - self.assertEqual('de', get_language()) - self.assertEqual(True, inst.boolean) - self.assertEqual(True, inst.boolean_de) - self.assertEqual(False, inst.boolean_en) - - inst.boolean = False - inst.save() - self.assertEqual(False, inst.boolean) - self.assertEqual(False, inst.boolean_de) - self.assertEqual(False, inst.boolean_en) - - trans_real.activate('en') - inst.boolean = True - self.assertEqual(True, inst.boolean) - self.assertEqual(False, inst.boolean_de) - self.assertEqual(True, inst.boolean_en) - - def test_translated_models_nullboolean_instance(self): - inst = models.OtherFieldsModel() - inst.nullboolean = True - self.assertEqual('de', get_language()) - self.assertEqual(True, inst.nullboolean) - self.assertEqual(True, inst.nullboolean_de) - self.assertEqual(None, inst.nullboolean_en) - - inst.nullboolean = False - inst.save() - self.assertEqual(False, inst.nullboolean) - self.assertEqual(False, inst.nullboolean_de) - self.assertEqual(None, inst.nullboolean_en) - - trans_real.activate('en') - inst.nullboolean = True - self.assertEqual(True, inst.nullboolean) - self.assertEqual(False, inst.nullboolean_de) - self.assertEqual(True, inst.nullboolean_en) - - inst.nullboolean = None - self.assertEqual(None, inst.nullboolean) - self.assertEqual(False, inst.nullboolean_de) - self.assertEqual(None, inst.nullboolean_en) - - def test_translated_models_commaseparatedinteger_instance(self): - inst = models.OtherFieldsModel() - inst.csi = '4,8,15,16,23,42' - self.assertEqual('de', get_language()) - self.assertEqual('4,8,15,16,23,42', inst.csi) - self.assertEqual('4,8,15,16,23,42', inst.csi_de) - self.assertEqual(None, inst.csi_en) - - inst.csi = '23,42' - inst.save() - self.assertEqual('23,42', inst.csi) - self.assertEqual('23,42', inst.csi_de) - self.assertEqual(None, inst.csi_en) - - trans_real.activate('en') - inst.csi = '4,8,15,16,23,42' - self.assertEqual('4,8,15,16,23,42', inst.csi) - self.assertEqual('23,42', inst.csi_de) - self.assertEqual('4,8,15,16,23,42', inst.csi_en) - - # Now that we have covered csi, lost, illuminati and hitchhiker - # compliance in a single test, do something useful... - - # Check if validation is preserved - inst.csi = '1;2' - self.assertRaises(ValidationError, inst.full_clean) - - def test_translated_models_ipaddress_instance(self): - inst = models.OtherFieldsModel() - inst.ip = '192.0.1.42' - self.assertEqual('de', get_language()) - self.assertEqual('192.0.1.42', inst.ip) - self.assertEqual('192.0.1.42', inst.ip_de) - self.assertEqual(None, inst.ip_en) - - inst.ip = '192.0.23.1' - inst.save() - self.assertEqual('192.0.23.1', inst.ip) - self.assertEqual('192.0.23.1', inst.ip_de) - self.assertEqual(None, inst.ip_en) - - trans_real.activate('en') - inst.ip = '192.0.1.42' - self.assertEqual('192.0.1.42', inst.ip) - self.assertEqual('192.0.23.1', inst.ip_de) - self.assertEqual('192.0.1.42', inst.ip_en) - - # Check if validation is preserved - inst.ip = '1;2' - self.assertRaises(ValidationError, inst.full_clean) - -# def test_translated_models_genericipaddress_instance(self): -# inst = OtherFieldsModel() -# inst.genericip = '2a02:42fe::4' -# self.assertEqual('de', get_language()) -# self.assertEqual('2a02:42fe::4', inst.genericip) -# self.assertEqual('2a02:42fe::4', inst.genericip_de) -# self.assertEqual(None, inst.genericip_en) -# -# inst.genericip = '2a02:23fe::4' -# inst.save() -# self.assertEqual('2a02:23fe::4', inst.genericip) -# self.assertEqual('2a02:23fe::4', inst.genericip_de) -# self.assertEqual(None, inst.genericip_en) -# -# trans_real.activate('en') -# inst.genericip = '2a02:42fe::4' -# self.assertEqual('2a02:42fe::4', inst.genericip) -# self.assertEqual('2a02:23fe::4', inst.genericip_de) -# self.assertEqual('2a02:42fe::4', inst.genericip_en) -# -# # Check if validation is preserved -# inst.genericip = '1;2' -# self.assertRaises(ValidationError, inst.full_clean) - - def test_translated_models_float_instance(self): - inst = models.OtherFieldsModel() - inst.float = 0.42 - self.assertEqual('de', get_language()) - self.assertEqual(0.42, inst.float) - self.assertEqual(0.42, inst.float_de) - self.assertEqual(None, inst.float_en) - - inst.float = 0.23 - inst.save() - self.assertEqual(0.23, inst.float) - self.assertEqual(0.23, inst.float_de) - self.assertEqual(None, inst.float_en) - - inst.float += 0.08 - self.assertEqual(0.31, inst.float) - self.assertEqual(0.31, inst.float_de) - self.assertEqual(None, inst.float_en) - - trans_real.activate('en') - inst.float = 0.42 - self.assertEqual(0.42, inst.float) - self.assertEqual(0.31, inst.float_de) - self.assertEqual(0.42, inst.float_en) - - def test_translated_models_decimal_instance(self): - inst = models.OtherFieldsModel() - inst.decimal = Decimal('0.42') - self.assertEqual('de', get_language()) - self.assertEqual(Decimal('0.42'), inst.decimal) - self.assertEqual(Decimal('0.42'), inst.decimal_de) - self.assertEqual(None, inst.decimal_en) - - inst.decimal = inst.decimal - Decimal('0.19') - inst.save() - self.assertEqual(Decimal('0.23'), inst.decimal) - self.assertEqual(Decimal('0.23'), inst.decimal_de) - self.assertEqual(None, inst.decimal_en) - - trans_real.activate('en') - self.assertRaises(TypeError, lambda x: inst.decimal + Decimal('0.19')) - self.assertEqual(None, inst.decimal) - self.assertEqual(Decimal('0.23'), inst.decimal_de) - self.assertEqual(None, inst.decimal_en) - - inst.decimal = Decimal('0.42') - self.assertEqual(Decimal('0.42'), inst.decimal) - self.assertEqual(Decimal('0.23'), inst.decimal_de) - self.assertEqual(Decimal('0.42'), inst.decimal_en) - - def test_translated_models_date_instance(self): - inst = models.OtherFieldsModel() - inst.date = datetime.date(2012, 12, 31) - self.assertEqual('de', get_language()) - self.assertEqual(datetime.date(2012, 12, 31), inst.date) - self.assertEqual(datetime.date(2012, 12, 31), inst.date_de) - self.assertEqual(None, inst.date_en) - - inst.date = datetime.date(1999, 1, 1) - inst.save() - self.assertEqual(datetime.date(1999, 1, 1), inst.date) - self.assertEqual(datetime.date(1999, 1, 1), inst.date_de) - self.assertEqual(None, inst.date_en) - - qs = models.OtherFieldsModel.objects.filter(date='1999-1-1') - self.assertEqual(len(qs), 1) - self.assertEqual(qs[0].date, datetime.date(1999, 1, 1)) - - trans_real.activate('en') - inst.date = datetime.date(2012, 12, 31) - self.assertEqual(datetime.date(2012, 12, 31), inst.date) - self.assertEqual(datetime.date(1999, 1, 1), inst.date_de) - self.assertEqual(datetime.date(2012, 12, 31), inst.date_en) - - def test_translated_models_datetime_instance(self): - inst = models.OtherFieldsModel() - inst.datetime = datetime.datetime(2012, 12, 31, 23, 42) - self.assertEqual('de', get_language()) - self.assertEqual(datetime.datetime(2012, 12, 31, 23, 42), inst.datetime) - self.assertEqual(datetime.datetime(2012, 12, 31, 23, 42), inst.datetime_de) - self.assertEqual(None, inst.datetime_en) - - inst.datetime = datetime.datetime(1999, 1, 1, 23, 42) - inst.save() - self.assertEqual(datetime.datetime(1999, 1, 1, 23, 42), inst.datetime) - self.assertEqual(datetime.datetime(1999, 1, 1, 23, 42), inst.datetime_de) - self.assertEqual(None, inst.datetime_en) - - qs = models.OtherFieldsModel.objects.filter(datetime='1999-1-1 23:42') - self.assertEqual(len(qs), 1) - self.assertEqual(qs[0].datetime, datetime.datetime(1999, 1, 1, 23, 42)) - - trans_real.activate('en') - inst.datetime = datetime.datetime(2012, 12, 31, 23, 42) - self.assertEqual(datetime.datetime(2012, 12, 31, 23, 42), inst.datetime) - self.assertEqual(datetime.datetime(1999, 1, 1, 23, 42), inst.datetime_de) - self.assertEqual(datetime.datetime(2012, 12, 31, 23, 42), inst.datetime_en) - - def test_translated_models_time_instance(self): - inst = models.OtherFieldsModel() - inst.time = datetime.time(23, 42, 0) - self.assertEqual('de', get_language()) - self.assertEqual(datetime.time(23, 42, 0), inst.time) - self.assertEqual(datetime.time(23, 42, 0), inst.time_de) - self.assertEqual(None, inst.time_en) - - inst.time = datetime.time(1, 2, 3) - inst.save() - self.assertEqual(datetime.time(1, 2, 3), inst.time) - self.assertEqual(datetime.time(1, 2, 3), inst.time_de) - self.assertEqual(None, inst.time_en) - - qs = models.OtherFieldsModel.objects.filter(time='01:02:03') - self.assertEqual(len(qs), 1) - self.assertEqual(qs[0].time, datetime.time(1, 2, 3)) - - trans_real.activate('en') - inst.time = datetime.time(23, 42, 0) - self.assertEqual(datetime.time(23, 42, 0), inst.time) - self.assertEqual(datetime.time(1, 2, 3), inst.time_de) - self.assertEqual(datetime.time(23, 42, 0), inst.time_en) - - def test_descriptors(self): - # Descriptor store ints in database and returns string of 'a' of that length - inst = models.DescriptorModel() - # Demonstrate desired behaviour - inst.normal = 2 - self.assertEqual('aa', inst.normal) - inst.normal = 'abc' - self.assertEqual('aaa', inst.normal) - - # Descriptor on translated field works too - self.assertEqual('de', get_language()) - inst.trans = 5 - self.assertEqual('aaaaa', inst.trans) - - inst.save() - db_values = models.DescriptorModel.objects.raw_values('normal', 'trans_en', 'trans_de')[0] - self.assertEqual(3, db_values['normal']) - self.assertEqual(5, db_values['trans_de']) - self.assertEqual(0, db_values['trans_en']) - - # Retrieval from db - inst = models.DescriptorModel.objects.all()[0] - self.assertEqual('aaa', inst.normal) - self.assertEqual('aaaaa', inst.trans) - self.assertEqual('aaaaa', inst.trans_de) - self.assertEqual('', inst.trans_en) - - # Other language - trans_real.activate('en') - self.assertEqual('', inst.trans) - inst.trans = 'q' - self.assertEqual('a', inst.trans) - inst.trans_de = 4 - self.assertEqual('aaaa', inst.trans_de) - inst.save() - db_values = models.DescriptorModel.objects.raw_values('normal', 'trans_en', 'trans_de')[0] - self.assertEqual(3, db_values['normal']) - self.assertEqual(4, db_values['trans_de']) - self.assertEqual(1, db_values['trans_en']) - - -class ModeltranslationTestRule1(ModeltranslationTestBase): - """ - Rule 1: Reading the value from the original field returns the value in - translated to the current language. - """ - def _test_field(self, field_name, value_de, value_en, deactivate=True): - field_name_de = '%s_de' % field_name - field_name_en = '%s_en' % field_name - params = {field_name_de: value_de, field_name_en: value_en} - - n = models.TestModel.objects.create(**params) - # Language is set to 'de' at this point - self.assertEqual(get_language(), 'de') - self.assertEqual(getattr(n, field_name), value_de) - self.assertEqual(getattr(n, field_name_de), value_de) - self.assertEqual(getattr(n, field_name_en), value_en) - # Now switch to "en" - trans_real.activate("en") - self.assertEqual(get_language(), "en") - # Should now be return the english one (just by switching the language) - self.assertEqual(getattr(n, field_name), value_en) - # But explicit language fields hold their values - self.assertEqual(getattr(n, field_name_de), value_de) - self.assertEqual(getattr(n, field_name_en), value_en) - - n = models.TestModel.objects.create(**params) - n.save() - # Language is set to "en" at this point - self.assertEqual(get_language(), "en") - self.assertEqual(getattr(n, field_name), value_en) - self.assertEqual(getattr(n, field_name_de), value_de) - self.assertEqual(getattr(n, field_name_en), value_en) - trans_real.activate('de') - self.assertEqual(get_language(), 'de') - self.assertEqual(getattr(n, field_name), value_de) - - if deactivate: - trans_real.deactivate() - - def test_rule1(self): - """ - Basic CharField/TextField test. - """ - title1_de = "title de" - title1_en = "title en" - text_de = "Dies ist ein deutscher Satz" - text_en = "This is an english sentence" - - self._test_field(field_name='title', value_de=title1_de, value_en=title1_en) - self._test_field(field_name='text', value_de=text_de, value_en=text_en) - - def test_rule1_url_field(self): - self._test_field(field_name='url', - value_de='http://www.google.de', - value_en='http://www.google.com') - - def test_rule1_email_field(self): - self._test_field(field_name='email', - value_de='django-modeltranslation@googlecode.de', - value_en='django-modeltranslation@googlecode.com') - - -class ModeltranslationTestRule2(ModeltranslationTestBase): - """ - Rule 2: Assigning a value to the original field updates the value - in the associated current language translation field. - """ - def _test_field(self, field_name, value1_de, value1_en, value2, value3, - deactivate=True): - field_name_de = '%s_de' % field_name - field_name_en = '%s_en' % field_name - params = {field_name_de: value1_de, field_name_en: value1_en} - - self.assertEqual(get_language(), 'de') - n = models.TestModel.objects.create(**params) - self.assertEqual(getattr(n, field_name), value1_de) - self.assertEqual(getattr(n, field_name_de), value1_de) - self.assertEqual(getattr(n, field_name_en), value1_en) - - setattr(n, field_name, value2) - n.save() - self.assertEqual(getattr(n, field_name), value2) - self.assertEqual(getattr(n, field_name_de), value2) - self.assertEqual(getattr(n, field_name_en), value1_en) - - trans_real.activate("en") - self.assertEqual(get_language(), "en") - - setattr(n, field_name, value3) - setattr(n, field_name_de, value1_de) - n.save() - self.assertEqual(getattr(n, field_name), value3) - self.assertEqual(getattr(n, field_name_en), value3) - self.assertEqual(getattr(n, field_name_de), value1_de) - - if deactivate: - trans_real.deactivate() - - def test_rule2(self): - """ - Basic CharField/TextField test. - """ - self._test_field(field_name='title', - value1_de='title de', - value1_en='title en', - value2='Neuer Titel', - value3='new title') - - def test_rule2_url_field(self): - self._test_field(field_name='url', - value1_de='http://www.google.de', - value1_en='http://www.google.com', - value2='http://www.google.at', - value3='http://www.google.co.uk') - - def test_rule2_email_field(self): - self._test_field(field_name='email', - value1_de='django-modeltranslation@googlecode.de', - value1_en='django-modeltranslation@googlecode.com', - value2='django-modeltranslation@googlecode.at', - value3='django-modeltranslation@googlecode.co.uk') - - -class ModeltranslationTestRule3(ModeltranslationTestBase): - """ - Rule 3: If both fields - the original and the current language translation - field - are updated at the same time, the current language translation - field wins. - """ - - def test_rule3(self): - self.assertEqual(get_language(), 'de') - title = 'title de' - - # Normal behaviour - n = models.TestModel(title='foo') - self.assertEqual(n.title, 'foo') - self.assertEqual(n.title_de, 'foo') - self.assertEqual(n.title_en, None) - - # constructor - n = models.TestModel(title_de=title, title='foo') - self.assertEqual(n.title, title) - self.assertEqual(n.title_de, title) - self.assertEqual(n.title_en, None) - - # object.create - n = models.TestModel.objects.create(title_de=title, title='foo') - self.assertEqual(n.title, title) - self.assertEqual(n.title_de, title) - self.assertEqual(n.title_en, None) - - # Database save/load - n = models.TestModel.objects.get(title_de=title) - self.assertEqual(n.title, title) - self.assertEqual(n.title_de, title) - self.assertEqual(n.title_en, None) - - # This is not subject to Rule 3, because updates are not *at the ame time* - n = models.TestModel() - n.title_de = title - n.title = 'foo' - self.assertEqual(n.title, 'foo') - self.assertEqual(n.title_de, 'foo') - self.assertEqual(n.title_en, None) - - @staticmethod - def _index(list, element): - for i, el in enumerate(list): - if el is element: - return i - raise ValueError - - def test_rule3_internals(self): - # Rule 3 work because translation fields are added to model field list - # later than original field. - original = models.TestModel._meta.get_field('title') - translated_de = models.TestModel._meta.get_field('title_de') - translated_en = models.TestModel._meta.get_field('title_en') - fields = models.TestModel._meta.fields - # Here we cannot use simple list.index, because Field has overloaded __cmp__ - self.assertTrue(self._index(fields, original) < self._index(fields, translated_de)) - self.assertTrue(self._index(fields, original) < self._index(fields, translated_en)) - - -class ModelValidationTest(ModeltranslationTestBase): - """ - Tests if a translation model field validates correctly. - """ - def assertRaisesValidation(self, func): - try: - func() - except ValidationError as e: - return e.message_dict - self.fail('ValidationError not raised.') - - def _test_model_validation(self, field_name, invalid_value, valid_value): - """ - Generic model field validation test. - """ - field_name_de = '%s_de' % field_name - field_name_en = '%s_en' % field_name - # Title need to be passed here - otherwise it would not validate - params = {'title_de': 'title de', 'title_en': 'title en', field_name: invalid_value} - - n = models.TestModel.objects.create(**params) - - # First check the original field - # Expect that the validation object contains an error - errors = self.assertRaisesValidation(n.full_clean) - self.assertIn(field_name, errors) - - # Set translation field to a valid value - # Language is set to 'de' at this point - self.assertEqual(get_language(), 'de') - setattr(n, field_name_de, valid_value) - n.full_clean() - - # All language fields are validated even though original field validation raise no error - setattr(n, field_name_en, invalid_value) - errors = self.assertRaisesValidation(n.full_clean) - self.assertNotIn(field_name, errors) - self.assertIn(field_name_en, errors) - - # When language is changed to en, the original field also doesn't validate - with override('en'): - setattr(n, field_name_en, invalid_value) - errors = self.assertRaisesValidation(n.full_clean) - self.assertIn(field_name, errors) - self.assertIn(field_name_en, errors) - - # Set translation field to an invalid value - setattr(n, field_name_en, valid_value) - setattr(n, field_name_de, invalid_value) - # Expect that the validation object contains an error for url_de - errors = self.assertRaisesValidation(n.full_clean) - self.assertIn(field_name, errors) - self.assertIn(field_name_de, errors) - - def test_model_validation_required(self): - """ - General test for CharField: if required/blank is handled properly. - """ - # Create an object without title (which is required) - n = models.TestModel.objects.create(text='Testtext') - - # First check the original field - # Expect that the validation object contains an error for title - errors = self.assertRaisesValidation(n.full_clean) - self.assertIn('title', errors) - n.save() - - # Check the translation field - # Language is set to 'de' at this point - self.assertEqual(get_language(), 'de') - # Set translation field to a valid title - n.title_de = 'Title' - n.full_clean() - - # Change language to en - # Now validation fails, because current language (en) title is empty - # So requirement validation depends on current language - with override('en'): - errors = self.assertRaisesValidation(n.full_clean) - self.assertIn('title', errors) - - # However, with fallback language (most cases), it validates (because empty title - # falls back to title_de): - with default_fallback(): - n.full_clean() - - # Set translation field to an empty title - n.title_de = None - # Even though the original field isn't optional, translation fields are - # per definition always optional. So we expect that the validation - # object contains no error for title_de. - # However, title still raises error, since it points to empty title_de - errors = self.assertRaisesValidation(n.full_clean) - self.assertNotIn('title_de', errors) - self.assertIn('title', errors) - - def test_model_validation_url_field(self): - self._test_model_validation( - field_name='url', - invalid_value='foo en', - valid_value='http://code.google.com/p/django-modeltranslation/') - - def test_model_validation_email_field(self): - self._test_model_validation( - field_name='email', invalid_value='foo en', - valid_value='django-modeltranslation@googlecode.com') - - -class ModelInheritanceTest(ModeltranslationTestBase): - """Tests for inheritance support in modeltranslation.""" - def test_abstract_inheritance(self): - field_names_b = models.AbstractModelB._meta.get_all_field_names() - self.assertTrue('titlea' in field_names_b) - self.assertTrue('titlea_de' in field_names_b) - self.assertTrue('titlea_en' in field_names_b) - self.assertTrue('titleb' in field_names_b) - self.assertTrue('titleb_de' in field_names_b) - self.assertTrue('titleb_en' in field_names_b) - self.assertFalse('titled' in field_names_b) - self.assertFalse('titled_de' in field_names_b) - self.assertFalse('titled_en' in field_names_b) - - def test_multitable_inheritance(self): - field_names_a = models.MultitableModelA._meta.get_all_field_names() - self.assertTrue('titlea' in field_names_a) - self.assertTrue('titlea_de' in field_names_a) - self.assertTrue('titlea_en' in field_names_a) - - field_names_b = models.MultitableModelB._meta.get_all_field_names() - self.assertTrue('titlea' in field_names_b) - self.assertTrue('titlea_de' in field_names_b) - self.assertTrue('titlea_en' in field_names_b) - self.assertTrue('titleb' in field_names_b) - self.assertTrue('titleb_de' in field_names_b) - self.assertTrue('titleb_en' in field_names_b) - - field_names_c = models.MultitableModelC._meta.get_all_field_names() - self.assertTrue('titlea' in field_names_c) - self.assertTrue('titlea_de' in field_names_c) - self.assertTrue('titlea_en' in field_names_c) - self.assertTrue('titleb' in field_names_c) - self.assertTrue('titleb_de' in field_names_c) - self.assertTrue('titleb_en' in field_names_c) - self.assertTrue('titlec' in field_names_c) - self.assertTrue('titlec_de' in field_names_c) - self.assertTrue('titlec_en' in field_names_c) - - field_names_d = models.MultitableModelD._meta.get_all_field_names() - self.assertTrue('titlea' in field_names_d) - self.assertTrue('titlea_de' in field_names_d) - self.assertTrue('titlea_en' in field_names_d) - self.assertTrue('titleb' in field_names_d) - self.assertTrue('titleb_de' in field_names_d) - self.assertTrue('titleb_en' in field_names_d) - self.assertTrue('titled' in field_names_d) - - def test_inheritance(self): - def assertLocalFields(model, local_fields): - # Proper fields are inherited. - opts = translator.translator.get_options_for_model(model) - self.assertEqual(set(opts.local_fields.keys()), set(local_fields)) - # Local translation fields are created on the model. - model_local_fields = [f.name for f in model._meta.local_fields] - for field in local_fields: - for lang in mt_settings.AVAILABLE_LANGUAGES: - translation_field = build_localized_fieldname(field, lang) - self.assertTrue(translation_field in model_local_fields) - - def assertFields(model, fields): - # The given fields are inherited. - opts = translator.translator.get_options_for_model(model) - self.assertEqual(set(opts.fields.keys()), set(fields)) - # Inherited translation fields are available on the model. - model_fields = model._meta.get_all_field_names() - for field in fields: - for lang in mt_settings.AVAILABLE_LANGUAGES: - translation_field = build_localized_fieldname(field, lang) - self.assertTrue(translation_field in model_fields) - - # Translation fields can be declared on abstract classes. - assertLocalFields(models.Slugged, ('slug',)) - assertLocalFields(models.MetaData, ('keywords',)) - assertLocalFields(models.RichText, ('content',)) - # Local fields are inherited from abstract superclasses. - assertLocalFields(models.Displayable, ('slug', 'keywords',)) - assertLocalFields(models.Page, ('slug', 'keywords', 'title',)) - # But not from concrete superclasses. - assertLocalFields(models.RichTextPage, ('content',)) - - # Fields inherited from concrete models are also available. - assertFields(models.Slugged, ('slug',)) - assertFields(models.Page, ('slug', 'keywords', 'title',)) - assertFields(models.RichTextPage, ('slug', 'keywords', 'title', - 'content',)) - - -class ModelInheritanceFieldAggregationTest(ModeltranslationTestBase): - """ - Tests for inheritance support with field aggregation - in modeltranslation. - """ - def test_field_aggregation(self): - clsb = FieldInheritanceCTranslationOptions - self.assertTrue('titlea' in clsb.fields) - self.assertTrue('titleb' in clsb.fields) - self.assertTrue('titlec' in clsb.fields) - self.assertEqual(3, len(clsb.fields)) - self.assertEqual(tuple, type(clsb.fields)) - - def test_multi_inheritance(self): - clsb = FieldInheritanceETranslationOptions - self.assertTrue('titlea' in clsb.fields) - self.assertTrue('titleb' in clsb.fields) - self.assertTrue('titlec' in clsb.fields) - self.assertTrue('titled' in clsb.fields) - self.assertTrue('titlee' in clsb.fields) - self.assertEqual(5, len(clsb.fields)) # there are no repetitions - - -class UpdateCommandTest(ModeltranslationTestBase): - def test_update_command(self): - # Here it would be convenient to use fixtures - unfortunately, - # fixtures loader doesn't use raw sql but rather creates objects, - # so translation descriptor affects result and we cannot set the - # 'original' field value. - pk1 = models.TestModel.objects.create(title_de='').pk - pk2 = models.TestModel.objects.create(title_de='already').pk - # Due to ``rewrite(False)`` here, original field will be affected. - models.TestModel.objects.all().rewrite(False).update(title='initial') - - # Check raw data using ``values`` - obj1 = models.TestModel.objects.filter(pk=pk1).raw_values()[0] - obj2 = models.TestModel.objects.filter(pk=pk2).raw_values()[0] - self.assertEqual('', obj1['title_de']) - self.assertEqual('initial', obj1['title']) - self.assertEqual('already', obj2['title_de']) - self.assertEqual('initial', obj2['title']) - - call_command('update_translation_fields', verbosity=0) - - obj1 = models.TestModel.objects.get(pk=pk1) - obj2 = models.TestModel.objects.get(pk=pk2) - self.assertEqual('initial', obj1.title_de) - self.assertEqual('already', obj2.title_de) - - -class TranslationAdminTest(ModeltranslationTestBase): - def setUp(self): - super(TranslationAdminTest, self).setUp() - self.test_obj = models.TestModel.objects.create( - title='Testtitle', text='Testtext') - self.site = AdminSite() - - def tearDown(self): - self.test_obj.delete() - super(TranslationAdminTest, self).tearDown() - - def test_default_fields(self): - class TestModelAdmin(admin.TranslationAdmin): - pass - - ma = TestModelAdmin(models.TestModel, self.site) - self.assertEqual( - tuple(ma.get_form(request).base_fields.keys()), - ('title_de', 'title_en', 'text_de', 'text_en', 'url_de', 'url_en', - 'email_de', 'email_en')) - - def test_default_fieldsets(self): - class TestModelAdmin(admin.TranslationAdmin): - pass - - ma = TestModelAdmin(models.TestModel, self.site) - # We expect that the original field is excluded and only the - # translation fields are included in fields - fields = ['title_de', 'title_en', 'text_de', 'text_en', - 'url_de', 'url_en', 'email_de', 'email_en'] - self.assertEqual( - ma.get_fieldsets(request), [(None, {'fields': fields})]) - self.assertEqual( - ma.get_fieldsets(request, self.test_obj), - [(None, {'fields': fields})]) - - def test_field_arguments(self): - class TestModelAdmin(admin.TranslationAdmin): - fields = ['title'] - - ma = TestModelAdmin(models.TestModel, self.site) - fields = ['title_de', 'title_en'] - self.assertEqual(tuple(ma.get_form(request).base_fields.keys()), tuple(fields)) - self.assertEqual( - tuple(ma.get_form(request, self.test_obj).base_fields.keys()), tuple(fields)) - - def test_field_arguments_restricted_on_form(self): - # Using `fields`. - class TestModelAdmin(admin.TranslationAdmin): - fields = ['title'] - - ma = TestModelAdmin(models.TestModel, self.site) - fields = ['title_de', 'title_en'] - self.assertEqual(tuple(ma.get_form(request).base_fields.keys()), tuple(fields)) - self.assertEqual( - tuple(ma.get_form(request, self.test_obj).base_fields.keys()), tuple(fields)) - - # Using `fieldsets`. - class TestModelAdmin(admin.TranslationAdmin): - fieldsets = [(None, {'fields': ['title']})] - - ma = TestModelAdmin(models.TestModel, self.site) - self.assertEqual(tuple(ma.get_form(request).base_fields.keys()), tuple(fields)) - self.assertEqual( - tuple(ma.get_form(request, self.test_obj).base_fields.keys()), tuple(fields)) - - # Using `exclude`. - class TestModelAdmin(admin.TranslationAdmin): - exclude = ['url', 'email'] - - ma = TestModelAdmin(models.TestModel, self.site) - fields = ['title_de', 'title_en', 'text_de', 'text_en'] - self.assertEqual( - tuple(ma.get_form(request).base_fields.keys()), tuple(fields)) - - # You can also pass a tuple to `exclude`. - class TestModelAdmin(admin.TranslationAdmin): - exclude = ('url', 'email') - - ma = TestModelAdmin(models.TestModel, self.site) - self.assertEqual( - tuple(ma.get_form(request).base_fields.keys()), tuple(fields)) - self.assertEqual( - tuple(ma.get_form(request, self.test_obj).base_fields.keys()), tuple(fields)) - - # Using `fields` and `exclude`. - class TestModelAdmin(admin.TranslationAdmin): - fields = ['title', 'url'] - exclude = ['url'] - - ma = TestModelAdmin(models.TestModel, self.site) - self.assertEqual( - tuple(ma.get_form(request).base_fields.keys()), ('title_de', 'title_en')) - - # Using `fields` and `readonly_fields`. - class TestModelAdmin(admin.TranslationAdmin): - fields = ['title', 'url'] - readonly_fields = ['url'] - - ma = TestModelAdmin(models.TestModel, self.site) - self.assertEqual( - tuple(ma.get_form(request).base_fields.keys()), ('title_de', 'title_en')) - - # Using `readonly_fields`. - # Note: readonly fields are not included in the form. - class TestModelAdmin(admin.TranslationAdmin): - readonly_fields = ['title'] - - ma = TestModelAdmin(models.TestModel, self.site) - self.assertEqual( - tuple(ma.get_form(request).base_fields.keys()), - ('text_de', 'text_en', 'url_de', 'url_en', 'email_de', 'email_en')) - - # Using grouped fields. - # Note: Current implementation flattens the nested fields. - class TestModelAdmin(admin.TranslationAdmin): - fields = (('title', 'url'), 'email',) - - ma = TestModelAdmin(models.TestModel, self.site) - self.assertEqual( - tuple(ma.get_form(request).base_fields.keys()), - ('title_de', 'title_en', 'url_de', 'url_en', 'email_de', 'email_en')) - - # Using grouped fields in `fieldsets`. - class TestModelAdmin(admin.TranslationAdmin): - fieldsets = [(None, {'fields': ('email', ('title', 'url'))})] - - ma = TestModelAdmin(models.TestModel, self.site) - fields = ['email_de', 'email_en', 'title_de', 'title_en', 'url_de', 'url_en'] - self.assertEqual(tuple(ma.get_form(request).base_fields.keys()), tuple(fields)) - self.assertEqual( - tuple(ma.get_form(request, self.test_obj).base_fields.keys()), tuple(fields)) - - def test_field_arguments_restricted_on_custom_form(self): - # Using `fields`. - class TestModelForm(forms.ModelForm): - class Meta: - model = models.TestModel - fields = ['url', 'email'] - - class TestModelAdmin(admin.TranslationAdmin): - form = TestModelForm - - ma = TestModelAdmin(models.TestModel, self.site) - fields = ['url_de', 'url_en', 'email_de', 'email_en'] - self.assertEqual( - tuple(ma.get_form(request).base_fields.keys()), tuple(fields)) - self.assertEqual( - tuple(ma.get_form(request, self.test_obj).base_fields.keys()), tuple(fields)) - - # Using `exclude`. - class TestModelForm(forms.ModelForm): - class Meta: - model = models.TestModel - exclude = ['url', 'email'] - - class TestModelAdmin(admin.TranslationAdmin): - form = TestModelForm - - ma = TestModelAdmin(models.TestModel, self.site) - fields = ['title_de', 'title_en', 'text_de', 'text_en'] - self.assertEqual( - tuple(ma.get_form(request).base_fields.keys()), tuple(fields)) - self.assertEqual( - tuple(ma.get_form(request, self.test_obj).base_fields.keys()), tuple(fields)) - - # If both, the custom form an the ModelAdmin define an `exclude` - # option, the ModelAdmin wins. This is Django behaviour. - class TestModelAdmin(admin.TranslationAdmin): - form = TestModelForm - exclude = ['url'] - - ma = TestModelAdmin(models.TestModel, self.site) - fields = ['title_de', 'title_en', 'text_de', 'text_en', 'email_de', - 'email_en'] - self.assertEqual( - tuple(ma.get_form(request).base_fields.keys()), tuple(fields)) - self.assertEqual( - tuple(ma.get_form(request, self.test_obj).base_fields.keys()), tuple(fields)) - - # Same for `fields`. - class TestModelForm(forms.ModelForm): - class Meta: - model = models.TestModel - fields = ['text', 'title'] - - class TestModelAdmin(admin.TranslationAdmin): - form = TestModelForm - fields = ['email'] - - ma = TestModelAdmin(models.TestModel, self.site) - fields = ['email_de', 'email_en'] - self.assertEqual( - tuple(ma.get_form(request).base_fields.keys()), tuple(fields)) - self.assertEqual( - tuple(ma.get_form(request, self.test_obj).base_fields.keys()), tuple(fields)) - - def test_inline_fieldsets(self): - class DataInline(admin.TranslationStackedInline): - model = models.DataModel - fieldsets = [ - ('Test', {'fields': ('data',)}) - ] - - class TestModelAdmin(admin.TranslationAdmin): - exclude = ('title', 'text',) - inlines = [DataInline] - - class DataTranslationOptions(translator.TranslationOptions): - fields = ('data',) - - translator.translator.register(models.DataModel, - DataTranslationOptions) - ma = TestModelAdmin(models.TestModel, self.site) - - fieldsets = [('Test', {'fields': ['data_de', 'data_en']})] - - try: - ma_fieldsets = ma.get_inline_instances( - request)[0].get_fieldsets(request) - except AttributeError: # Django 1.3 fallback - ma_fieldsets = ma.inlines[0]( - models.TestModel, self.site).get_fieldsets(request) - self.assertEqual(ma_fieldsets, fieldsets) - - try: - ma_fieldsets = ma.get_inline_instances( - request)[0].get_fieldsets(request, self.test_obj) - except AttributeError: # Django 1.3 fallback - ma_fieldsets = ma.inlines[0]( - models.TestModel, self.site).get_fieldsets(request, self.test_obj) - self.assertEqual(ma_fieldsets, fieldsets) - - # Remove translation for DataModel - translator.translator.unregister(models.DataModel) - - def test_build_css_class(self): - with reload_override_settings(LANGUAGES=(('de', 'German'), ('en', 'English'), - ('es-ar', 'Argentinian Spanish'),)): - fields = { - 'foo_en': 'foo-en', - 'foo_es_ar': 'foo-es_ar', - 'foo_en_us': 'foo-en_us', - 'foo_bar_de': 'foo_bar-de', - '_foo_en': '_foo-en', - '_foo_es_ar': '_foo-es_ar', - '_foo_bar_de': '_foo_bar-de', - 'foo__en': 'foo_-en', - 'foo__es_ar': 'foo_-es_ar', - 'foo_bar__de': 'foo_bar_-de', - } - for field, css in fields.items(): - self.assertEqual(build_css_class(field), css) - - def test_multitable_inheritance(self): - class MultitableModelAAdmin(admin.TranslationAdmin): - pass - - class MultitableModelBAdmin(admin.TranslationAdmin): - pass - - maa = MultitableModelAAdmin(models.MultitableModelA, self.site) - mab = MultitableModelBAdmin(models.MultitableModelB, self.site) - - self.assertEqual(tuple(maa.get_form(request).base_fields.keys()), - ('titlea_de', 'titlea_en')) - self.assertEqual(tuple(mab.get_form(request).base_fields.keys()), - ('titlea_de', 'titlea_en', 'titleb_de', 'titleb_en')) - - def test_group_fieldsets(self): - # Declared fieldsets take precedence over group_fieldsets - class GroupFieldsetsModelAdmin(admin.TranslationAdmin): - fieldsets = [(None, {'fields': ['title']})] - group_fieldsets = True - ma = GroupFieldsetsModelAdmin(models.GroupFieldsetsModel, self.site) - fields = ['title_de', 'title_en'] - self.assertEqual(tuple(ma.get_form(request).base_fields.keys()), tuple(fields)) - self.assertEqual( - tuple(ma.get_form(request, self.test_obj).base_fields.keys()), tuple(fields)) - - # Now set group_fieldsets only - class GroupFieldsetsModelAdmin(admin.TranslationAdmin): - group_fieldsets = True - ma = GroupFieldsetsModelAdmin(models.GroupFieldsetsModel, self.site) - # Only text and title are registered for translation. We expect to get - # three fieldsets. The first which gathers all untranslated field - # (email only) and one for each translation field (text and title). - fieldsets = [ - ('', {'fields': ['email']}), - ('title', {'classes': ('mt-fieldset',), 'fields': ['title_de', 'title_en']}), - ('text', {'classes': ('mt-fieldset',), 'fields': ['text_de', 'text_en']}), - ] - self.assertEqual(ma.get_fieldsets(request), fieldsets) - self.assertEqual(ma.get_fieldsets(request, self.test_obj), fieldsets) - - # Verify that other options are still taken into account - - # Exclude an untranslated field - class GroupFieldsetsModelAdmin(admin.TranslationAdmin): - group_fieldsets = True - exclude = ('email',) - ma = GroupFieldsetsModelAdmin(models.GroupFieldsetsModel, self.site) - fieldsets = [ - ('title', {'classes': ('mt-fieldset',), 'fields': ['title_de', 'title_en']}), - ('text', {'classes': ('mt-fieldset',), 'fields': ['text_de', 'text_en']}), - ] - self.assertEqual(ma.get_fieldsets(request), fieldsets) - self.assertEqual(ma.get_fieldsets(request, self.test_obj), fieldsets) - - # Exclude a translation field - class GroupFieldsetsModelAdmin(admin.TranslationAdmin): - group_fieldsets = True - exclude = ('text',) - ma = GroupFieldsetsModelAdmin(models.GroupFieldsetsModel, self.site) - fieldsets = [ - ('', {'fields': ['email']}), - ('title', {'classes': ('mt-fieldset',), 'fields': ['title_de', 'title_en']}) - ] - self.assertEqual(ma.get_fieldsets(request), fieldsets) - self.assertEqual(ma.get_fieldsets(request, self.test_obj), fieldsets) - - def test_prepopulated_fields(self): - trans_real.activate('de') - self.assertEqual(get_language(), 'de') - - # Non-translated slug based on translated field (using active language) - class NameModelAdmin(admin.TranslationAdmin): - prepopulated_fields = {'slug': ('firstname',)} - ma = NameModelAdmin(models.NameModel, self.site) - self.assertEqual(ma.prepopulated_fields, {'slug': ('firstname_de',)}) - - # Checking multi-field - class NameModelAdmin(admin.TranslationAdmin): - prepopulated_fields = {'slug': ('firstname', 'lastname',)} - ma = NameModelAdmin(models.NameModel, self.site) - self.assertEqual(ma.prepopulated_fields, {'slug': ('firstname_de', 'lastname_de',)}) - - # Non-translated slug based on non-translated field (no change) - class NameModelAdmin(admin.TranslationAdmin): - prepopulated_fields = {'slug': ('age',)} - ma = NameModelAdmin(models.NameModel, self.site) - self.assertEqual(ma.prepopulated_fields, {'slug': ('age',)}) - - # Translated slug based on non-translated field (all populated on the same value) - class NameModelAdmin(admin.TranslationAdmin): - prepopulated_fields = {'slug2': ('age',)} - ma = NameModelAdmin(models.NameModel, self.site) - self.assertEqual(ma.prepopulated_fields, {'slug2_en': ('age',), 'slug2_de': ('age',)}) - - # Translated slug based on translated field (corresponding) - class NameModelAdmin(admin.TranslationAdmin): - prepopulated_fields = {'slug2': ('firstname',)} - ma = NameModelAdmin(models.NameModel, self.site) - self.assertEqual(ma.prepopulated_fields, {'slug2_en': ('firstname_en',), - 'slug2_de': ('firstname_de',)}) - - # Check that current active language is used - trans_real.activate('en') - self.assertEqual(get_language(), 'en') - - class NameModelAdmin(admin.TranslationAdmin): - prepopulated_fields = {'slug': ('firstname',)} - ma = NameModelAdmin(models.NameModel, self.site) - self.assertEqual(ma.prepopulated_fields, {'slug': ('firstname_en',)}) - - # Prepopulation language can be overriden by MODELTRANSLATION_PREPOPULATE_LANGUAGE - with reload_override_settings(MODELTRANSLATION_PREPOPULATE_LANGUAGE='de'): - class NameModelAdmin(admin.TranslationAdmin): - prepopulated_fields = {'slug': ('firstname',)} - ma = NameModelAdmin(models.NameModel, self.site) - self.assertEqual(ma.prepopulated_fields, {'slug': ('firstname_de',)}) - - def test_proxymodel_field_argument(self): - class ProxyTestModelAdmin(admin.TranslationAdmin): - fields = ['title'] - - ma = ProxyTestModelAdmin(models.ProxyTestModel, self.site) - fields = ['title_de', 'title_en'] - self.assertEqual(tuple(ma.get_form(request).base_fields.keys()), tuple(fields)) - self.assertEqual( - tuple(ma.get_form(request, self.test_obj).base_fields.keys()), tuple(fields)) - - -class ThirdPartyAppIntegrationTest(ModeltranslationTestBase): - """ - This test case and a test case below have identical tests. The models they test have the same - definition - but in this case the model is not registered for translation and in the other - case it is. - """ - registered = False - - @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(ThirdPartyAppIntegrationTest, cls).setUpClass() - cls.model = models.ThirdPartyModel - - def test_form(self): - class CreationForm(forms.ModelForm): - class Meta: - model = self.model - - creation_form = CreationForm({'name': 'abc'}) - inst = creation_form.save() - self.assertEqual('de', get_language()) - self.assertEqual('abc', inst.name) - self.assertEqual(1, self.model.objects.count()) - - -class ThirdPartyAppIntegrationRegisteredTest(ThirdPartyAppIntegrationTest): - registered = True - - @classmethod - def setUpClass(cls): - super(ThirdPartyAppIntegrationRegisteredTest, cls).setUpClass() - cls.model = models.ThirdPartyRegisteredModel - - -class TestManager(ModeltranslationTestBase): - def setUp(self): - # In this test case the default language is en, not de. - super(TestManager, self).setUp() - trans_real.activate('en') - - def test_filter_update(self): - """Test if filtering and updating is language-aware.""" - n = models.ManagerTestModel(title='') - n.title_en = 'en' - n.title_de = 'de' - n.save() - - m = models.ManagerTestModel(title='') - m.title_en = 'title en' - m.title_de = 'de' - m.save() - - self.assertEqual('en', get_language()) - - self.assertEqual(0, models.ManagerTestModel.objects.filter(title='de').count()) - self.assertEqual(1, models.ManagerTestModel.objects.filter(title='en').count()) - # Spanning works - self.assertEqual(2, models.ManagerTestModel.objects.filter(title__contains='en').count()) - - with override('de'): - self.assertEqual(2, models.ManagerTestModel.objects.filter(title='de').count()) - self.assertEqual(0, models.ManagerTestModel.objects.filter(title='en').count()) - # Spanning works - self.assertEqual(2, models.ManagerTestModel.objects.filter(title__endswith='e').count()) - - # Still possible to use explicit language version - self.assertEqual(1, models.ManagerTestModel.objects.filter(title_en='en').count()) - self.assertEqual(2, models.ManagerTestModel.objects.filter( - title_en__contains='en').count()) - - models.ManagerTestModel.objects.update(title='new') - self.assertEqual(2, models.ManagerTestModel.objects.filter(title='new').count()) - n = models.ManagerTestModel.objects.get(pk=n.pk) - m = models.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 = models.ManagerTestModel(title='') - n.title_en = 'en' - n.title_de = 'de' - n.save() - - self.assertEqual('en', get_language()) - self.assertEqual(0, models.ManagerTestModel.objects.filter(Q(title='de') - | Q(pk=42)).count()) - self.assertEqual(1, models.ManagerTestModel.objects.filter(Q(title='en') - | Q(pk=42)).count()) - - with override('de'): - self.assertEqual(1, models.ManagerTestModel.objects.filter(Q(title='de') - | Q(pk=42)).count()) - self.assertEqual(0, models.ManagerTestModel.objects.filter(Q(title='en') - | Q(pk=42)).count()) - - def test_f(self): - """Test if F queries are rewritten.""" - n = models.ManagerTestModel.objects.create(visits_en=1, visits_de=2) - - self.assertEqual('en', get_language()) - models.ManagerTestModel.objects.update(visits=F('visits') + 10) - n = models.ManagerTestModel.objects.all()[0] - self.assertEqual(n.visits_en, 11) - self.assertEqual(n.visits_de, 2) - - with override('de'): - models.ManagerTestModel.objects.update(visits=F('visits') + 20) - n = models.ManagerTestModel.objects.all()[0] - self.assertEqual(n.visits_en, 11) - self.assertEqual(n.visits_de, 22) - - def test_order_by(self): - """Check that field names are rewritten in order_by keys.""" - manager = models.ManagerTestModel.objects - manager.create(title='a') - m = manager.create(title='b') - manager.create(title='c') - with override('de'): - # Make the order of the 'title' column different. - m.title = 'd' - m.save() - titles_asc = tuple(m.title for m in manager.order_by('title')) - titles_desc = tuple(m.title for m in manager.order_by('-title')) - self.assertEqual(titles_asc, ('a', 'b', 'c')) - self.assertEqual(titles_desc, ('c', 'b', 'a')) - - def test_order_by_meta(self): - """Check that meta ordering is rewritten.""" - manager = models.ManagerTestModel.objects - manager.create(title='more_de', visits_en=1, visits_de=2) - manager.create(title='more_en', visits_en=2, visits_de=1) - manager.create(title='most', visits_en=3, visits_de=3) - manager.create(title='least', visits_en=0, visits_de=0) - - # Ordering descending with visits_en - titles_for_en = tuple(m.title_en for m in manager.all()) - with override('de'): - # Ordering descending with visits_de - titles_for_de = tuple(m.title_en for m in manager.all()) - - self.assertEqual(titles_for_en, ('most', 'more_en', 'more_de', 'least')) - self.assertEqual(titles_for_de, ('most', 'more_de', 'more_en', 'least')) - - def test_values(self): - manager = models.ManagerTestModel.objects - manager.create(title_en='en', title_de='de') - - raw_obj = manager.raw_values('title')[0] - obj = manager.values('title')[0] - with override('de'): - raw_obj2 = manager.raw_values('title')[0] - obj2 = manager.values('title')[0] - - # Raw_values returns real database values regardless of current language - self.assertEqual(raw_obj['title'], raw_obj2['title']) - # Values present language-aware data, from the moment of retrieval - self.assertEqual(obj['title'], 'en') - self.assertEqual(obj2['title'], 'de') - - # Values_list behave similarly - self.assertEqual(list(manager.values_list('title', flat=True)), ['en']) - with override('de'): - self.assertEqual(list(manager.values_list('title', flat=True)), ['de']) - - # One can always turn rewrite off - a = list(manager.rewrite(False).values_list('title', flat=True)) - with override('de'): - b = list(manager.rewrite(False).values_list('title', flat=True)) - self.assertEqual(a, b) - - def test_custom_manager(self): - """Test if user-defined manager is still working""" - n = models.CustomManagerTestModel(title='') - n.title_en = 'enigma' - n.title_de = 'foo' - n.save() - - m = models.CustomManagerTestModel(title='') - m.title_en = 'enigma' - m.title_de = 'bar' - m.save() - - # Custom method - self.assertEqual('bar', models.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, models.CustomManagerTestModel.objects.count()) - with override('de'): - self.assertEqual(1, models.CustomManagerTestModel.objects.count()) - - def test_non_objects_manager(self): - """Test if managers other than ``objects`` are patched too""" - from modeltranslation.manager import MultilingualManager - manager = models.CustomManagerTestModel.another_mgr_name - self.assertTrue(isinstance(manager, MultilingualManager)) - - def test_custom_manager2(self): - """Test if user-defined queryset is still working""" - from modeltranslation.manager import MultilingualManager, MultilingualQuerySet - manager = models.CustomManager2TestModel.objects - self.assertTrue(isinstance(manager, models.CustomManager2)) - self.assertTrue(isinstance(manager, MultilingualManager)) - qs = manager.all() - self.assertTrue(isinstance(qs, models.CustomQuerySet)) - self.assertTrue(isinstance(qs, MultilingualQuerySet)) - - def test_creation(self): - """Test if field are rewritten in create.""" - self.assertEqual('en', get_language()) - n = models.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 = models.ManagerTestModel.objects.create(title_en='foo') - self.assertEqual('foo', n.title_en) - self.assertEqual(None, n.title_de) - self.assertEqual('foo', n.title) - - # Language suffixed version wins - n = models.ManagerTestModel.objects.create(title='bar', 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 = 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.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.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 - with override('de'): - self.assertEqual('foo', n.title) # points to de - self.assertEqual('en', get_language()) - - # 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 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 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.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) - - def test_fixture_population_via_command(self): - """ - Test that the loaddata command takes new option. - """ - call_command('loaddata', 'fixture.json', verbosity=0, commit=False, populate='required') - 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) - - call_command('loaddata', 'fixture.json', verbosity=0, commit=False, populate='all') - 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, 'bar') - - # Test if option overrides current context - with auto_populate('all'): - call_command('loaddata', 'fixture.json', verbosity=0, commit=False, populate=False) - m = models.TestModel.objects.get() - self.assertEqual(m.title_en, 'foo') - self.assertEqual(m.title_de, None) - self.assertEqual(m.text_en, 'bar') - self.assertEqual(m.text_de, None) - - def assertDeferred(self, use_defer, *fields): - manager = models.TestModel.objects.defer if use_defer else models.TestModel.objects.only - inst1 = manager(*fields)[0] - with override('de'): - inst2 = manager(*fields)[0] - self.assertEqual('title_en', inst1.title) - self.assertEqual('title_en', inst2.title) - with override('de'): - self.assertEqual('title_de', inst1.title) - self.assertEqual('title_de', inst2.title) - - def test_deferred(self): - """ - Check if ``only`` and ``defer`` are working. - """ - models.TestModel.objects.create(title_de='title_de', title_en='title_en') - inst = models.TestModel.objects.only('title_en')[0] - self.assertNotEqual(inst.__class__, models.TestModel) - self.assertTrue(isinstance(inst, models.TestModel)) - self.assertDeferred(False, 'title_en') - - with auto_populate('all'): - self.assertDeferred(False, 'title') - self.assertDeferred(False, 'title_de') - self.assertDeferred(False, 'title_en') - self.assertDeferred(False, 'title_en', 'title_de') - self.assertDeferred(False, 'title', 'title_en') - self.assertDeferred(False, 'title', 'title_de') - # Check if fields are deferred properly with ``only`` - self.assertDeferred(False, 'text') - - # Defer - self.assertDeferred(True, 'title') - self.assertDeferred(True, 'title_de') - self.assertDeferred(True, 'title_en') - self.assertDeferred(True, 'title_en', 'title_de') - self.assertDeferred(True, 'title', 'title_en') - self.assertDeferred(True, 'title', 'title_de') - self.assertDeferred(True, 'text', 'email', 'url') - - def test_constructor_inheritance(self): - inst = models.AbstractModelB() - # Check if fields assigned in constructor hasn't been ignored. - self.assertEqual(inst.titlea, 'title_a') - self.assertEqual(inst.titleb, 'title_b') - - -class TranslationModelFormTest(ModeltranslationTestBase): - def test_fields(self): - class TestModelForm(TranslationModelForm): - class Meta: - model = models.TestModel - - form = TestModelForm() - self.assertEqual(list(form.base_fields), - ['title', 'title_de', 'title_en', 'text', 'text_de', 'text_en', - 'url', 'url_de', 'url_en', 'email', 'email_de', 'email_en']) - self.assertEqual(list(form.fields), ['title', 'text', 'url', 'email']) - - def test_updating_with_empty_value(self): - """ - Can we update the current language translation with an empty value, when - the original field is excluded from the form? - """ - class Form(forms.ModelForm): - class Meta: - model = models.TestModel - exclude = ('text',) - - instance = models.TestModel.objects.create(text_de='something') - form = Form({'text_de': '', 'title': 'a', 'email_de': '', 'email_en': ''}, - instance=instance) - instance = form.save() - self.assertEqual('de', get_language()) - self.assertEqual('', instance.text_de) - - -class ProxyModelTest(ModeltranslationTestBase): - def test_equality(self): - n = models.TestModel.objects.create(title='Title') - m = models.ProxyTestModel.objects.get(title='Title') - self.assertEqual(n.title, m.title) - self.assertEqual(n.title_de, m.title_de) - self.assertEqual(n.title_en, m.title_en) +# For Django < 1.6 testrunner +from .tests import * \ No newline at end of file diff --git a/modeltranslation/tests/tests.py b/modeltranslation/tests/tests.py new file mode 100644 index 00000000..ef59e845 --- /dev/null +++ b/modeltranslation/tests/tests.py @@ -0,0 +1,2711 @@ +# -*- coding: utf-8 -*- +import datetime +from decimal import Decimal +import os +import shutil +import imp + +from django import forms +from django.conf import settings as django_settings +from django.contrib.admin.sites import AdminSite +from django.contrib.auth.models import User +from django.core.exceptions import ValidationError, ImproperlyConfigured +from django.core.files.base import ContentFile +from django.core.files.storage import default_storage +from django.core.management import call_command +from django.db import IntegrityError +from django.db.models import Q, F +from django.db.models.loading import AppCache +from django.test import TestCase, TransactionTestCase +from django.test.utils import override_settings +from django.utils import six +from django.utils.translation import get_language, override, trans_real + +from modeltranslation import admin, settings as mt_settings, translator +from modeltranslation.forms import TranslationModelForm +from modeltranslation.models import autodiscover +from modeltranslation.tests import models +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, + auto_populate, fallbacks) + + +# None of the following tests really depend on the content of the request, +# so we'll just pass in None. +request = None + +# How many models are registered for tests. +TEST_MODELS = 27 + + +class reload_override_settings(override_settings): + """Context manager that not only override settings, but also reload modeltranslation conf.""" + def __enter__(self): + super(reload_override_settings, self).__enter__() + imp.reload(mt_settings) + + def __exit__(self, exc_type, exc_value, traceback): + super(reload_override_settings, self).__exit__(exc_type, exc_value, traceback) + imp.reload(mt_settings) + + +# In this test suite fallback language is turned off. This context manager temporarily turns it on. +def default_fallback(): + return reload_override_settings( + MODELTRANSLATION_FALLBACK_LANGUAGES=(mt_settings.DEFAULT_LANGUAGE,)) + + +@override_settings(**TEST_SETTINGS) +class ModeltranslationTransactionTestBase(TransactionTestCase): + urls = 'modeltranslation.tests.urls' + cache = AppCache() + synced = False + + @classmethod + def setUpClass(cls): + """ + Prepare database: + * Call syncdb to create tables for tests.models (since during + default testrunner's db creation modeltranslation.tests was not in INSTALLED_APPS + """ + super(ModeltranslationTransactionTestBase, cls).setUpClass() + if not ModeltranslationTestBase.synced: + # In order to perform only one syncdb + ModeltranslationTestBase.synced = True + with override_settings(**TEST_SETTINGS): + import sys + + # 1. Reload translation in case USE_I18N was False + from django.utils import translation + imp.reload(translation) + + # 2. Reload MT because LANGUAGES likely changed. + imp.reload(mt_settings) + imp.reload(translator) + imp.reload(admin) + + # 3. Reset test models (because autodiscover have already run, those models + # have translation fields, but for languages previously defined. We want + # to be sure that 'de' and 'en' are available) + del cls.cache.app_models['tests'] + imp.reload(models) + cls.cache.load_app('modeltranslation.tests') + sys.modules.pop('modeltranslation.tests.translation', None) + + # 4. Autodiscover + from modeltranslation import models as aut_models + imp.reload(aut_models) + + # 5. Syncdb (``migrate=False`` in case of south) + from django.db import connections, DEFAULT_DB_ALIAS + call_command('syncdb', verbosity=0, migrate=False, interactive=False, + database=connections[DEFAULT_DB_ALIAS].alias, load_initial_data=False) + + def setUp(self): + self._old_language = get_language() + trans_real.activate('de') + + def tearDown(self): + trans_real.activate(self._old_language) + + +class ModeltranslationTestBase(ModeltranslationTransactionTestBase, TestCase): + pass + + +class TestAutodiscover(ModeltranslationTestBase): + # The way the ``override_settings`` works on ``TestCase`` is wicked; + # it patches ``_pre_setup`` and ``_post_teardown`` methods. + # Because of this, if class B extends class A and both are ``override_settings``'ed, + # class B settings would be overwritten by class A settings (if some keys clash). + # To solve this, override some settings after parents ``_pre_setup`` is called. + def _pre_setup(self): + super(TestAutodiscover, self)._pre_setup() + # Add test_app to INSTALLED_APPS + new_installed_apps = django_settings.INSTALLED_APPS + ('modeltranslation.tests.test_app',) + self.__override = override_settings(INSTALLED_APPS=new_installed_apps) + self.__override.enable() + + def _post_teardown(self): + self.__override.disable() + imp.reload(mt_settings) # restore mt_settings.FALLBACK_LANGUAGES + super(TestAutodiscover, self)._post_teardown() + + @classmethod + def setUpClass(cls): + """Save registry (and restore it after tests).""" + super(TestAutodiscover, cls).setUpClass() + from copy import copy + from modeltranslation.translator import translator + cls.registry_cpy = copy(translator._registry) + + @classmethod + def tearDownClass(cls): + from modeltranslation.translator import translator + translator._registry = cls.registry_cpy + super(TestAutodiscover, cls).tearDownClass() + + def tearDown(self): + import sys + # Rollback model classes + del self.cache.app_models['test_app'] + from .test_app import models + imp.reload(models) + # Delete translation modules from import cache + sys.modules.pop('modeltranslation.tests.test_app.translation', None) + sys.modules.pop('modeltranslation.tests.project_translation', None) + super(TestAutodiscover, self).tearDown() + + def check_news(self): + from .test_app.models import News + fields = dir(News()) + self.assertIn('title', fields) + self.assertIn('title_en', fields) + self.assertIn('title_de', fields) + self.assertIn('visits', fields) + self.assertNotIn('visits_en', fields) + self.assertNotIn('visits_de', fields) + + def check_other(self, present=True): + from .test_app.models import Other + fields = dir(Other()) + self.assertIn('name', fields) + if present: + self.assertIn('name_en', fields) + self.assertIn('name_de', fields) + else: + self.assertNotIn('name_en', fields) + self.assertNotIn('name_de', fields) + + def test_simple(self): + """Check if translation is imported for installed apps.""" + autodiscover() + self.check_news() + self.check_other(present=False) + + @reload_override_settings( + MODELTRANSLATION_TRANSLATION_FILES=('modeltranslation.tests.project_translation',) + ) + def test_global(self): + """Check if translation is imported for global translation file.""" + autodiscover() + self.check_news() + self.check_other() + + @reload_override_settings( + MODELTRANSLATION_TRANSLATION_FILES=('modeltranslation.tests.test_app.translation',) + ) + def test_duplication(self): + """Check if there is no problem with duplicated filenames.""" + autodiscover() + self.check_news() + + +class ModeltranslationTest(ModeltranslationTestBase): + """Basic tests for the modeltranslation application.""" + def test_registration(self): + langs = tuple(l[0] for l in django_settings.LANGUAGES) + self.assertEqual(langs, tuple(mt_settings.AVAILABLE_LANGUAGES)) + self.assertEqual(2, len(langs)) + self.assertTrue('de' in langs) + self.assertTrue('en' in langs) + self.assertTrue(translator.translator) + + # Check that all models are registered for translation + self.assertEqual(len(translator.translator.get_registered_models()), TEST_MODELS) + + # Try to unregister a model that is not registered + self.assertRaises(translator.NotRegistered, + translator.translator.unregister, models.BasePage) + + # Try to get options for a model that is not registered + self.assertRaises(translator.NotRegistered, + translator.translator.get_options_for_model, User) + + # Ensure that a base can't be registered after a subclass. + self.assertRaises(translator.DescendantRegistered, + translator.translator.register, models.BasePage) + + # Or unregistered before it. + self.assertRaises(translator.DescendantRegistered, + translator.translator.unregister, models.Slugged) + + def test_fields(self): + field_names = dir(models.TestModel()) + self.assertTrue('id' in field_names) + self.assertTrue('title' in field_names) + self.assertTrue('title_de' in field_names) + self.assertTrue('title_en' in field_names) + self.assertTrue('text' in field_names) + self.assertTrue('text_de' in field_names) + self.assertTrue('text_en' in field_names) + self.assertTrue('url' in field_names) + self.assertTrue('url_de' in field_names) + self.assertTrue('url_en' in field_names) + self.assertTrue('email' in field_names) + self.assertTrue('email_de' in field_names) + self.assertTrue('email_en' in field_names) + + def test_verbose_name(self): + verbose_name = models.TestModel._meta.get_field('title_de').verbose_name + self.assertEqual(six.text_type(verbose_name), 'title [de]') + + def test_descriptor_introspection(self): + # See Django #8248 + try: + models.TestModel.title + models.TestModel.title.__doc__ + self.assertTrue(True) + except: + self.fail('Descriptor accessed on class should return itself.') + + def test_fields_hashes(self): + opts = models.TestModel._meta + orig = opts.get_field('title') + en = opts.get_field('title_en') + de = opts.get_field('title_de') + # Translation field retain creation_counters + self.assertEqual(orig.creation_counter, en.creation_counter) + self.assertEqual(orig.creation_counter, de.creation_counter) + # But they compare unequal + self.assertNotEqual(orig, en) + self.assertNotEqual(orig, de) + self.assertNotEqual(en, de) + # Their hashes too + self.assertNotEqual(hash(orig), hash(en)) + self.assertNotEqual(hash(orig), hash(de)) + self.assertNotEqual(hash(en), hash(de)) + self.assertEqual(3, len(set([orig, en, de]))) + # TranslationFields can compare equal if they have the same language + de.language = 'en' + self.assertNotEqual(orig, de) + self.assertEqual(en, de) + self.assertEqual(hash(en), hash(de)) + self.assertEqual(2, len(set([orig, en, de]))) + de.language = 'de' + + def test_set_translation(self): + """This test briefly shows main modeltranslation features.""" + self.assertEqual(get_language(), 'de') + title_de = "title de" + title_en = "title en" + + # The original field "title" passed in the constructor is + # populated for the current language field: "title_de". + inst2 = models.TestModel(title=title_de) + self.assertEqual(inst2.title, title_de) + self.assertEqual(inst2.title_en, None) + self.assertEqual(inst2.title_de, title_de) + + # So creating object is language-aware + with override('en'): + inst2 = models.TestModel(title=title_en) + self.assertEqual(inst2.title, title_en) + self.assertEqual(inst2.title_en, title_en) + self.assertEqual(inst2.title_de, None) + + # Value from original field is presented in current language: + inst2 = models.TestModel(title_de=title_de, title_en=title_en) + self.assertEqual(inst2.title, title_de) + with override('en'): + self.assertEqual(inst2.title, title_en) + + # Changes made via original field affect current language field: + inst2.title = 'foo' + self.assertEqual(inst2.title, 'foo') + self.assertEqual(inst2.title_en, title_en) + self.assertEqual(inst2.title_de, 'foo') + with override('en'): + inst2.title = 'bar' + self.assertEqual(inst2.title, 'bar') + self.assertEqual(inst2.title_en, 'bar') + self.assertEqual(inst2.title_de, 'foo') + self.assertEqual(inst2.title, 'foo') + + # When conflict, language field wins with original field + inst2 = models.TestModel(title='foo', title_de=title_de, title_en=title_en) + self.assertEqual(inst2.title, title_de) + self.assertEqual(inst2.title_en, title_en) + self.assertEqual(inst2.title_de, title_de) + + # Creating model and assigning only one language + inst1 = models.TestModel(title_en=title_en) + # Please note: '' and not None, because descriptor falls back to field default value + self.assertEqual(inst1.title, '') + self.assertEqual(inst1.title_en, title_en) + self.assertEqual(inst1.title_de, None) + # Assign current language value - de + inst1.title = title_de + self.assertEqual(inst1.title, title_de) + self.assertEqual(inst1.title_en, title_en) + self.assertEqual(inst1.title_de, title_de) + inst1.save() + + # Check that the translation fields are correctly saved and provide the + # correct value when retrieving them again. + n = models.TestModel.objects.get(title=title_de) + self.assertEqual(n.title, title_de) + self.assertEqual(n.title_en, title_en) + self.assertEqual(n.title_de, title_de) + + # Queries are also language-aware: + self.assertEqual(1, models.TestModel.objects.filter(title=title_de).count()) + with override('en'): + self.assertEqual(0, models.TestModel.objects.filter(title=title_de).count()) + + def test_fallback_language(self): + # Present what happens if current language field is empty + self.assertEqual(get_language(), 'de') + title_de = "title de" + + # Create model with value in de only... + inst2 = models.TestModel(title=title_de) + self.assertEqual(inst2.title, title_de) + self.assertEqual(inst2.title_en, None) + self.assertEqual(inst2.title_de, title_de) + + # In this test environment, fallback language is not set. So return value for en + # will be field's default: '' + with override('en'): + self.assertEqual(inst2.title, '') + self.assertEqual(inst2.title_en, None) # Language field access returns real value + + # However, by default FALLBACK_LANGUAGES is set to DEFAULT_LANGUAGE + with default_fallback(): + + # No change here... + self.assertEqual(inst2.title, title_de) + + # ... but for empty en fall back to de + with override('en'): + self.assertEqual(inst2.title, title_de) + self.assertEqual(inst2.title_en, None) # Still real value + + def test_fallback_values_1(self): + """ + If ``fallback_values`` is set to string, all untranslated fields would + return this string. + """ + title1_de = "title de" + n = models.FallbackModel(title=title1_de) + n.save() + n = models.FallbackModel.objects.get(title=title1_de) + self.assertEqual(n.title, title1_de) + trans_real.activate("en") + self.assertEqual(n.title, "fallback") + + def test_fallback_values_2(self): + """ + If ``fallback_values`` is set to ``dict``, all untranslated fields in + ``dict`` would return this mapped value. Fields not in ``dict`` would + return default translation. + """ + title1_de = "title de" + text1_de = "text in german" + n = models.FallbackModel2(title=title1_de, text=text1_de) + n.save() + n = models.FallbackModel2.objects.get(title=title1_de) + trans_real.activate("en") + self.assertEqual(n.title, '') # Falling back to default field value + self.assertEqual( + n.text, + FallbackModel2TranslationOptions.fallback_values['text']) + + def _compare_instances(self, x, y, field): + self.assertEqual(getattr(x, field), getattr(y, field), + "Constructor diff on field %s." % field) + + def _test_constructor(self, keywords): + n = models.TestModel(**keywords) + m = models.TestModel.objects.create(**keywords) + opts = translator.translator.get_options_for_model(models.TestModel) + for base_field, trans_fields in opts.fields.items(): + self._compare_instances(n, m, base_field) + for lang_field in trans_fields: + self._compare_instances(n, m, lang_field.name) + + def test_constructor(self): + """ + Ensure that model constructor behaves exactly the same as objects.create + """ + # test different arguments compositions + keywords = dict( + # original only + title='title', + # both languages + original + email='q@q.qq', email_de='d@d.dd', email_en='e@e.ee', + # both languages without original + text_en='text en', text_de='text de', + ) + self._test_constructor(keywords) + + keywords = dict( + # only current language + title_de='title', + # only not current language + url_en='http://www.google.com', + # original + current + text='text def', text_de='text de', + # original + not current + email='q@q.qq', email_en='e@e.ee', + ) + self._test_constructor(keywords) + + +class ModeltranslationTransactionTest(ModeltranslationTransactionTestBase): + def test_unique_nullable_field(self): + from django.db import transaction + models.UniqueNullableModel.objects.create() + models.UniqueNullableModel.objects.create() + models.UniqueNullableModel.objects.create(title=None) + models.UniqueNullableModel.objects.create(title=None) + + models.UniqueNullableModel.objects.create(title='') + self.assertRaises(IntegrityError, models.UniqueNullableModel.objects.create, title='') + transaction.rollback() # Postgres + models.UniqueNullableModel.objects.create(title='foo') + self.assertRaises(IntegrityError, models.UniqueNullableModel.objects.create, title='foo') + transaction.rollback() # Postgres + + +class FallbackTests(ModeltranslationTestBase): + test_fallback = { + 'default': ('de',), + 'de': ('en',) + } + + def test_settings(self): + # Initial + self.assertEqual(mt_settings.FALLBACK_LANGUAGES, {'default': ()}) + # Tuple/list + with reload_override_settings(MODELTRANSLATION_FALLBACK_LANGUAGES=('de',)): + self.assertEqual(mt_settings.FALLBACK_LANGUAGES, {'default': ('de',)}) + # Whole dict + with reload_override_settings(MODELTRANSLATION_FALLBACK_LANGUAGES=self.test_fallback): + self.assertEqual(mt_settings.FALLBACK_LANGUAGES, self.test_fallback) + # Improper language raises error + config = {'default': (), 'fr': ('en',)} + with override_settings(MODELTRANSLATION_FALLBACK_LANGUAGES=config): + self.assertRaises(ImproperlyConfigured, lambda: imp.reload(mt_settings)) + imp.reload(mt_settings) + + def test_resolution_order(self): + from modeltranslation.utils import resolution_order + with reload_override_settings(MODELTRANSLATION_FALLBACK_LANGUAGES=self.test_fallback): + self.assertEqual(('en', 'de'), resolution_order('en')) + self.assertEqual(('de', 'en'), resolution_order('de')) + # Overriding + config = {'default': ()} + self.assertEqual(('en',), resolution_order('en', config)) + self.assertEqual(('de', 'en'), resolution_order('de', config)) + # Uniqueness + config = {'de': ('en', 'de')} + self.assertEqual(('en', 'de'), resolution_order('en', config)) + self.assertEqual(('de', 'en'), resolution_order('de', config)) + + # Default fallbacks are always used at the end + # That's it: fallbacks specified for a language don't replace defaults, + # but just are prepended + config = {'default': ('en', 'de'), 'de': ()} + self.assertEqual(('en', 'de'), resolution_order('en', config)) + self.assertEqual(('de', 'en'), resolution_order('de', config)) + # What one may have expected + self.assertNotEqual(('de',), resolution_order('de', config)) + + # To completely override settings, one should override all keys + config = {'default': (), 'de': ()} + self.assertEqual(('en',), resolution_order('en', config)) + self.assertEqual(('de',), resolution_order('de', config)) + + def test_fallback_languages(self): + with reload_override_settings(MODELTRANSLATION_FALLBACK_LANGUAGES=self.test_fallback): + title_de = 'title de' + title_en = 'title en' + n = models.TestModel(title=title_de) + self.assertEqual(n.title_de, title_de) + self.assertEqual(n.title_en, None) + self.assertEqual(n.title, title_de) + trans_real.activate('en') + self.assertEqual(n.title, title_de) # since default fallback is de + + n = models.TestModel(title=title_en) + self.assertEqual(n.title_de, None) + self.assertEqual(n.title_en, title_en) + self.assertEqual(n.title, title_en) + trans_real.activate('de') + self.assertEqual(n.title, title_en) # since fallback for de is en + + n.title_en = None + self.assertEqual(n.title, '') # if all fallbacks fail, return field.get_default() + + def test_fallbacks_toggle(self): + with reload_override_settings(MODELTRANSLATION_FALLBACK_LANGUAGES=self.test_fallback): + m = models.TestModel(title='foo') + with fallbacks(True): + self.assertEqual(m.title_de, 'foo') + self.assertEqual(m.title_en, None) + self.assertEqual(m.title, 'foo') + with override('en'): + self.assertEqual(m.title, 'foo') + with fallbacks(False): + self.assertEqual(m.title_de, 'foo') + self.assertEqual(m.title_en, None) + self.assertEqual(m.title, 'foo') + with override('en'): + self.assertEqual(m.title, '') # '' is the default + + def test_fallback_undefined(self): + """ + Checks if a sensible value is considered undefined and triggers + fallbacks. Tests if the value can be overridden as documented. + """ + with reload_override_settings(MODELTRANSLATION_FALLBACK_LANGUAGES=self.test_fallback): + # Non-nullable CharField falls back on empty strings. + m = models.FallbackModel(title_en='value', title_de='') + with override('en'): + self.assertEqual(m.title, 'value') + with override('de'): + self.assertEqual(m.title, 'value') + + # Nullable CharField does not fall back on empty strings. + m = models.FallbackModel(description_en='value', description_de='') + with override('en'): + self.assertEqual(m.description, 'value') + with override('de'): + self.assertEqual(m.description, '') + + # Nullable CharField does fall back on None. + m = models.FallbackModel(description_en='value', description_de=None) + with override('en'): + self.assertEqual(m.description, 'value') + with override('de'): + self.assertEqual(m.description, 'value') + + # The undefined value may be overridden. + m = models.FallbackModel2(title_en='value', title_de='') + with override('en'): + self.assertEqual(m.title, 'value') + with override('de'): + self.assertEqual(m.title, '') + m = models.FallbackModel2(title_en='value', title_de='no title') + with override('en'): + self.assertEqual(m.title, 'value') + with override('de'): + self.assertEqual(m.title, 'value') + + +class FileFieldsTest(ModeltranslationTestBase): + + def tearDown(self): + if default_storage.exists('modeltranslation_tests'): + # With FileSystemStorage uploading files creates a new directory, + # that's not automatically removed upon their deletion. + tests_dir = default_storage.path('modeltranslation_tests') + if os.path.isdir(tests_dir): + shutil.rmtree(tests_dir) + super(FileFieldsTest, self).tearDown() + + def test_translated_models(self): + field_names = dir(models.FileFieldsModel()) + self.assertTrue('id' in field_names) + self.assertTrue('title' in field_names) + self.assertTrue('title_de' in field_names) + self.assertTrue('title_en' in field_names) + self.assertTrue('file' in field_names) + self.assertTrue('file_de' in field_names) + self.assertTrue('file_en' in field_names) + self.assertTrue('image' in field_names) + self.assertTrue('image_de' in field_names) + self.assertTrue('image_en' in field_names) + + def _file_factory(self, name, content): + try: + return ContentFile(content, name=name) + except TypeError: # In Django 1.3 ContentFile had no name parameter + file = ContentFile(content) + file.name = name + return file + + def test_translated_models_instance(self): + inst = models.FileFieldsModel(title="Testtitle") + + trans_real.activate("en") + inst.title = 'title_en' + inst.file = 'a_en' + inst.file.save('b_en', ContentFile('file in english')) + inst.image = self._file_factory('i_en.jpg', 'image in english') # Direct assign + + trans_real.activate("de") + inst.title = 'title_de' + inst.file = 'a_de' + inst.file.save('b_de', ContentFile('file in german')) + inst.image = self._file_factory('i_de.jpg', 'image in german') + + inst.save() + + trans_real.activate("en") + self.assertEqual(inst.title, 'title_en') + self.assertTrue(inst.file.name.count('b_en') > 0) + self.assertEqual(inst.file.read(), b'file in english') + self.assertTrue(inst.image.name.count('i_en') > 0) + self.assertEqual(inst.image.read(), b'image in english') + + # Check if file was actually created in the global storage. + self.assertTrue(default_storage.exists(inst.file)) + self.assertTrue(inst.file.size > 0) + self.assertTrue(default_storage.exists(inst.image)) + self.assertTrue(inst.image.size > 0) + + trans_real.activate("de") + self.assertEqual(inst.title, 'title_de') + self.assertTrue(inst.file.name.count('b_de') > 0) + self.assertEqual(inst.file.read(), b'file in german') + self.assertTrue(inst.image.name.count('i_de') > 0) + self.assertEqual(inst.image.read(), b'image in german') + + inst.file_en.delete() + inst.image_en.delete() + inst.file_de.delete() + inst.image_de.delete() + + def test_empty_field(self): + from django.db.models.fields.files import FieldFile + inst = models.FileFieldsModel() + self.assertIsInstance(inst.file, FieldFile) + self.assertIsInstance(inst.file2, FieldFile) + inst.save() + inst = models.FileFieldsModel.objects.all()[0] + self.assertIsInstance(inst.file, FieldFile) + self.assertIsInstance(inst.file2, FieldFile) + + def test_fallback(self): + from django.db.models.fields.files import FieldFile + with reload_override_settings(MODELTRANSLATION_FALLBACK_LANGUAGES=('en',)): + self.assertEqual(get_language(), 'de') + inst = models.FileFieldsModel() + inst.file_de = '' + inst.file_en = 'foo' + inst.file2_de = '' + inst.file2_en = 'bar' + self.assertIsInstance(inst.file, FieldFile) + self.assertIsInstance(inst.file2, FieldFile) + self.assertEqual(inst.file.name, 'foo') + self.assertEqual(inst.file2.name, 'bar') + inst.save() + inst = models.FileFieldsModel.objects.all()[0] + self.assertIsInstance(inst.file, FieldFile) + self.assertIsInstance(inst.file2, FieldFile) + self.assertEqual(inst.file.name, 'foo') + self.assertEqual(inst.file2.name, 'bar') + + +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(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 = self.model._meta + + # Make sure the correct database columns always get used: + attname, col = meta.get_field('test').get_attname_column() + self.assertEqual(attname, 'test_id') + self.assertEqual(attname, col) + + attname, col = meta.get_field('test_en').get_attname_column() + self.assertEqual(attname, 'test_en_id') + self.assertEqual(attname, col) + + attname, col = meta.get_field('test_de').get_attname_column() + self.assertEqual(attname, 'test_de_id') + self.assertEqual(attname, col) + + def test_translated_models_instance(self): + test_inst1 = models.TestModel(title_en='title1_en', title_de='title1_de') + test_inst1.save() + test_inst2 = models.TestModel(title_en='title2_en', title_de='title2_de') + test_inst2.save() + inst = self.model() + + trans_real.activate("de") + inst.test = test_inst1 + inst.optional = None + + trans_real.activate("en") + # Test assigning relation by ID: + inst.optional_id = test_inst2.pk + inst.save() + + trans_real.activate("de") + self.assertEqual(inst.test_id, test_inst1.pk) + self.assertEqual(inst.test.title, 'title1_de') + self.assertEqual(inst.test_de_id, test_inst1.pk) + self.assertEqual(inst.test_de.title, 'title1_de') + self.assertEqual(inst.optional, None) + + # Test fallbacks: + trans_real.activate("en") + with default_fallback(): + self.assertEqual(inst.test_id, test_inst1.pk) + self.assertEqual(inst.test.pk, test_inst1.pk) + self.assertEqual(inst.test.title, 'title1_en') + + # Test English: + self.assertEqual(inst.optional_id, test_inst2.pk) + self.assertEqual(inst.optional.title, 'title2_en') + self.assertEqual(inst.optional_en_id, test_inst2.pk) + self.assertEqual(inst.optional_en.title, 'title2_en') + + # Test caching + inst.test_en = test_inst2 + inst.save() + trans_real.activate("de") + self.assertEqual(inst.test, test_inst1) + trans_real.activate("en") + self.assertEqual(inst.test, test_inst2) + + # Check filtering in direct way + lookup spanning + manager = self.model.objects + trans_real.activate("de") + self.assertEqual(manager.filter(test=test_inst1).count(), 1) + self.assertEqual(manager.filter(test_en=test_inst1).count(), 0) + self.assertEqual(manager.filter(test_de=test_inst1).count(), 1) + self.assertEqual(manager.filter(test=test_inst2).count(), 0) + self.assertEqual(manager.filter(test_en=test_inst2).count(), 1) + self.assertEqual(manager.filter(test_de=test_inst2).count(), 0) + self.assertEqual(manager.filter(test__title='title1_de').count(), 1) + self.assertEqual(manager.filter(test__title='title1_en').count(), 0) + self.assertEqual(manager.filter(test__title_en='title1_en').count(), 1) + trans_real.activate("en") + self.assertEqual(manager.filter(test=test_inst1).count(), 0) + self.assertEqual(manager.filter(test_en=test_inst1).count(), 0) + self.assertEqual(manager.filter(test_de=test_inst1).count(), 1) + self.assertEqual(manager.filter(test=test_inst2).count(), 1) + self.assertEqual(manager.filter(test_en=test_inst2).count(), 1) + self.assertEqual(manager.filter(test_de=test_inst2).count(), 0) + self.assertEqual(manager.filter(test__title='title2_en').count(), 1) + self.assertEqual(manager.filter(test__title='title2_de').count(), 0) + self.assertEqual(manager.filter(test__title_de='title2_de').count(), 1) + + def test_reverse_relations(self): + test_inst = models.TestModel(title_en='title_en', title_de='title_de') + test_inst.save() + + # Instantiate many 'ForeignKeyModel' instances: + 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 = 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_fks', testmodel_fields) + self.assertIn('test_fks_de', testmodel_fields) + self.assertIn('test_fks_en', testmodel_fields) + self.assertIn('test_fks', testmodel_methods) + self.assertIn('test_fks_de', testmodel_methods) + self.assertIn('test_fks_en', testmodel_methods) + # Implicit related_name: manager descriptor name != query field name + self.assertIn('foreignkeymodel', testmodel_fields) + self.assertIn('foreignkeymodel_de', testmodel_fields) + self.assertIn('foreignkeymodel_en', testmodel_fields) + self.assertIn('foreignkeymodel_set', testmodel_methods) + self.assertIn('foreignkeymodel_set_de', testmodel_methods) + self.assertIn('foreignkeymodel_set_en', testmodel_methods) + + # Check the German reverse accessor: + self.assertIn(fk_inst_both, test_inst.test_fks_de.all()) + self.assertIn(fk_inst_de, test_inst.test_fks_de.all()) + self.assertNotIn(fk_inst_en, test_inst.test_fks_de.all()) + + # Check the English reverse accessor: + self.assertIn(fk_inst_both, test_inst.test_fks_en.all()) + self.assertIn(fk_inst_en, test_inst.test_fks_en.all()) + self.assertNotIn(fk_inst_de, test_inst.test_fks_en.all()) + + # Check the default reverse accessor: + trans_real.activate("de") + self.assertIn(fk_inst_de, test_inst.test_fks.all()) + self.assertNotIn(fk_inst_en, test_inst.test_fks.all()) + trans_real.activate("en") + self.assertIn(fk_inst_en, test_inst.test_fks.all()) + self.assertNotIn(fk_inst_de, test_inst.test_fks.all()) + + # Check implicit related_name reverse accessor: + self.assertIn(fk_option_en, test_inst.foreignkeymodel_set.all()) + + # Check filtering in reverse way + lookup spanning: + manager = models.TestModel.objects + trans_real.activate("de") + self.assertEqual(manager.filter(test_fks=fk_inst_both).count(), 1) + self.assertEqual(manager.filter(test_fks=fk_inst_de).count(), 1) + self.assertEqual(manager.filter(test_fks__id=fk_inst_de.pk).count(), 1) + self.assertEqual(manager.filter(test_fks=fk_inst_en).count(), 0) + self.assertEqual(manager.filter(test_fks_en=fk_inst_en).count(), 1) + self.assertEqual(manager.filter(foreignkeymodel=fk_option_de).count(), 1) + self.assertEqual(manager.filter(foreignkeymodel=fk_option_en).count(), 0) + self.assertEqual(manager.filter(foreignkeymodel_en=fk_option_en).count(), 1) + self.assertEqual(manager.filter(test_fks__title='f_title_de').distinct().count(), 1) + self.assertEqual(manager.filter(test_fks__title='f_title_en').distinct().count(), 0) + self.assertEqual(manager.filter(test_fks__title_en='f_title_en').distinct().count(), 1) + trans_real.activate("en") + self.assertEqual(manager.filter(test_fks=fk_inst_both).count(), 1) + self.assertEqual(manager.filter(test_fks=fk_inst_en).count(), 1) + self.assertEqual(manager.filter(test_fks__id=fk_inst_en.pk).count(), 1) + self.assertEqual(manager.filter(test_fks=fk_inst_de).count(), 0) + self.assertEqual(manager.filter(test_fks_de=fk_inst_de).count(), 1) + self.assertEqual(manager.filter(foreignkeymodel=fk_option_en).count(), 1) + self.assertEqual(manager.filter(foreignkeymodel=fk_option_de).count(), 0) + self.assertEqual(manager.filter(foreignkeymodel_de=fk_option_de).count(), 1) + self.assertEqual(manager.filter(test_fks__title='f_title_en').distinct().count(), 1) + self.assertEqual(manager.filter(test_fks__title='f_title_de').distinct().count(), 0) + self.assertEqual(manager.filter(test_fks__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_fks = [fk_inst_de, fk_inst_both] + test_inst2.test_fks_en = (fk_inst_en, fk_inst_both) + + self.assertEqual(fk_inst_both.test.pk, test_inst2.pk) + self.assertEqual(fk_inst_both.test_id, test_inst2.pk) + self.assertEqual(fk_inst_both.test_de, test_inst2) + self.assertQuerysetsEqual(test_inst2.test_fks_de.all(), test_inst2.test_fks.all()) + self.assertIn(fk_inst_both, test_inst2.test_fks.all()) + self.assertIn(fk_inst_de, test_inst2.test_fks.all()) + self.assertNotIn(fk_inst_en, test_inst2.test_fks.all()) + trans_real.activate("en") + self.assertQuerysetsEqual(test_inst2.test_fks_en.all(), test_inst2.test_fks.all()) + self.assertIn(fk_inst_both, test_inst2.test_fks.all()) + self.assertIn(fk_inst_en, test_inst2.test_fks.all()) + self.assertNotIn(fk_inst_de, test_inst2.test_fks.all()) + + 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_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 = self.model.objects.create(non_de=non_de) + fk_inst_en = self.model.objects.create(non_en=non_en) + + # Forward relation + spanning + 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) + self.assertEqual(manager.filter(non_en=non_en).count(), 2) + self.assertEqual(manager.filter(non__title='title_de').count(), 2) + self.assertEqual(manager.filter(non__title='title_en').count(), 0) + self.assertEqual(manager.filter(non_en__title='title_en').count(), 2) + trans_real.activate("en") + self.assertEqual(manager.filter(non=non_en).count(), 2) + self.assertEqual(manager.filter(non=non_de).count(), 0) + self.assertEqual(manager.filter(non_de=non_de).count(), 2) + self.assertEqual(manager.filter(non__title='title_en').count(), 2) + self.assertEqual(manager.filter(non__title='title_de').count(), 0) + self.assertEqual(manager.filter(non_de__title='title_de').count(), 2) + + # Reverse relation + spanning + manager = models.NonTranslated.objects + trans_real.activate("de") + self.assertEqual(manager.filter(test_fks=fk_inst_both).count(), 1) + self.assertEqual(manager.filter(test_fks=fk_inst_de).count(), 1) + self.assertEqual(manager.filter(test_fks=fk_inst_en).count(), 0) + self.assertEqual(manager.filter(test_fks_en=fk_inst_en).count(), 1) + self.assertEqual(manager.filter(test_fks__title='f_title_de').count(), 1) + self.assertEqual(manager.filter(test_fks__title='f_title_en').count(), 0) + self.assertEqual(manager.filter(test_fks__title_en='f_title_en').count(), 1) + trans_real.activate("en") + self.assertEqual(manager.filter(test_fks=fk_inst_both).count(), 1) + self.assertEqual(manager.filter(test_fks=fk_inst_en).count(), 1) + self.assertEqual(manager.filter(test_fks=fk_inst_de).count(), 0) + self.assertEqual(manager.filter(test_fks_de=fk_inst_de).count(), 1) + self.assertEqual(manager.filter(test_fks__title='f_title_en').count(), 1) + self.assertEqual(manager.filter(test_fks__title='f_title_de').count(), 0) + self.assertEqual(manager.filter(test_fks__title_de='f_title_de').count(), 1) + + def assertQuerysetsEqual(self, qs1, qs2): + pk = lambda o: o.pk + 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() + field_names = dir(inst) + self.assertTrue('id' in field_names) + self.assertTrue('int' in field_names) + self.assertTrue('int_de' in field_names) + self.assertTrue('int_en' in field_names) + self.assertTrue('boolean' in field_names) + self.assertTrue('boolean_de' in field_names) + self.assertTrue('boolean_en' in field_names) + self.assertTrue('nullboolean' in field_names) + self.assertTrue('nullboolean_de' in field_names) + self.assertTrue('nullboolean_en' in field_names) + self.assertTrue('csi' in field_names) + self.assertTrue('csi_de' in field_names) + self.assertTrue('csi_en' in field_names) + self.assertTrue('ip' in field_names) + self.assertTrue('ip_de' in field_names) + self.assertTrue('ip_en' in field_names) +# self.assertTrue('genericip' in field_names) +# self.assertTrue('genericip_de' in field_names) +# self.assertTrue('genericip_en' in field_names) + self.assertTrue('float' in field_names) + self.assertTrue('float_de' in field_names) + self.assertTrue('float_en' in field_names) + self.assertTrue('decimal' in field_names) + self.assertTrue('decimal_de' in field_names) + self.assertTrue('decimal_en' in field_names) + inst.delete() + + def test_translated_models_integer_instance(self): + inst = models.OtherFieldsModel() + inst.int = 7 + 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 + + inst.int += 2 + inst.save() + self.assertEqual(9, inst.int) + self.assertEqual(9, inst.int_de) + self.assertEqual(42, inst.int_en) + + trans_real.activate('en') + inst.int -= 1 + self.assertEqual(41, inst.int) + self.assertEqual(9, inst.int_de) + self.assertEqual(41, inst.int_en) + + # this field has validator - let's try to make it below 0! + inst.int -= 50 + self.assertRaises(ValidationError, inst.full_clean) + + def test_translated_models_boolean_instance(self): + inst = models.OtherFieldsModel() + inst.boolean = True + self.assertEqual('de', get_language()) + self.assertEqual(True, inst.boolean) + self.assertEqual(True, inst.boolean_de) + self.assertEqual(False, inst.boolean_en) + + inst.boolean = False + inst.save() + self.assertEqual(False, inst.boolean) + self.assertEqual(False, inst.boolean_de) + self.assertEqual(False, inst.boolean_en) + + trans_real.activate('en') + inst.boolean = True + self.assertEqual(True, inst.boolean) + self.assertEqual(False, inst.boolean_de) + self.assertEqual(True, inst.boolean_en) + + def test_translated_models_nullboolean_instance(self): + inst = models.OtherFieldsModel() + inst.nullboolean = True + self.assertEqual('de', get_language()) + self.assertEqual(True, inst.nullboolean) + self.assertEqual(True, inst.nullboolean_de) + self.assertEqual(None, inst.nullboolean_en) + + inst.nullboolean = False + inst.save() + self.assertEqual(False, inst.nullboolean) + self.assertEqual(False, inst.nullboolean_de) + self.assertEqual(None, inst.nullboolean_en) + + trans_real.activate('en') + inst.nullboolean = True + self.assertEqual(True, inst.nullboolean) + self.assertEqual(False, inst.nullboolean_de) + self.assertEqual(True, inst.nullboolean_en) + + inst.nullboolean = None + self.assertEqual(None, inst.nullboolean) + self.assertEqual(False, inst.nullboolean_de) + self.assertEqual(None, inst.nullboolean_en) + + def test_translated_models_commaseparatedinteger_instance(self): + inst = models.OtherFieldsModel() + inst.csi = '4,8,15,16,23,42' + self.assertEqual('de', get_language()) + self.assertEqual('4,8,15,16,23,42', inst.csi) + self.assertEqual('4,8,15,16,23,42', inst.csi_de) + self.assertEqual(None, inst.csi_en) + + inst.csi = '23,42' + inst.save() + self.assertEqual('23,42', inst.csi) + self.assertEqual('23,42', inst.csi_de) + self.assertEqual(None, inst.csi_en) + + trans_real.activate('en') + inst.csi = '4,8,15,16,23,42' + self.assertEqual('4,8,15,16,23,42', inst.csi) + self.assertEqual('23,42', inst.csi_de) + self.assertEqual('4,8,15,16,23,42', inst.csi_en) + + # Now that we have covered csi, lost, illuminati and hitchhiker + # compliance in a single test, do something useful... + + # Check if validation is preserved + inst.csi = '1;2' + self.assertRaises(ValidationError, inst.full_clean) + + def test_translated_models_ipaddress_instance(self): + inst = models.OtherFieldsModel() + inst.ip = '192.0.1.42' + self.assertEqual('de', get_language()) + self.assertEqual('192.0.1.42', inst.ip) + self.assertEqual('192.0.1.42', inst.ip_de) + self.assertEqual(None, inst.ip_en) + + inst.ip = '192.0.23.1' + inst.save() + self.assertEqual('192.0.23.1', inst.ip) + self.assertEqual('192.0.23.1', inst.ip_de) + self.assertEqual(None, inst.ip_en) + + trans_real.activate('en') + inst.ip = '192.0.1.42' + self.assertEqual('192.0.1.42', inst.ip) + self.assertEqual('192.0.23.1', inst.ip_de) + self.assertEqual('192.0.1.42', inst.ip_en) + + # Check if validation is preserved + inst.ip = '1;2' + self.assertRaises(ValidationError, inst.full_clean) + +# def test_translated_models_genericipaddress_instance(self): +# inst = OtherFieldsModel() +# inst.genericip = '2a02:42fe::4' +# self.assertEqual('de', get_language()) +# self.assertEqual('2a02:42fe::4', inst.genericip) +# self.assertEqual('2a02:42fe::4', inst.genericip_de) +# self.assertEqual(None, inst.genericip_en) +# +# inst.genericip = '2a02:23fe::4' +# inst.save() +# self.assertEqual('2a02:23fe::4', inst.genericip) +# self.assertEqual('2a02:23fe::4', inst.genericip_de) +# self.assertEqual(None, inst.genericip_en) +# +# trans_real.activate('en') +# inst.genericip = '2a02:42fe::4' +# self.assertEqual('2a02:42fe::4', inst.genericip) +# self.assertEqual('2a02:23fe::4', inst.genericip_de) +# self.assertEqual('2a02:42fe::4', inst.genericip_en) +# +# # Check if validation is preserved +# inst.genericip = '1;2' +# self.assertRaises(ValidationError, inst.full_clean) + + def test_translated_models_float_instance(self): + inst = models.OtherFieldsModel() + inst.float = 0.42 + self.assertEqual('de', get_language()) + self.assertEqual(0.42, inst.float) + self.assertEqual(0.42, inst.float_de) + self.assertEqual(None, inst.float_en) + + inst.float = 0.23 + inst.save() + self.assertEqual(0.23, inst.float) + self.assertEqual(0.23, inst.float_de) + self.assertEqual(None, inst.float_en) + + inst.float += 0.08 + self.assertEqual(0.31, inst.float) + self.assertEqual(0.31, inst.float_de) + self.assertEqual(None, inst.float_en) + + trans_real.activate('en') + inst.float = 0.42 + self.assertEqual(0.42, inst.float) + self.assertEqual(0.31, inst.float_de) + self.assertEqual(0.42, inst.float_en) + + def test_translated_models_decimal_instance(self): + inst = models.OtherFieldsModel() + inst.decimal = Decimal('0.42') + self.assertEqual('de', get_language()) + self.assertEqual(Decimal('0.42'), inst.decimal) + self.assertEqual(Decimal('0.42'), inst.decimal_de) + self.assertEqual(None, inst.decimal_en) + + inst.decimal = inst.decimal - Decimal('0.19') + inst.save() + self.assertEqual(Decimal('0.23'), inst.decimal) + self.assertEqual(Decimal('0.23'), inst.decimal_de) + self.assertEqual(None, inst.decimal_en) + + trans_real.activate('en') + self.assertRaises(TypeError, lambda x: inst.decimal + Decimal('0.19')) + self.assertEqual(None, inst.decimal) + self.assertEqual(Decimal('0.23'), inst.decimal_de) + self.assertEqual(None, inst.decimal_en) + + inst.decimal = Decimal('0.42') + self.assertEqual(Decimal('0.42'), inst.decimal) + self.assertEqual(Decimal('0.23'), inst.decimal_de) + self.assertEqual(Decimal('0.42'), inst.decimal_en) + + def test_translated_models_date_instance(self): + inst = models.OtherFieldsModel() + inst.date = datetime.date(2012, 12, 31) + self.assertEqual('de', get_language()) + self.assertEqual(datetime.date(2012, 12, 31), inst.date) + self.assertEqual(datetime.date(2012, 12, 31), inst.date_de) + self.assertEqual(None, inst.date_en) + + inst.date = datetime.date(1999, 1, 1) + inst.save() + self.assertEqual(datetime.date(1999, 1, 1), inst.date) + self.assertEqual(datetime.date(1999, 1, 1), inst.date_de) + self.assertEqual(None, inst.date_en) + + qs = models.OtherFieldsModel.objects.filter(date='1999-1-1') + self.assertEqual(len(qs), 1) + self.assertEqual(qs[0].date, datetime.date(1999, 1, 1)) + + trans_real.activate('en') + inst.date = datetime.date(2012, 12, 31) + self.assertEqual(datetime.date(2012, 12, 31), inst.date) + self.assertEqual(datetime.date(1999, 1, 1), inst.date_de) + self.assertEqual(datetime.date(2012, 12, 31), inst.date_en) + + def test_translated_models_datetime_instance(self): + inst = models.OtherFieldsModel() + inst.datetime = datetime.datetime(2012, 12, 31, 23, 42) + self.assertEqual('de', get_language()) + self.assertEqual(datetime.datetime(2012, 12, 31, 23, 42), inst.datetime) + self.assertEqual(datetime.datetime(2012, 12, 31, 23, 42), inst.datetime_de) + self.assertEqual(None, inst.datetime_en) + + inst.datetime = datetime.datetime(1999, 1, 1, 23, 42) + inst.save() + self.assertEqual(datetime.datetime(1999, 1, 1, 23, 42), inst.datetime) + self.assertEqual(datetime.datetime(1999, 1, 1, 23, 42), inst.datetime_de) + self.assertEqual(None, inst.datetime_en) + + qs = models.OtherFieldsModel.objects.filter(datetime='1999-1-1 23:42') + self.assertEqual(len(qs), 1) + self.assertEqual(qs[0].datetime, datetime.datetime(1999, 1, 1, 23, 42)) + + trans_real.activate('en') + inst.datetime = datetime.datetime(2012, 12, 31, 23, 42) + self.assertEqual(datetime.datetime(2012, 12, 31, 23, 42), inst.datetime) + self.assertEqual(datetime.datetime(1999, 1, 1, 23, 42), inst.datetime_de) + self.assertEqual(datetime.datetime(2012, 12, 31, 23, 42), inst.datetime_en) + + def test_translated_models_time_instance(self): + inst = models.OtherFieldsModel() + inst.time = datetime.time(23, 42, 0) + self.assertEqual('de', get_language()) + self.assertEqual(datetime.time(23, 42, 0), inst.time) + self.assertEqual(datetime.time(23, 42, 0), inst.time_de) + self.assertEqual(None, inst.time_en) + + inst.time = datetime.time(1, 2, 3) + inst.save() + self.assertEqual(datetime.time(1, 2, 3), inst.time) + self.assertEqual(datetime.time(1, 2, 3), inst.time_de) + self.assertEqual(None, inst.time_en) + + qs = models.OtherFieldsModel.objects.filter(time='01:02:03') + self.assertEqual(len(qs), 1) + self.assertEqual(qs[0].time, datetime.time(1, 2, 3)) + + trans_real.activate('en') + inst.time = datetime.time(23, 42, 0) + self.assertEqual(datetime.time(23, 42, 0), inst.time) + self.assertEqual(datetime.time(1, 2, 3), inst.time_de) + self.assertEqual(datetime.time(23, 42, 0), inst.time_en) + + def test_descriptors(self): + # Descriptor store ints in database and returns string of 'a' of that length + inst = models.DescriptorModel() + # Demonstrate desired behaviour + inst.normal = 2 + self.assertEqual('aa', inst.normal) + inst.normal = 'abc' + self.assertEqual('aaa', inst.normal) + + # Descriptor on translated field works too + self.assertEqual('de', get_language()) + inst.trans = 5 + self.assertEqual('aaaaa', inst.trans) + + inst.save() + db_values = models.DescriptorModel.objects.raw_values('normal', 'trans_en', 'trans_de')[0] + self.assertEqual(3, db_values['normal']) + self.assertEqual(5, db_values['trans_de']) + self.assertEqual(0, db_values['trans_en']) + + # Retrieval from db + inst = models.DescriptorModel.objects.all()[0] + self.assertEqual('aaa', inst.normal) + self.assertEqual('aaaaa', inst.trans) + self.assertEqual('aaaaa', inst.trans_de) + self.assertEqual('', inst.trans_en) + + # Other language + trans_real.activate('en') + self.assertEqual('', inst.trans) + inst.trans = 'q' + self.assertEqual('a', inst.trans) + inst.trans_de = 4 + self.assertEqual('aaaa', inst.trans_de) + inst.save() + db_values = models.DescriptorModel.objects.raw_values('normal', 'trans_en', 'trans_de')[0] + self.assertEqual(3, db_values['normal']) + self.assertEqual(4, db_values['trans_de']) + self.assertEqual(1, db_values['trans_en']) + + +class ModeltranslationTestRule1(ModeltranslationTestBase): + """ + Rule 1: Reading the value from the original field returns the value in + translated to the current language. + """ + def _test_field(self, field_name, value_de, value_en, deactivate=True): + field_name_de = '%s_de' % field_name + field_name_en = '%s_en' % field_name + params = {field_name_de: value_de, field_name_en: value_en} + + n = models.TestModel.objects.create(**params) + # Language is set to 'de' at this point + self.assertEqual(get_language(), 'de') + self.assertEqual(getattr(n, field_name), value_de) + self.assertEqual(getattr(n, field_name_de), value_de) + self.assertEqual(getattr(n, field_name_en), value_en) + # Now switch to "en" + trans_real.activate("en") + self.assertEqual(get_language(), "en") + # Should now be return the english one (just by switching the language) + self.assertEqual(getattr(n, field_name), value_en) + # But explicit language fields hold their values + self.assertEqual(getattr(n, field_name_de), value_de) + self.assertEqual(getattr(n, field_name_en), value_en) + + n = models.TestModel.objects.create(**params) + n.save() + # Language is set to "en" at this point + self.assertEqual(get_language(), "en") + self.assertEqual(getattr(n, field_name), value_en) + self.assertEqual(getattr(n, field_name_de), value_de) + self.assertEqual(getattr(n, field_name_en), value_en) + trans_real.activate('de') + self.assertEqual(get_language(), 'de') + self.assertEqual(getattr(n, field_name), value_de) + + if deactivate: + trans_real.deactivate() + + def test_rule1(self): + """ + Basic CharField/TextField test. + """ + title1_de = "title de" + title1_en = "title en" + text_de = "Dies ist ein deutscher Satz" + text_en = "This is an english sentence" + + self._test_field(field_name='title', value_de=title1_de, value_en=title1_en) + self._test_field(field_name='text', value_de=text_de, value_en=text_en) + + def test_rule1_url_field(self): + self._test_field(field_name='url', + value_de='http://www.google.de', + value_en='http://www.google.com') + + def test_rule1_email_field(self): + self._test_field(field_name='email', + value_de='django-modeltranslation@googlecode.de', + value_en='django-modeltranslation@googlecode.com') + + +class ModeltranslationTestRule2(ModeltranslationTestBase): + """ + Rule 2: Assigning a value to the original field updates the value + in the associated current language translation field. + """ + def _test_field(self, field_name, value1_de, value1_en, value2, value3, + deactivate=True): + field_name_de = '%s_de' % field_name + field_name_en = '%s_en' % field_name + params = {field_name_de: value1_de, field_name_en: value1_en} + + self.assertEqual(get_language(), 'de') + n = models.TestModel.objects.create(**params) + self.assertEqual(getattr(n, field_name), value1_de) + self.assertEqual(getattr(n, field_name_de), value1_de) + self.assertEqual(getattr(n, field_name_en), value1_en) + + setattr(n, field_name, value2) + n.save() + self.assertEqual(getattr(n, field_name), value2) + self.assertEqual(getattr(n, field_name_de), value2) + self.assertEqual(getattr(n, field_name_en), value1_en) + + trans_real.activate("en") + self.assertEqual(get_language(), "en") + + setattr(n, field_name, value3) + setattr(n, field_name_de, value1_de) + n.save() + self.assertEqual(getattr(n, field_name), value3) + self.assertEqual(getattr(n, field_name_en), value3) + self.assertEqual(getattr(n, field_name_de), value1_de) + + if deactivate: + trans_real.deactivate() + + def test_rule2(self): + """ + Basic CharField/TextField test. + """ + self._test_field(field_name='title', + value1_de='title de', + value1_en='title en', + value2='Neuer Titel', + value3='new title') + + def test_rule2_url_field(self): + self._test_field(field_name='url', + value1_de='http://www.google.de', + value1_en='http://www.google.com', + value2='http://www.google.at', + value3='http://www.google.co.uk') + + def test_rule2_email_field(self): + self._test_field(field_name='email', + value1_de='django-modeltranslation@googlecode.de', + value1_en='django-modeltranslation@googlecode.com', + value2='django-modeltranslation@googlecode.at', + value3='django-modeltranslation@googlecode.co.uk') + + +class ModeltranslationTestRule3(ModeltranslationTestBase): + """ + Rule 3: If both fields - the original and the current language translation + field - are updated at the same time, the current language translation + field wins. + """ + + def test_rule3(self): + self.assertEqual(get_language(), 'de') + title = 'title de' + + # Normal behaviour + n = models.TestModel(title='foo') + self.assertEqual(n.title, 'foo') + self.assertEqual(n.title_de, 'foo') + self.assertEqual(n.title_en, None) + + # constructor + n = models.TestModel(title_de=title, title='foo') + self.assertEqual(n.title, title) + self.assertEqual(n.title_de, title) + self.assertEqual(n.title_en, None) + + # object.create + n = models.TestModel.objects.create(title_de=title, title='foo') + self.assertEqual(n.title, title) + self.assertEqual(n.title_de, title) + self.assertEqual(n.title_en, None) + + # Database save/load + n = models.TestModel.objects.get(title_de=title) + self.assertEqual(n.title, title) + self.assertEqual(n.title_de, title) + self.assertEqual(n.title_en, None) + + # This is not subject to Rule 3, because updates are not *at the ame time* + n = models.TestModel() + n.title_de = title + n.title = 'foo' + self.assertEqual(n.title, 'foo') + self.assertEqual(n.title_de, 'foo') + self.assertEqual(n.title_en, None) + + @staticmethod + def _index(list, element): + for i, el in enumerate(list): + if el is element: + return i + raise ValueError + + def test_rule3_internals(self): + # Rule 3 work because translation fields are added to model field list + # later than original field. + original = models.TestModel._meta.get_field('title') + translated_de = models.TestModel._meta.get_field('title_de') + translated_en = models.TestModel._meta.get_field('title_en') + fields = models.TestModel._meta.fields + # Here we cannot use simple list.index, because Field has overloaded __cmp__ + self.assertTrue(self._index(fields, original) < self._index(fields, translated_de)) + self.assertTrue(self._index(fields, original) < self._index(fields, translated_en)) + + +class ModelValidationTest(ModeltranslationTestBase): + """ + Tests if a translation model field validates correctly. + """ + def assertRaisesValidation(self, func): + try: + func() + except ValidationError as e: + return e.message_dict + self.fail('ValidationError not raised.') + + def _test_model_validation(self, field_name, invalid_value, valid_value): + """ + Generic model field validation test. + """ + field_name_de = '%s_de' % field_name + field_name_en = '%s_en' % field_name + # Title need to be passed here - otherwise it would not validate + params = {'title_de': 'title de', 'title_en': 'title en', field_name: invalid_value} + + n = models.TestModel.objects.create(**params) + + # First check the original field + # Expect that the validation object contains an error + errors = self.assertRaisesValidation(n.full_clean) + self.assertIn(field_name, errors) + + # Set translation field to a valid value + # Language is set to 'de' at this point + self.assertEqual(get_language(), 'de') + setattr(n, field_name_de, valid_value) + n.full_clean() + + # All language fields are validated even though original field validation raise no error + setattr(n, field_name_en, invalid_value) + errors = self.assertRaisesValidation(n.full_clean) + self.assertNotIn(field_name, errors) + self.assertIn(field_name_en, errors) + + # When language is changed to en, the original field also doesn't validate + with override('en'): + setattr(n, field_name_en, invalid_value) + errors = self.assertRaisesValidation(n.full_clean) + self.assertIn(field_name, errors) + self.assertIn(field_name_en, errors) + + # Set translation field to an invalid value + setattr(n, field_name_en, valid_value) + setattr(n, field_name_de, invalid_value) + # Expect that the validation object contains an error for url_de + errors = self.assertRaisesValidation(n.full_clean) + self.assertIn(field_name, errors) + self.assertIn(field_name_de, errors) + + def test_model_validation_required(self): + """ + General test for CharField: if required/blank is handled properly. + """ + # Create an object without title (which is required) + n = models.TestModel.objects.create(text='Testtext') + + # First check the original field + # Expect that the validation object contains an error for title + errors = self.assertRaisesValidation(n.full_clean) + self.assertIn('title', errors) + n.save() + + # Check the translation field + # Language is set to 'de' at this point + self.assertEqual(get_language(), 'de') + # Set translation field to a valid title + n.title_de = 'Title' + n.full_clean() + + # Change language to en + # Now validation fails, because current language (en) title is empty + # So requirement validation depends on current language + with override('en'): + errors = self.assertRaisesValidation(n.full_clean) + self.assertIn('title', errors) + + # However, with fallback language (most cases), it validates (because empty title + # falls back to title_de): + with default_fallback(): + n.full_clean() + + # Set translation field to an empty title + n.title_de = None + # Even though the original field isn't optional, translation fields are + # per definition always optional. So we expect that the validation + # object contains no error for title_de. + # However, title still raises error, since it points to empty title_de + errors = self.assertRaisesValidation(n.full_clean) + self.assertNotIn('title_de', errors) + self.assertIn('title', errors) + + def test_model_validation_url_field(self): + self._test_model_validation( + field_name='url', + invalid_value='foo en', + valid_value='http://code.google.com/p/django-modeltranslation/') + + def test_model_validation_email_field(self): + self._test_model_validation( + field_name='email', invalid_value='foo en', + valid_value='django-modeltranslation@googlecode.com') + + +class ModelInheritanceTest(ModeltranslationTestBase): + """Tests for inheritance support in modeltranslation.""" + def test_abstract_inheritance(self): + field_names_b = models.AbstractModelB._meta.get_all_field_names() + self.assertTrue('titlea' in field_names_b) + self.assertTrue('titlea_de' in field_names_b) + self.assertTrue('titlea_en' in field_names_b) + self.assertTrue('titleb' in field_names_b) + self.assertTrue('titleb_de' in field_names_b) + self.assertTrue('titleb_en' in field_names_b) + self.assertFalse('titled' in field_names_b) + self.assertFalse('titled_de' in field_names_b) + self.assertFalse('titled_en' in field_names_b) + + def test_multitable_inheritance(self): + field_names_a = models.MultitableModelA._meta.get_all_field_names() + self.assertTrue('titlea' in field_names_a) + self.assertTrue('titlea_de' in field_names_a) + self.assertTrue('titlea_en' in field_names_a) + + field_names_b = models.MultitableModelB._meta.get_all_field_names() + self.assertTrue('titlea' in field_names_b) + self.assertTrue('titlea_de' in field_names_b) + self.assertTrue('titlea_en' in field_names_b) + self.assertTrue('titleb' in field_names_b) + self.assertTrue('titleb_de' in field_names_b) + self.assertTrue('titleb_en' in field_names_b) + + field_names_c = models.MultitableModelC._meta.get_all_field_names() + self.assertTrue('titlea' in field_names_c) + self.assertTrue('titlea_de' in field_names_c) + self.assertTrue('titlea_en' in field_names_c) + self.assertTrue('titleb' in field_names_c) + self.assertTrue('titleb_de' in field_names_c) + self.assertTrue('titleb_en' in field_names_c) + self.assertTrue('titlec' in field_names_c) + self.assertTrue('titlec_de' in field_names_c) + self.assertTrue('titlec_en' in field_names_c) + + field_names_d = models.MultitableModelD._meta.get_all_field_names() + self.assertTrue('titlea' in field_names_d) + self.assertTrue('titlea_de' in field_names_d) + self.assertTrue('titlea_en' in field_names_d) + self.assertTrue('titleb' in field_names_d) + self.assertTrue('titleb_de' in field_names_d) + self.assertTrue('titleb_en' in field_names_d) + self.assertTrue('titled' in field_names_d) + + def test_inheritance(self): + def assertLocalFields(model, local_fields): + # Proper fields are inherited. + opts = translator.translator.get_options_for_model(model) + self.assertEqual(set(opts.local_fields.keys()), set(local_fields)) + # Local translation fields are created on the model. + model_local_fields = [f.name for f in model._meta.local_fields] + for field in local_fields: + for lang in mt_settings.AVAILABLE_LANGUAGES: + translation_field = build_localized_fieldname(field, lang) + self.assertTrue(translation_field in model_local_fields) + + def assertFields(model, fields): + # The given fields are inherited. + opts = translator.translator.get_options_for_model(model) + self.assertEqual(set(opts.fields.keys()), set(fields)) + # Inherited translation fields are available on the model. + model_fields = model._meta.get_all_field_names() + for field in fields: + for lang in mt_settings.AVAILABLE_LANGUAGES: + translation_field = build_localized_fieldname(field, lang) + self.assertTrue(translation_field in model_fields) + + # Translation fields can be declared on abstract classes. + assertLocalFields(models.Slugged, ('slug',)) + assertLocalFields(models.MetaData, ('keywords',)) + assertLocalFields(models.RichText, ('content',)) + # Local fields are inherited from abstract superclasses. + assertLocalFields(models.Displayable, ('slug', 'keywords',)) + assertLocalFields(models.Page, ('slug', 'keywords', 'title',)) + # But not from concrete superclasses. + assertLocalFields(models.RichTextPage, ('content',)) + + # Fields inherited from concrete models are also available. + assertFields(models.Slugged, ('slug',)) + assertFields(models.Page, ('slug', 'keywords', 'title',)) + assertFields(models.RichTextPage, ('slug', 'keywords', 'title', + 'content',)) + + +class ModelInheritanceFieldAggregationTest(ModeltranslationTestBase): + """ + Tests for inheritance support with field aggregation + in modeltranslation. + """ + def test_field_aggregation(self): + clsb = FieldInheritanceCTranslationOptions + self.assertTrue('titlea' in clsb.fields) + self.assertTrue('titleb' in clsb.fields) + self.assertTrue('titlec' in clsb.fields) + self.assertEqual(3, len(clsb.fields)) + self.assertEqual(tuple, type(clsb.fields)) + + def test_multi_inheritance(self): + clsb = FieldInheritanceETranslationOptions + self.assertTrue('titlea' in clsb.fields) + self.assertTrue('titleb' in clsb.fields) + self.assertTrue('titlec' in clsb.fields) + self.assertTrue('titled' in clsb.fields) + self.assertTrue('titlee' in clsb.fields) + self.assertEqual(5, len(clsb.fields)) # there are no repetitions + + +class UpdateCommandTest(ModeltranslationTestBase): + def test_update_command(self): + # Here it would be convenient to use fixtures - unfortunately, + # fixtures loader doesn't use raw sql but rather creates objects, + # so translation descriptor affects result and we cannot set the + # 'original' field value. + pk1 = models.TestModel.objects.create(title_de='').pk + pk2 = models.TestModel.objects.create(title_de='already').pk + # Due to ``rewrite(False)`` here, original field will be affected. + models.TestModel.objects.all().rewrite(False).update(title='initial') + + # Check raw data using ``values`` + obj1 = models.TestModel.objects.filter(pk=pk1).raw_values()[0] + obj2 = models.TestModel.objects.filter(pk=pk2).raw_values()[0] + self.assertEqual('', obj1['title_de']) + self.assertEqual('initial', obj1['title']) + self.assertEqual('already', obj2['title_de']) + self.assertEqual('initial', obj2['title']) + + call_command('update_translation_fields', verbosity=0) + + obj1 = models.TestModel.objects.get(pk=pk1) + obj2 = models.TestModel.objects.get(pk=pk2) + self.assertEqual('initial', obj1.title_de) + self.assertEqual('already', obj2.title_de) + + +class TranslationAdminTest(ModeltranslationTestBase): + def setUp(self): + super(TranslationAdminTest, self).setUp() + self.test_obj = models.TestModel.objects.create( + title='Testtitle', text='Testtext') + self.site = AdminSite() + + def tearDown(self): + self.test_obj.delete() + super(TranslationAdminTest, self).tearDown() + + def test_default_fields(self): + class TestModelAdmin(admin.TranslationAdmin): + pass + + ma = TestModelAdmin(models.TestModel, self.site) + self.assertEqual( + tuple(ma.get_form(request).base_fields.keys()), + ('title_de', 'title_en', 'text_de', 'text_en', 'url_de', 'url_en', + 'email_de', 'email_en')) + + def test_default_fieldsets(self): + class TestModelAdmin(admin.TranslationAdmin): + pass + + ma = TestModelAdmin(models.TestModel, self.site) + # We expect that the original field is excluded and only the + # translation fields are included in fields + fields = ['title_de', 'title_en', 'text_de', 'text_en', + 'url_de', 'url_en', 'email_de', 'email_en'] + self.assertEqual( + ma.get_fieldsets(request), [(None, {'fields': fields})]) + self.assertEqual( + ma.get_fieldsets(request, self.test_obj), + [(None, {'fields': fields})]) + + def test_field_arguments(self): + class TestModelAdmin(admin.TranslationAdmin): + fields = ['title'] + + ma = TestModelAdmin(models.TestModel, self.site) + fields = ['title_de', 'title_en'] + self.assertEqual(tuple(ma.get_form(request).base_fields.keys()), tuple(fields)) + self.assertEqual( + tuple(ma.get_form(request, self.test_obj).base_fields.keys()), tuple(fields)) + + def test_field_arguments_restricted_on_form(self): + # Using `fields`. + class TestModelAdmin(admin.TranslationAdmin): + fields = ['title'] + + ma = TestModelAdmin(models.TestModel, self.site) + fields = ['title_de', 'title_en'] + self.assertEqual(tuple(ma.get_form(request).base_fields.keys()), tuple(fields)) + self.assertEqual( + tuple(ma.get_form(request, self.test_obj).base_fields.keys()), tuple(fields)) + + # Using `fieldsets`. + class TestModelAdmin(admin.TranslationAdmin): + fieldsets = [(None, {'fields': ['title']})] + + ma = TestModelAdmin(models.TestModel, self.site) + self.assertEqual(tuple(ma.get_form(request).base_fields.keys()), tuple(fields)) + self.assertEqual( + tuple(ma.get_form(request, self.test_obj).base_fields.keys()), tuple(fields)) + + # Using `exclude`. + class TestModelAdmin(admin.TranslationAdmin): + exclude = ['url', 'email'] + + ma = TestModelAdmin(models.TestModel, self.site) + fields = ['title_de', 'title_en', 'text_de', 'text_en'] + self.assertEqual( + tuple(ma.get_form(request).base_fields.keys()), tuple(fields)) + + # You can also pass a tuple to `exclude`. + class TestModelAdmin(admin.TranslationAdmin): + exclude = ('url', 'email') + + ma = TestModelAdmin(models.TestModel, self.site) + self.assertEqual( + tuple(ma.get_form(request).base_fields.keys()), tuple(fields)) + self.assertEqual( + tuple(ma.get_form(request, self.test_obj).base_fields.keys()), tuple(fields)) + + # Using `fields` and `exclude`. + class TestModelAdmin(admin.TranslationAdmin): + fields = ['title', 'url'] + exclude = ['url'] + + ma = TestModelAdmin(models.TestModel, self.site) + self.assertEqual( + tuple(ma.get_form(request).base_fields.keys()), ('title_de', 'title_en')) + + # Using `fields` and `readonly_fields`. + class TestModelAdmin(admin.TranslationAdmin): + fields = ['title', 'url'] + readonly_fields = ['url'] + + ma = TestModelAdmin(models.TestModel, self.site) + self.assertEqual( + tuple(ma.get_form(request).base_fields.keys()), ('title_de', 'title_en')) + + # Using `readonly_fields`. + # Note: readonly fields are not included in the form. + class TestModelAdmin(admin.TranslationAdmin): + readonly_fields = ['title'] + + ma = TestModelAdmin(models.TestModel, self.site) + self.assertEqual( + tuple(ma.get_form(request).base_fields.keys()), + ('text_de', 'text_en', 'url_de', 'url_en', 'email_de', 'email_en')) + + # Using grouped fields. + # Note: Current implementation flattens the nested fields. + class TestModelAdmin(admin.TranslationAdmin): + fields = (('title', 'url'), 'email',) + + ma = TestModelAdmin(models.TestModel, self.site) + self.assertEqual( + tuple(ma.get_form(request).base_fields.keys()), + ('title_de', 'title_en', 'url_de', 'url_en', 'email_de', 'email_en')) + + # Using grouped fields in `fieldsets`. + class TestModelAdmin(admin.TranslationAdmin): + fieldsets = [(None, {'fields': ('email', ('title', 'url'))})] + + ma = TestModelAdmin(models.TestModel, self.site) + fields = ['email_de', 'email_en', 'title_de', 'title_en', 'url_de', 'url_en'] + self.assertEqual(tuple(ma.get_form(request).base_fields.keys()), tuple(fields)) + self.assertEqual( + tuple(ma.get_form(request, self.test_obj).base_fields.keys()), tuple(fields)) + + def test_field_arguments_restricted_on_custom_form(self): + # Using `fields`. + class TestModelForm(forms.ModelForm): + class Meta: + model = models.TestModel + fields = ['url', 'email'] + + class TestModelAdmin(admin.TranslationAdmin): + form = TestModelForm + + ma = TestModelAdmin(models.TestModel, self.site) + fields = ['url_de', 'url_en', 'email_de', 'email_en'] + self.assertEqual( + tuple(ma.get_form(request).base_fields.keys()), tuple(fields)) + self.assertEqual( + tuple(ma.get_form(request, self.test_obj).base_fields.keys()), tuple(fields)) + + # Using `exclude`. + class TestModelForm(forms.ModelForm): + class Meta: + model = models.TestModel + exclude = ['url', 'email'] + + class TestModelAdmin(admin.TranslationAdmin): + form = TestModelForm + + ma = TestModelAdmin(models.TestModel, self.site) + fields = ['title_de', 'title_en', 'text_de', 'text_en'] + self.assertEqual( + tuple(ma.get_form(request).base_fields.keys()), tuple(fields)) + self.assertEqual( + tuple(ma.get_form(request, self.test_obj).base_fields.keys()), tuple(fields)) + + # If both, the custom form an the ModelAdmin define an `exclude` + # option, the ModelAdmin wins. This is Django behaviour. + class TestModelAdmin(admin.TranslationAdmin): + form = TestModelForm + exclude = ['url'] + + ma = TestModelAdmin(models.TestModel, self.site) + fields = ['title_de', 'title_en', 'text_de', 'text_en', 'email_de', + 'email_en'] + self.assertEqual( + tuple(ma.get_form(request).base_fields.keys()), tuple(fields)) + self.assertEqual( + tuple(ma.get_form(request, self.test_obj).base_fields.keys()), tuple(fields)) + + # Same for `fields`. + class TestModelForm(forms.ModelForm): + class Meta: + model = models.TestModel + fields = ['text', 'title'] + + class TestModelAdmin(admin.TranslationAdmin): + form = TestModelForm + fields = ['email'] + + ma = TestModelAdmin(models.TestModel, self.site) + fields = ['email_de', 'email_en'] + self.assertEqual( + tuple(ma.get_form(request).base_fields.keys()), tuple(fields)) + self.assertEqual( + tuple(ma.get_form(request, self.test_obj).base_fields.keys()), tuple(fields)) + + def test_inline_fieldsets(self): + class DataInline(admin.TranslationStackedInline): + model = models.DataModel + fieldsets = [ + ('Test', {'fields': ('data',)}) + ] + + class TestModelAdmin(admin.TranslationAdmin): + exclude = ('title', 'text',) + inlines = [DataInline] + + class DataTranslationOptions(translator.TranslationOptions): + fields = ('data',) + + translator.translator.register(models.DataModel, + DataTranslationOptions) + ma = TestModelAdmin(models.TestModel, self.site) + + fieldsets = [('Test', {'fields': ['data_de', 'data_en']})] + + try: + ma_fieldsets = ma.get_inline_instances( + request)[0].get_fieldsets(request) + except AttributeError: # Django 1.3 fallback + ma_fieldsets = ma.inlines[0]( + models.TestModel, self.site).get_fieldsets(request) + self.assertEqual(ma_fieldsets, fieldsets) + + try: + ma_fieldsets = ma.get_inline_instances( + request)[0].get_fieldsets(request, self.test_obj) + except AttributeError: # Django 1.3 fallback + ma_fieldsets = ma.inlines[0]( + models.TestModel, self.site).get_fieldsets(request, self.test_obj) + self.assertEqual(ma_fieldsets, fieldsets) + + # Remove translation for DataModel + translator.translator.unregister(models.DataModel) + + def test_build_css_class(self): + with reload_override_settings(LANGUAGES=(('de', 'German'), ('en', 'English'), + ('es-ar', 'Argentinian Spanish'),)): + fields = { + 'foo_en': 'foo-en', + 'foo_es_ar': 'foo-es_ar', + 'foo_en_us': 'foo-en_us', + 'foo_bar_de': 'foo_bar-de', + '_foo_en': '_foo-en', + '_foo_es_ar': '_foo-es_ar', + '_foo_bar_de': '_foo_bar-de', + 'foo__en': 'foo_-en', + 'foo__es_ar': 'foo_-es_ar', + 'foo_bar__de': 'foo_bar_-de', + } + for field, css in fields.items(): + self.assertEqual(build_css_class(field), css) + + def test_multitable_inheritance(self): + class MultitableModelAAdmin(admin.TranslationAdmin): + pass + + class MultitableModelBAdmin(admin.TranslationAdmin): + pass + + maa = MultitableModelAAdmin(models.MultitableModelA, self.site) + mab = MultitableModelBAdmin(models.MultitableModelB, self.site) + + self.assertEqual(tuple(maa.get_form(request).base_fields.keys()), + ('titlea_de', 'titlea_en')) + self.assertEqual(tuple(mab.get_form(request).base_fields.keys()), + ('titlea_de', 'titlea_en', 'titleb_de', 'titleb_en')) + + def test_group_fieldsets(self): + # Declared fieldsets take precedence over group_fieldsets + class GroupFieldsetsModelAdmin(admin.TranslationAdmin): + fieldsets = [(None, {'fields': ['title']})] + group_fieldsets = True + ma = GroupFieldsetsModelAdmin(models.GroupFieldsetsModel, self.site) + fields = ['title_de', 'title_en'] + self.assertEqual(tuple(ma.get_form(request).base_fields.keys()), tuple(fields)) + self.assertEqual( + tuple(ma.get_form(request, self.test_obj).base_fields.keys()), tuple(fields)) + + # Now set group_fieldsets only + class GroupFieldsetsModelAdmin(admin.TranslationAdmin): + group_fieldsets = True + ma = GroupFieldsetsModelAdmin(models.GroupFieldsetsModel, self.site) + # Only text and title are registered for translation. We expect to get + # three fieldsets. The first which gathers all untranslated field + # (email only) and one for each translation field (text and title). + fieldsets = [ + ('', {'fields': ['email']}), + ('title', {'classes': ('mt-fieldset',), 'fields': ['title_de', 'title_en']}), + ('text', {'classes': ('mt-fieldset',), 'fields': ['text_de', 'text_en']}), + ] + self.assertEqual(ma.get_fieldsets(request), fieldsets) + self.assertEqual(ma.get_fieldsets(request, self.test_obj), fieldsets) + + # Verify that other options are still taken into account + + # Exclude an untranslated field + class GroupFieldsetsModelAdmin(admin.TranslationAdmin): + group_fieldsets = True + exclude = ('email',) + ma = GroupFieldsetsModelAdmin(models.GroupFieldsetsModel, self.site) + fieldsets = [ + ('title', {'classes': ('mt-fieldset',), 'fields': ['title_de', 'title_en']}), + ('text', {'classes': ('mt-fieldset',), 'fields': ['text_de', 'text_en']}), + ] + self.assertEqual(ma.get_fieldsets(request), fieldsets) + self.assertEqual(ma.get_fieldsets(request, self.test_obj), fieldsets) + + # Exclude a translation field + class GroupFieldsetsModelAdmin(admin.TranslationAdmin): + group_fieldsets = True + exclude = ('text',) + ma = GroupFieldsetsModelAdmin(models.GroupFieldsetsModel, self.site) + fieldsets = [ + ('', {'fields': ['email']}), + ('title', {'classes': ('mt-fieldset',), 'fields': ['title_de', 'title_en']}) + ] + self.assertEqual(ma.get_fieldsets(request), fieldsets) + self.assertEqual(ma.get_fieldsets(request, self.test_obj), fieldsets) + + def test_prepopulated_fields(self): + trans_real.activate('de') + self.assertEqual(get_language(), 'de') + + # Non-translated slug based on translated field (using active language) + class NameModelAdmin(admin.TranslationAdmin): + prepopulated_fields = {'slug': ('firstname',)} + ma = NameModelAdmin(models.NameModel, self.site) + self.assertEqual(ma.prepopulated_fields, {'slug': ('firstname_de',)}) + + # Checking multi-field + class NameModelAdmin(admin.TranslationAdmin): + prepopulated_fields = {'slug': ('firstname', 'lastname',)} + ma = NameModelAdmin(models.NameModel, self.site) + self.assertEqual(ma.prepopulated_fields, {'slug': ('firstname_de', 'lastname_de',)}) + + # Non-translated slug based on non-translated field (no change) + class NameModelAdmin(admin.TranslationAdmin): + prepopulated_fields = {'slug': ('age',)} + ma = NameModelAdmin(models.NameModel, self.site) + self.assertEqual(ma.prepopulated_fields, {'slug': ('age',)}) + + # Translated slug based on non-translated field (all populated on the same value) + class NameModelAdmin(admin.TranslationAdmin): + prepopulated_fields = {'slug2': ('age',)} + ma = NameModelAdmin(models.NameModel, self.site) + self.assertEqual(ma.prepopulated_fields, {'slug2_en': ('age',), 'slug2_de': ('age',)}) + + # Translated slug based on translated field (corresponding) + class NameModelAdmin(admin.TranslationAdmin): + prepopulated_fields = {'slug2': ('firstname',)} + ma = NameModelAdmin(models.NameModel, self.site) + self.assertEqual(ma.prepopulated_fields, {'slug2_en': ('firstname_en',), + 'slug2_de': ('firstname_de',)}) + + # Check that current active language is used + trans_real.activate('en') + self.assertEqual(get_language(), 'en') + + class NameModelAdmin(admin.TranslationAdmin): + prepopulated_fields = {'slug': ('firstname',)} + ma = NameModelAdmin(models.NameModel, self.site) + self.assertEqual(ma.prepopulated_fields, {'slug': ('firstname_en',)}) + + # Prepopulation language can be overriden by MODELTRANSLATION_PREPOPULATE_LANGUAGE + with reload_override_settings(MODELTRANSLATION_PREPOPULATE_LANGUAGE='de'): + class NameModelAdmin(admin.TranslationAdmin): + prepopulated_fields = {'slug': ('firstname',)} + ma = NameModelAdmin(models.NameModel, self.site) + self.assertEqual(ma.prepopulated_fields, {'slug': ('firstname_de',)}) + + def test_proxymodel_field_argument(self): + class ProxyTestModelAdmin(admin.TranslationAdmin): + fields = ['title'] + + ma = ProxyTestModelAdmin(models.ProxyTestModel, self.site) + fields = ['title_de', 'title_en'] + self.assertEqual(tuple(ma.get_form(request).base_fields.keys()), tuple(fields)) + self.assertEqual( + tuple(ma.get_form(request, self.test_obj).base_fields.keys()), tuple(fields)) + + +class ThirdPartyAppIntegrationTest(ModeltranslationTestBase): + """ + This test case and a test case below have identical tests. The models they test have the same + definition - but in this case the model is not registered for translation and in the other + case it is. + """ + registered = False + + @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(ThirdPartyAppIntegrationTest, cls).setUpClass() + cls.model = models.ThirdPartyModel + + def test_form(self): + class CreationForm(forms.ModelForm): + class Meta: + model = self.model + + creation_form = CreationForm({'name': 'abc'}) + inst = creation_form.save() + self.assertEqual('de', get_language()) + self.assertEqual('abc', inst.name) + self.assertEqual(1, self.model.objects.count()) + + +class ThirdPartyAppIntegrationRegisteredTest(ThirdPartyAppIntegrationTest): + registered = True + + @classmethod + def setUpClass(cls): + super(ThirdPartyAppIntegrationRegisteredTest, cls).setUpClass() + cls.model = models.ThirdPartyRegisteredModel + + +class TestManager(ModeltranslationTestBase): + def setUp(self): + # In this test case the default language is en, not de. + super(TestManager, self).setUp() + trans_real.activate('en') + + def test_filter_update(self): + """Test if filtering and updating is language-aware.""" + n = models.ManagerTestModel(title='') + n.title_en = 'en' + n.title_de = 'de' + n.save() + + m = models.ManagerTestModel(title='') + m.title_en = 'title en' + m.title_de = 'de' + m.save() + + self.assertEqual('en', get_language()) + + self.assertEqual(0, models.ManagerTestModel.objects.filter(title='de').count()) + self.assertEqual(1, models.ManagerTestModel.objects.filter(title='en').count()) + # Spanning works + self.assertEqual(2, models.ManagerTestModel.objects.filter(title__contains='en').count()) + + with override('de'): + self.assertEqual(2, models.ManagerTestModel.objects.filter(title='de').count()) + self.assertEqual(0, models.ManagerTestModel.objects.filter(title='en').count()) + # Spanning works + self.assertEqual(2, models.ManagerTestModel.objects.filter(title__endswith='e').count()) + + # Still possible to use explicit language version + self.assertEqual(1, models.ManagerTestModel.objects.filter(title_en='en').count()) + self.assertEqual(2, models.ManagerTestModel.objects.filter( + title_en__contains='en').count()) + + models.ManagerTestModel.objects.update(title='new') + self.assertEqual(2, models.ManagerTestModel.objects.filter(title='new').count()) + n = models.ManagerTestModel.objects.get(pk=n.pk) + m = models.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 = models.ManagerTestModel(title='') + n.title_en = 'en' + n.title_de = 'de' + n.save() + + self.assertEqual('en', get_language()) + self.assertEqual(0, models.ManagerTestModel.objects.filter(Q(title='de') + | Q(pk=42)).count()) + self.assertEqual(1, models.ManagerTestModel.objects.filter(Q(title='en') + | Q(pk=42)).count()) + + with override('de'): + self.assertEqual(1, models.ManagerTestModel.objects.filter(Q(title='de') + | Q(pk=42)).count()) + self.assertEqual(0, models.ManagerTestModel.objects.filter(Q(title='en') + | Q(pk=42)).count()) + + def test_f(self): + """Test if F queries are rewritten.""" + n = models.ManagerTestModel.objects.create(visits_en=1, visits_de=2) + + self.assertEqual('en', get_language()) + models.ManagerTestModel.objects.update(visits=F('visits') + 10) + n = models.ManagerTestModel.objects.all()[0] + self.assertEqual(n.visits_en, 11) + self.assertEqual(n.visits_de, 2) + + with override('de'): + models.ManagerTestModel.objects.update(visits=F('visits') + 20) + n = models.ManagerTestModel.objects.all()[0] + self.assertEqual(n.visits_en, 11) + self.assertEqual(n.visits_de, 22) + + def test_order_by(self): + """Check that field names are rewritten in order_by keys.""" + manager = models.ManagerTestModel.objects + manager.create(title='a') + m = manager.create(title='b') + manager.create(title='c') + with override('de'): + # Make the order of the 'title' column different. + m.title = 'd' + m.save() + titles_asc = tuple(m.title for m in manager.order_by('title')) + titles_desc = tuple(m.title for m in manager.order_by('-title')) + self.assertEqual(titles_asc, ('a', 'b', 'c')) + self.assertEqual(titles_desc, ('c', 'b', 'a')) + + def test_order_by_meta(self): + """Check that meta ordering is rewritten.""" + manager = models.ManagerTestModel.objects + manager.create(title='more_de', visits_en=1, visits_de=2) + manager.create(title='more_en', visits_en=2, visits_de=1) + manager.create(title='most', visits_en=3, visits_de=3) + manager.create(title='least', visits_en=0, visits_de=0) + + # Ordering descending with visits_en + titles_for_en = tuple(m.title_en for m in manager.all()) + with override('de'): + # Ordering descending with visits_de + titles_for_de = tuple(m.title_en for m in manager.all()) + + self.assertEqual(titles_for_en, ('most', 'more_en', 'more_de', 'least')) + self.assertEqual(titles_for_de, ('most', 'more_de', 'more_en', 'least')) + + def test_values(self): + manager = models.ManagerTestModel.objects + manager.create(title_en='en', title_de='de') + + raw_obj = manager.raw_values('title')[0] + obj = manager.values('title')[0] + with override('de'): + raw_obj2 = manager.raw_values('title')[0] + obj2 = manager.values('title')[0] + + # Raw_values returns real database values regardless of current language + self.assertEqual(raw_obj['title'], raw_obj2['title']) + # Values present language-aware data, from the moment of retrieval + self.assertEqual(obj['title'], 'en') + self.assertEqual(obj2['title'], 'de') + + # Values_list behave similarly + self.assertEqual(list(manager.values_list('title', flat=True)), ['en']) + with override('de'): + self.assertEqual(list(manager.values_list('title', flat=True)), ['de']) + + # One can always turn rewrite off + a = list(manager.rewrite(False).values_list('title', flat=True)) + with override('de'): + b = list(manager.rewrite(False).values_list('title', flat=True)) + self.assertEqual(a, b) + + def test_custom_manager(self): + """Test if user-defined manager is still working""" + n = models.CustomManagerTestModel(title='') + n.title_en = 'enigma' + n.title_de = 'foo' + n.save() + + m = models.CustomManagerTestModel(title='') + m.title_en = 'enigma' + m.title_de = 'bar' + m.save() + + # Custom method + self.assertEqual('bar', models.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, models.CustomManagerTestModel.objects.count()) + with override('de'): + self.assertEqual(1, models.CustomManagerTestModel.objects.count()) + + def test_non_objects_manager(self): + """Test if managers other than ``objects`` are patched too""" + from modeltranslation.manager import MultilingualManager + manager = models.CustomManagerTestModel.another_mgr_name + self.assertTrue(isinstance(manager, MultilingualManager)) + + def test_custom_manager2(self): + """Test if user-defined queryset is still working""" + from modeltranslation.manager import MultilingualManager, MultilingualQuerySet + manager = models.CustomManager2TestModel.objects + self.assertTrue(isinstance(manager, models.CustomManager2)) + self.assertTrue(isinstance(manager, MultilingualManager)) + qs = manager.all() + self.assertTrue(isinstance(qs, models.CustomQuerySet)) + self.assertTrue(isinstance(qs, MultilingualQuerySet)) + + def test_creation(self): + """Test if field are rewritten in create.""" + self.assertEqual('en', get_language()) + n = models.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 = models.ManagerTestModel.objects.create(title_en='foo') + self.assertEqual('foo', n.title_en) + self.assertEqual(None, n.title_de) + self.assertEqual('foo', n.title) + + # Language suffixed version wins + n = models.ManagerTestModel.objects.create(title='bar', 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 = 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.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.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 + with override('de'): + self.assertEqual('foo', n.title) # points to de + self.assertEqual('en', get_language()) + + # 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 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 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.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) + + def test_fixture_population_via_command(self): + """ + Test that the loaddata command takes new option. + """ + call_command('loaddata', 'fixture.json', verbosity=0, commit=False, populate='required') + 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) + + call_command('loaddata', 'fixture.json', verbosity=0, commit=False, populate='all') + 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, 'bar') + + # Test if option overrides current context + with auto_populate('all'): + call_command('loaddata', 'fixture.json', verbosity=0, commit=False, populate=False) + m = models.TestModel.objects.get() + self.assertEqual(m.title_en, 'foo') + self.assertEqual(m.title_de, None) + self.assertEqual(m.text_en, 'bar') + self.assertEqual(m.text_de, None) + + def assertDeferred(self, use_defer, *fields): + manager = models.TestModel.objects.defer if use_defer else models.TestModel.objects.only + inst1 = manager(*fields)[0] + with override('de'): + inst2 = manager(*fields)[0] + self.assertEqual('title_en', inst1.title) + self.assertEqual('title_en', inst2.title) + with override('de'): + self.assertEqual('title_de', inst1.title) + self.assertEqual('title_de', inst2.title) + + def test_deferred(self): + """ + Check if ``only`` and ``defer`` are working. + """ + models.TestModel.objects.create(title_de='title_de', title_en='title_en') + inst = models.TestModel.objects.only('title_en')[0] + self.assertNotEqual(inst.__class__, models.TestModel) + self.assertTrue(isinstance(inst, models.TestModel)) + self.assertDeferred(False, 'title_en') + + with auto_populate('all'): + self.assertDeferred(False, 'title') + self.assertDeferred(False, 'title_de') + self.assertDeferred(False, 'title_en') + self.assertDeferred(False, 'title_en', 'title_de') + self.assertDeferred(False, 'title', 'title_en') + self.assertDeferred(False, 'title', 'title_de') + # Check if fields are deferred properly with ``only`` + self.assertDeferred(False, 'text') + + # Defer + self.assertDeferred(True, 'title') + self.assertDeferred(True, 'title_de') + self.assertDeferred(True, 'title_en') + self.assertDeferred(True, 'title_en', 'title_de') + self.assertDeferred(True, 'title', 'title_en') + self.assertDeferred(True, 'title', 'title_de') + self.assertDeferred(True, 'text', 'email', 'url') + + def test_constructor_inheritance(self): + inst = models.AbstractModelB() + # Check if fields assigned in constructor hasn't been ignored. + self.assertEqual(inst.titlea, 'title_a') + self.assertEqual(inst.titleb, 'title_b') + + +class TranslationModelFormTest(ModeltranslationTestBase): + def test_fields(self): + class TestModelForm(TranslationModelForm): + class Meta: + model = models.TestModel + + form = TestModelForm() + self.assertEqual(list(form.base_fields), + ['title', 'title_de', 'title_en', 'text', 'text_de', 'text_en', + 'url', 'url_de', 'url_en', 'email', 'email_de', 'email_en']) + self.assertEqual(list(form.fields), ['title', 'text', 'url', 'email']) + + def test_updating_with_empty_value(self): + """ + Can we update the current language translation with an empty value, when + the original field is excluded from the form? + """ + class Form(forms.ModelForm): + class Meta: + model = models.TestModel + exclude = ('text',) + + instance = models.TestModel.objects.create(text_de='something') + form = Form({'text_de': '', 'title': 'a', 'email_de': '', 'email_en': ''}, + instance=instance) + instance = form.save() + self.assertEqual('de', get_language()) + self.assertEqual('', instance.text_de) + + +class ProxyModelTest(ModeltranslationTestBase): + def test_equality(self): + n = models.TestModel.objects.create(title='Title') + m = models.ProxyTestModel.objects.get(title='Title') + self.assertEqual(n.title, m.title) + self.assertEqual(n.title_de, m.title_de) + self.assertEqual(n.title_en, m.title_en) diff --git a/runtests.py b/runtests.py index a4036bfb..6236ded4 100755 --- a/runtests.py +++ b/runtests.py @@ -45,8 +45,7 @@ def runtests(): ) failures = call_command( - 'test', 'modeltranslation', interactive=False, failfast=False, - verbosity=2) + 'test', 'modeltranslation', interactive=False, failfast=False, verbosity=2) sys.exit(bool(failures)) From a15b6f894956359166fe43c15817bc93f1ff735e Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Sun, 19 Jan 2014 22:30:31 +0100 Subject: [PATCH 003/170] Fixes for flake8. --- modeltranslation/tests/__init__.py | 2 +- modeltranslation/translator.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/modeltranslation/tests/__init__.py b/modeltranslation/tests/__init__.py index a49dea43..47945aff 100644 --- a/modeltranslation/tests/__init__.py +++ b/modeltranslation/tests/__init__.py @@ -1,2 +1,2 @@ # For Django < 1.6 testrunner -from .tests import * \ No newline at end of file +from .tests import * # NOQA diff --git a/modeltranslation/translator.py b/modeltranslation/translator.py index 61c0d7fe..577887b5 100644 --- a/modeltranslation/translator.py +++ b/modeltranslation/translator.py @@ -3,7 +3,6 @@ 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, From ef6a8b2e81ca14438814136a30b400e68decf108 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francesc=20Arp=C3=AD=20Roca?= Date: Wed, 22 Jan 2014 17:23:02 +0100 Subject: [PATCH 004/170] Remove the http protocol. So we don't get warning about not secure elements --- modeltranslation/admin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modeltranslation/admin.py b/modeltranslation/admin.py index 88240e35..59030909 100644 --- a/modeltranslation/admin.py +++ b/modeltranslation/admin.py @@ -345,7 +345,7 @@ class TabbedDjangoJqueryTranslationAdmin(TranslationAdmin): class Media: js = ( 'modeltranslation/js/force_jquery.js', - 'http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.2/jquery-ui.min.js', + '//ajax.googleapis.com/ajax/libs/jqueryui/1.8.2/jquery-ui.min.js', 'modeltranslation/js/tabbed_translation_fields.js', ) css = { @@ -357,8 +357,8 @@ class Media: class TabbedExternalJqueryTranslationAdmin(TranslationAdmin): class Media: js = ( - 'http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js', - 'http://ajax.googleapis.com/ajax/libs/jqueryui/1.10.2/jquery-ui.min.js', + '//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js', + '//ajax.googleapis.com/ajax/libs/jqueryui/1.10.2/jquery-ui.min.js', 'modeltranslation/js/tabbed_translation_fields.js', ) css = { From 3ffb0ce0a657cf55ffb741e8a7e74b8bd284fcd4 Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Wed, 22 Jan 2014 22:32:59 +0100 Subject: [PATCH 005/170] Add MODELTRANSLATION_LANGUAGES setting. --- docs/modeltranslation/installation.rst | 33 +++++++++++++++++++++++++- modeltranslation/settings.py | 3 ++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/docs/modeltranslation/installation.rst b/docs/modeltranslation/installation.rst index 64d91702..66aad1c1 100644 --- a/docs/modeltranslation/installation.rst +++ b/docs/modeltranslation/installation.rst @@ -116,9 +116,18 @@ and ``en`` in your project, set the ``LANGUAGES`` variable like this (where rather required for Django to be able to (statically) translate the verbose names of the languages using the standard ``i18n`` solution. +.. note:: + If, for some reason, you don't want to translate objects to exactly the same languages as + the site would be displayed into, you can set ``MODELTRANSLATION_LANGUAGES`` (see below). + For any language in ``LANGUAGES`` not present in ``MODELTRANSLATION_LANGUAGES``, the *default + language* will be used when accessing translated content. For any language in + ``MODELTRANSLATION_LANGUAGES`` not present in ``LANGUAGES``, probably nobody will see translated + content, since the site wouldn't be accessible in that language. + .. warning:: Modeltranslation does not enforce the ``LANGUAGES`` setting to be defined - in your project. When it isn't present, it defaults to Django's + in your project. When it isn't present (and neither is ``MODELTRANSLATION_LANGUAGES``), it + defaults to Django's `global LANGUAGES setting `_ instead, and that are quite a number of languages! @@ -147,6 +156,28 @@ Example:: MODELTRANSLATION_DEFAULT_LANGUAGE = 'en' +``MODELTRANSLATION_LANGUAGES`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. versionadded:: 0.8 + +Default: same as ``LANGUAGES`` + +Allow to set languages the content will be translated into. If not set, by default all +languages listed in ``LANGUAGES`` will be used. + +Example:: + + LANGUAGES = ( + ('en', 'English'), + ('de', 'German'), + ('pl', 'Polish'), + ) + MODELTRANSLATION_LANGUAGES = ('en', 'de') + +.. note:: We doubt this setting will ever be needed, but why not add it since we can? + + .. _settings-modeltranslation_fallback_languages: ``MODELTRANSLATION_FALLBACK_LANGUAGES`` diff --git a/modeltranslation/settings.py b/modeltranslation/settings.py index 68b3997b..95325ce3 100644 --- a/modeltranslation/settings.py +++ b/modeltranslation/settings.py @@ -5,7 +5,8 @@ TRANSLATION_FILES = tuple(getattr(settings, 'MODELTRANSLATION_TRANSLATION_FILES', ())) -AVAILABLE_LANGUAGES = [l[0] for l in settings.LANGUAGES] +AVAILABLE_LANGUAGES = getattr(settings, 'MODELTRANSLATION_LANGUAGES', + [l[0] for l in settings.LANGUAGES]) DEFAULT_LANGUAGE = getattr(settings, 'MODELTRANSLATION_DEFAULT_LANGUAGE', None) if DEFAULT_LANGUAGE and DEFAULT_LANGUAGE not in AVAILABLE_LANGUAGES: raise ImproperlyConfigured('MODELTRANSLATION_DEFAULT_LANGUAGE not in LANGUAGES setting.') From a2c6f75efe367008ba2c00ec5eb12e100672ebf8 Mon Sep 17 00:00:00 2001 From: Mathieu Leplatre Date: Tue, 4 Feb 2014 16:57:45 +0100 Subject: [PATCH 006/170] Take db_column into account while syncing fields --- .../management/commands/sync_translation_fields.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/modeltranslation/management/commands/sync_translation_fields.py b/modeltranslation/management/commands/sync_translation_fields.py index ea309f65..ce4d50bf 100644 --- a/modeltranslation/management/commands/sync_translation_fields.py +++ b/modeltranslation/management/commands/sync_translation_fields.py @@ -69,8 +69,11 @@ def handle_noargs(self, **options): db_table = model._meta.db_table model_full_name = '%s.%s' % (model._meta.app_label, model._meta.module_name) opts = translator.get_options_for_model(model) - for field_name in opts.local_fields.keys(): - missing_langs = list(self.get_missing_languages(field_name, db_table)) + for field_name, fields in opts.local_fields.items(): + # Take `db_column` attribute into account + field = list(fields)[0] + column_name = field.db_column if field.db_column else field_name + missing_langs = list(self.get_missing_languages(column_name, db_table)) if missing_langs: found_missing_fields = True print_missing_langs(missing_langs, field_name, model_full_name) From ebb7a3cab4219cb0f213162c352c02f7a4b17e70 Mon Sep 17 00:00:00 2001 From: Mathieu Leplatre Date: Wed, 5 Feb 2014 17:21:52 +0100 Subject: [PATCH 007/170] Describe use-case for MODELTRANSLATION_LANGUAGES --- docs/modeltranslation/installation.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/modeltranslation/installation.rst b/docs/modeltranslation/installation.rst index 66aad1c1..88237a42 100644 --- a/docs/modeltranslation/installation.rst +++ b/docs/modeltranslation/installation.rst @@ -175,7 +175,9 @@ Example:: ) MODELTRANSLATION_LANGUAGES = ('en', 'de') -.. note:: We doubt this setting will ever be needed, but why not add it since we can? +.. note:: + This setting may become useful if your users shall produce content for a restricted + set of languages, while your application is translated into a greater number of locales. .. _settings-modeltranslation_fallback_languages: From bb29a72b458f408872aa3deba8bcf419e388e17e Mon Sep 17 00:00:00 2001 From: Mathieu Leplatre Date: Wed, 5 Feb 2014 17:44:16 +0100 Subject: [PATCH 008/170] Use AVAILABLE_LANGUAGES do detect missing fields --- .../management/commands/sync_translation_fields.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modeltranslation/management/commands/sync_translation_fields.py b/modeltranslation/management/commands/sync_translation_fields.py index ea309f65..ce6a0497 100644 --- a/modeltranslation/management/commands/sync_translation_fields.py +++ b/modeltranslation/management/commands/sync_translation_fields.py @@ -16,6 +16,7 @@ from django.db import connection, transaction from django.utils.six import moves +from modeltranslation.settings import AVAILABLE_LANGUAGES from modeltranslation.translator import translator from modeltranslation.utils import build_localized_fieldname @@ -102,7 +103,7 @@ def get_missing_languages(self, field_name, db_table): Gets only missings fields. """ db_table_fields = self.get_table_fields(db_table) - for lang_code, lang_name in settings.LANGUAGES: + for lang_code in AVAILABLE_LANGUAGES: if build_localized_fieldname(field_name, lang_code) not in db_table_fields: yield lang_code From fd5a8cb6da6fef879b76c080ed077dab99cd1eb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francesc=20Arp=C3=AD=20Roca?= Date: Mon, 10 Feb 2014 11:05:24 +0100 Subject: [PATCH 009/170] Modify javascript to also consider the iframes. --- .../static/modeltranslation/js/tabbed_translation_fields.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modeltranslation/static/modeltranslation/js/tabbed_translation_fields.js b/modeltranslation/static/modeltranslation/js/tabbed_translation_fields.js index 50d7a6e9..8fe9928f 100644 --- a/modeltranslation/static/modeltranslation/js/tabbed_translation_fields.js +++ b/modeltranslation/static/modeltranslation/js/tabbed_translation_fields.js @@ -391,7 +391,7 @@ var google, django, gettext; // Group normal fields and fields in (existing) stacked inlines var grouper = new TranslationFieldGrouper({ $fields: $('.mt').filter( - 'input:visible, textarea:visible, select:visible').filter(':parents(.tabular)') + 'input:visible, textarea:visible, select:visible, iframe').filter(':parents(.tabular)') }); MainSwitch.init(grouper.groupedTranslations, createTabs(grouper.groupedTranslations)); From fb8155a4a55c5175c0fcda2ec1220aad5d3fdb86 Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Tue, 25 Feb 2014 22:06:55 +0100 Subject: [PATCH 010/170] Fix exclude for nullable field Manager Rewriting (#231). --- modeltranslation/manager.py | 24 ++++++++++++++++++++++++ modeltranslation/tests/models.py | 3 ++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/modeltranslation/manager.py b/modeltranslation/manager.py index cf5922b3..69a30261 100644 --- a/modeltranslation/manager.py +++ b/modeltranslation/manager.py @@ -6,6 +6,7 @@ https://github.com/zmathew/django-linguo """ from django.db import models +from django.db.models import FieldDoesNotExist from django.db.models.fields.related import RelatedField, RelatedObject from django.db.models.sql.where import Constraint from django.utils.tree import Node @@ -71,6 +72,27 @@ def get_fields_to_translatable_models(model): _F2TM_CACHE[model] = results return _F2TM_CACHE[model] +_C2F_CACHE = {} + + +def get_field_by_colum_name(model, col): + # First, try field with the column name + try: + field = model._meta.get_field(col) + if field.column == col: + return field + except FieldDoesNotExist: + pass + field = _C2F_CACHE.get((model, col), None) + if field: + return field + # D'oh, need to search through all of them. + for field in model._meta.fields: + if field.column == col: + _C2F_CACHE[(model, col)] = field + return field + assert False, "No field found for column %s" % col + class MultilingualQuerySet(models.query.QuerySet): def __init__(self, *args, **kwargs): @@ -126,6 +148,8 @@ def _rewrite_where(self, q): """ if isinstance(q, tuple) and isinstance(q[0], Constraint): c = q[0] + if c.field is None: + c.field = get_field_by_colum_name(self.model, c.col) new_name = rewrite_lookup_key(self.model, c.field.name) if c.field.name != new_name: c.field = self.model._meta.get_field(new_name) diff --git a/modeltranslation/tests/models.py b/modeltranslation/tests/models.py index fd0ab83c..d78cc7a5 100644 --- a/modeltranslation/tests/models.py +++ b/modeltranslation/tests/models.py @@ -262,7 +262,7 @@ class Meta: class CustomManager(models.Manager): def get_query_set(self): - return super(CustomManager, self).get_query_set().filter(title__contains='a') + return super(CustomManager, self).get_query_set().filter(title__contains='a').exclude(description__contains='x') def foo(self): return 'bar' @@ -270,6 +270,7 @@ def foo(self): class CustomManagerTestModel(models.Model): title = models.CharField(ugettext_lazy('title'), max_length=255) + description = models.CharField(max_length=255, null=True, db_column='xyz') objects = CustomManager() another_mgr_name = CustomManager() From b3137934901a6f2a943d3cd99cbb67fa8db00122 Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Thu, 13 Mar 2014 19:51:57 +0100 Subject: [PATCH 011/170] Use _default_manager instead of objects; patch _default_manager as well. --- .../management/commands/update_translation_fields.py | 2 +- modeltranslation/translator.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/modeltranslation/management/commands/update_translation_fields.py b/modeltranslation/management/commands/update_translation_fields.py index 86f51ecb..76bfc68f 100644 --- a/modeltranslation/management/commands/update_translation_fields.py +++ b/modeltranslation/management/commands/update_translation_fields.py @@ -29,5 +29,5 @@ def handle_noargs(self, **options): if field.empty_strings_allowed: q |= Q(**{def_lang_fieldname: ""}) - model.objects.filter(q).rewrite(False).update( + model._default_manager.filter(q).rewrite(False).update( **{def_lang_fieldname: F(field_name)}) diff --git a/modeltranslation/translator.py b/modeltranslation/translator.py index 577887b5..c96aeb54 100644 --- a/modeltranslation/translator.py +++ b/modeltranslation/translator.py @@ -141,12 +141,20 @@ def add_manager(model): current_manager = getattr(model, attname) if isinstance(current_manager, MultilingualManager): continue + prev_class = current_manager.__class__ if current_manager.__class__ is Manager: current_manager.__class__ = MultilingualManager else: class NewMultilingualManager(MultilingualManager, current_manager.__class__): pass current_manager.__class__ = NewMultilingualManager + if model._default_manager.__class__ is prev_class: + # Normally model._default_manager is a reference to one of model's managers + # (and would be patched by the way). + # However, in some rare situations (mostly proxy models) + # model._default_manager is not the same instance as one of managers, but it + # share the same class. + model._default_manager.__class__ = current_manager.__class__ def patch_constructor(model): From 85ddf84b8f18a73e4feb90297bc75369e214c468 Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Thu, 13 Mar 2014 19:55:49 +0100 Subject: [PATCH 012/170] PEP8 fix. --- modeltranslation/tests/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modeltranslation/tests/models.py b/modeltranslation/tests/models.py index d78cc7a5..56b96c7a 100644 --- a/modeltranslation/tests/models.py +++ b/modeltranslation/tests/models.py @@ -262,7 +262,8 @@ class Meta: class CustomManager(models.Manager): def get_query_set(self): - return super(CustomManager, self).get_query_set().filter(title__contains='a').exclude(description__contains='x') + return (super(CustomManager, self).get_query_set().filter(title__contains='a') + .exclude(description__contains='x')) def foo(self): return 'bar' From cbca2201031cd149f52bcb68df1eb71212f54a6b Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Thu, 13 Mar 2014 20:02:22 +0100 Subject: [PATCH 013/170] Capitalize group_fieldsets titles (close #234). --- modeltranslation/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modeltranslation/admin.py b/modeltranslation/admin.py index 59030909..bbbf5b9e 100644 --- a/modeltranslation/admin.py +++ b/modeltranslation/admin.py @@ -284,7 +284,7 @@ def _group_fieldsets(self, fieldsets): # Extract the original field's verbose_name for use as this # fieldset's label - using ugettext_lazy in your model # declaration can make that translatable. - label = self.model._meta.get_field(orig_field).verbose_name + label = self.model._meta.get_field(orig_field).verbose_name.capitalize() temp_fieldsets[orig_field] = (label, { 'fields': trans_fieldnames, 'classes': ('mt-fieldset',) From b55917303faf72caa9bc112ae45fddc4270cefb5 Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Sat, 15 Mar 2014 14:10:05 +0100 Subject: [PATCH 014/170] Fix tests to reflect changes in group fieldset capitalization (ref #234). --- modeltranslation/tests/tests.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/modeltranslation/tests/tests.py b/modeltranslation/tests/tests.py index ef59e845..0cafcd5b 100644 --- a/modeltranslation/tests/tests.py +++ b/modeltranslation/tests/tests.py @@ -2173,8 +2173,8 @@ class GroupFieldsetsModelAdmin(admin.TranslationAdmin): # (email only) and one for each translation field (text and title). fieldsets = [ ('', {'fields': ['email']}), - ('title', {'classes': ('mt-fieldset',), 'fields': ['title_de', 'title_en']}), - ('text', {'classes': ('mt-fieldset',), 'fields': ['text_de', 'text_en']}), + ('Title', {'classes': ('mt-fieldset',), 'fields': ['title_de', 'title_en']}), + ('Text', {'classes': ('mt-fieldset',), 'fields': ['text_de', 'text_en']}), ] self.assertEqual(ma.get_fieldsets(request), fieldsets) self.assertEqual(ma.get_fieldsets(request, self.test_obj), fieldsets) @@ -2187,8 +2187,8 @@ class GroupFieldsetsModelAdmin(admin.TranslationAdmin): exclude = ('email',) ma = GroupFieldsetsModelAdmin(models.GroupFieldsetsModel, self.site) fieldsets = [ - ('title', {'classes': ('mt-fieldset',), 'fields': ['title_de', 'title_en']}), - ('text', {'classes': ('mt-fieldset',), 'fields': ['text_de', 'text_en']}), + ('Title', {'classes': ('mt-fieldset',), 'fields': ['title_de', 'title_en']}), + ('Text', {'classes': ('mt-fieldset',), 'fields': ['text_de', 'text_en']}), ] self.assertEqual(ma.get_fieldsets(request), fieldsets) self.assertEqual(ma.get_fieldsets(request, self.test_obj), fieldsets) @@ -2200,7 +2200,7 @@ class GroupFieldsetsModelAdmin(admin.TranslationAdmin): ma = GroupFieldsetsModelAdmin(models.GroupFieldsetsModel, self.site) fieldsets = [ ('', {'fields': ['email']}), - ('title', {'classes': ('mt-fieldset',), 'fields': ['title_de', 'title_en']}) + ('Title', {'classes': ('mt-fieldset',), 'fields': ['title_de', 'title_en']}) ] self.assertEqual(ma.get_fieldsets(request), fieldsets) self.assertEqual(ma.get_fieldsets(request, self.test_obj), fieldsets) From 591e945c337b523b5f3ab3e2aeb26854da262462 Mon Sep 17 00:00:00 2001 From: Dirk Eschler Date: Mon, 18 Feb 2013 17:45:36 +0100 Subject: [PATCH 015/170] Add more control over required languages (close #143). --- docs/modeltranslation/registration.rst | 47 ++++++++++++++++++++++++-- modeltranslation/fields.py | 28 +++++++++++++-- modeltranslation/tests/models.py | 9 +++++ modeltranslation/tests/tests.py | 45 +++++++++++++++++++++++- modeltranslation/tests/translation.py | 14 +++++++- modeltranslation/translator.py | 27 +++++++++++++++ 6 files changed, 162 insertions(+), 8 deletions(-) diff --git a/docs/modeltranslation/registration.rst b/docs/modeltranslation/registration.rst index 3dc5f802..09a7d3bb 100644 --- a/docs/modeltranslation/registration.rst +++ b/docs/modeltranslation/registration.rst @@ -154,15 +154,49 @@ involves copying migration files, using ``SOUTH_MIGRATION_MODULES`` setting, and passing ``--delete-ghost-migrations`` flag, so we don't recommend it. Invoking ``sync_translation_fields`` is plain easier. -Note that all added fields are +Note that all added fields are by default declared ``blank=True`` and ``null=True`` no matter if the original field is -required or not. In other words - all translations are optional. To populate -the default translation fields added by the modeltranslation application +required or not. In other words - all translations are optional, unless an explicit option +is provided - see below. + +To populate the default translation fields added by the modeltranslation application with values from existing database fields, you can use the ``update_translation_fields`` command below. See :ref:`commands-update_translation_fields` for more info on this. +.. _required_langs: + +Required fields +--------------- + +By default, all translation fields are optional (not required). It can be changed using special +attribute on ``TranslationOptions``, though:: + + class NewsTranslationOptions(TranslationOptions): + fields = ('title', 'text',) + required_languages = ('en', 'de') + +It quite self-explanatory: for German and English, all translation fields are required. For other +languages - optional. + +A more fine-grained control is available:: + + class NewsTranslationOptions(TranslationOptions): + fields = ('title', 'text',) + required_languages = {'de': ('title', 'text'), 'default': ('title',)} + +For German, all fields (both ``title`` and ``text``) are required; for all other languages - only +``title`` is required. The ``'default'`` is optional. + +.. note:: + Requirement is enforced by ``blank=False``. Please remember that it will trigger validation only + in modelforms and admin (as always in Django). Manual model validation can be performed via + ``full_clean()`` model method. + + The required fields are still ``null=True``, though. + + ``TranslationOptions`` attributes reference ------------------------------------------- @@ -205,6 +239,13 @@ Classes inheriting from ``TranslationOptions`` can have following attributes def empty_values = '' empty_values = {'title': '', 'slug': None, 'desc': 'both'} +.. attribute:: TranslationOptions.required_languages + + Control which translation fields are required. See :ref:`required_langs`. :: + + required_languages = ('en', 'de') + required_languages = {'de': ('title','text'), 'default': ('title',)} + .. _supported_field_matrix: diff --git a/modeltranslation/fields.py b/modeltranslation/fields.py index 3442fdf8..8189b167 100644 --- a/modeltranslation/fields.py +++ b/modeltranslation/fields.py @@ -98,6 +98,8 @@ class TranslationField(object): that needs to be specified when the field is created. """ def __init__(self, translated_field, language, empty_value, *args, **kwargs): + from modeltranslation.translator import translator + # Update the dict of this field with the content of the original one # This might be a bit radical?! Seems to work though... self.__dict__.update(translated_field.__dict__) @@ -109,15 +111,35 @@ def __init__(self, translated_field, language, empty_value, *args, **kwargs): if empty_value is NONE: self.empty_value = None if translated_field.null else '' - # Translation are always optional (for now - maybe add some parameters - # to the translation options for configuring this) - + # Default behaviour is that all translations are optional if not isinstance(self, fields.BooleanField): # TODO: Do we really want to enforce null *at all*? Shouldn't this # better honour the null setting of the translated field? self.null = True self.blank = True + # Take required_languages translation option into account + trans_opts = translator.get_options_for_model(self.model) + if trans_opts.required_languages: + required_languages = trans_opts.required_languages + if isinstance(trans_opts.required_languages, (tuple, list)): + # All fields + if self.language in required_languages: + # self.null = False + self.blank = False + else: + # Certain fields only + # Try current language - if not present, try 'default' key + try: + req_fields = required_languages[self.language] + except KeyError: + req_fields = required_languages.get('default', ()) + if self.name in req_fields: + # TODO: We might have to handle the whole thing through the + # FieldsAggregationMetaClass, as fields can be inherited. + # self.null = False + self.blank = False + # Adjust the name of this field to reflect the language self.attname = build_localized_fieldname(self.translated_field.name, self.language) self.name = self.attname diff --git a/modeltranslation/tests/models.py b/modeltranslation/tests/models.py index 56b96c7a..0e9c18fc 100644 --- a/modeltranslation/tests/models.py +++ b/modeltranslation/tests/models.py @@ -289,3 +289,12 @@ def get_query_set(self): class CustomManager2TestModel(models.Model): title = models.CharField(ugettext_lazy('title'), max_length=255) objects = CustomManager2() + + +########## Required fields testing + +class RequiredModel(models.Model): + non_req = models.CharField(max_length=10, blank=True) + req = models.CharField(max_length=10) + req_reg = models.CharField(max_length=10) + req_en_reg = models.CharField(max_length=10) diff --git a/modeltranslation/tests/tests.py b/modeltranslation/tests/tests.py index 0cafcd5b..52be7cfc 100644 --- a/modeltranslation/tests/tests.py +++ b/modeltranslation/tests/tests.py @@ -38,7 +38,7 @@ request = None # How many models are registered for tests. -TEST_MODELS = 27 +TEST_MODELS = 28 class reload_override_settings(override_settings): @@ -2709,3 +2709,46 @@ def test_equality(self): self.assertEqual(n.title, m.title) self.assertEqual(n.title_de, m.title_de) self.assertEqual(n.title_en, m.title_en) + + +class TestRequired(ModeltranslationTestBase): + def assertRequired(self, field_name): + self.assertFalse(self.opts.get_field(field_name).blank) + + def assertNotRequired(self, field_name): + self.assertTrue(self.opts.get_field(field_name).blank) + + def test_required(self): + self.opts = models.RequiredModel._meta + + # All non required + self.assertNotRequired('non_req') + self.assertNotRequired('non_req_en') + self.assertNotRequired('non_req_de') + + # Original required, but translated fields not - default behaviour + self.assertRequired('req') + self.assertNotRequired('req_en') + self.assertNotRequired('req_de') + + # Set all translated field required + self.assertRequired('req_reg') + self.assertRequired('req_reg_en') + self.assertRequired('req_reg_de') + + # Set some translated field required + self.assertRequired('req_en_reg') + self.assertRequired('req_en_reg_en') + self.assertNotRequired('req_en_reg_de') + + # Test validation + inst = models.RequiredModel() + inst.req = 'abc' + inst.req_reg = 'def' + try: + inst.full_clean() + except ValidationError as e: + error_fields = set(e.message_dict.keys()) + self.assertEqual(set(('req_reg_en', 'req_en_reg', 'req_en_reg_en')), error_fields) + else: + self.fail('ValidationError not raised!') diff --git a/modeltranslation/tests/translation.py b/modeltranslation/tests/translation.py index c817a899..36fe3c42 100644 --- a/modeltranslation/tests/translation.py +++ b/modeltranslation/tests/translation.py @@ -7,7 +7,8 @@ DescriptorModel, AbstractModelA, AbstractModelB, Slugged, MetaData, Displayable, Page, RichText, RichTextPage, MultitableModelA, MultitableModelB, MultitableModelC, ManagerTestModel, CustomManagerTestModel, CustomManager2TestModel, GroupFieldsetsModel, NameModel, - ThirdPartyRegisteredModel, ProxyTestModel, UniqueNullableModel, OneToOneFieldModel) + ThirdPartyRegisteredModel, ProxyTestModel, UniqueNullableModel, OneToOneFieldModel, + RequiredModel) class TestTranslationOptions(TranslationOptions): @@ -186,3 +187,14 @@ class GroupFieldsetsTranslationOptions(TranslationOptions): class NameTranslationOptions(TranslationOptions): fields = ('firstname', 'lastname', 'slug2') translator.register(NameModel, NameTranslationOptions) + + +########## Required fields testing + +class RequiredTranslationOptions(TranslationOptions): + fields = ('non_req', 'req', 'req_reg', 'req_en_reg') + required_languages = { + 'en': ('req_reg', 'req_en_reg',), + 'default': ('req_reg',), # for all other languages + } +translator.register(RequiredModel, RequiredTranslationOptions) diff --git a/modeltranslation/translator.py b/modeltranslation/translator.py index c96aeb54..0e70946e 100644 --- a/modeltranslation/translator.py +++ b/modeltranslation/translator.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- from django.utils.six import with_metaclass +from django.core.exceptions import ImproperlyConfigured from django.db.models import Manager, ForeignKey, OneToOneField from django.db.models.base import ModelBase from django.db.models.signals import post_init @@ -57,6 +58,7 @@ class TranslationOptions(with_metaclass(FieldsAggregationMetaClass, object)): with translated model. This model may be not translated itself. ``related_fields`` contains names of reverse lookup fields. """ + required_languages = () def __init__(self, model): """ @@ -69,6 +71,28 @@ def __init__(self, model): self.fields = dict((f, set()) for f in self.fields) self.related_fields = [] + def validate(self): + """ + Perform options validation. + """ + # TODO: at the moment only required_languages is validated. + # Maybe check other options as well? + if self.required_languages: + if isinstance(self.required_languages, (tuple, list)): + self._check_languages(self.required_languages) + else: + self._check_languages(self.required_languages.iterkeys(), extra=('default',)) + for fieldnames in self.required_languages.itervalues(): + if any(f not in self.fields for f in fieldnames): + raise ImproperlyConfigured( + 'Fieldname in required_languages which is not in fields option.') + + def _check_languages(self, languages, extra=()): + correct = mt_settings.AVAILABLE_LANGUAGES + list(extra) + if any(l not in correct for l in languages): + raise ImproperlyConfigured( + 'Language in required_languages which is not in AVAILABLE_LANGUAGES.') + def update(self, other): """ Update with options from a superclass. @@ -342,6 +366,9 @@ def register(self, model_or_iterable, opts_class=None, **options): # Find inherited fields and create options instance for the model. opts = self._get_options_for_model(model, opts_class, **options) + # Now, when all fields are initialized and inherited, validate configuration. + opts.validate() + # Mark the object explicitly as registered -- registry caches # options of all models, registered or not. opts.registered = True From 0957067316c8507ea5a1858a593878f062274142 Mon Sep 17 00:00:00 2001 From: Vadim Yalo Date: Tue, 18 Mar 2014 14:49:37 +0300 Subject: [PATCH 016/170] remove bad style In all browsers except Firefox left: -10000px; don't work, so I remove this and add display: none; that work in all browsers. --- .../static/modeltranslation/css/tabbed_translation_fields.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modeltranslation/static/modeltranslation/css/tabbed_translation_fields.css b/modeltranslation/static/modeltranslation/css/tabbed_translation_fields.css index 102fef9d..d21fe7d4 100644 --- a/modeltranslation/static/modeltranslation/css/tabbed_translation_fields.css +++ b/modeltranslation/static/modeltranslation/css/tabbed_translation_fields.css @@ -36,7 +36,7 @@ backward compatibility: .ui-tabs .ui-tabs-panel { display: block; border-width: 0; padding: 1em 1.4em; background: none; } .ui-tabs .ui-tabs-hide { position: absolute; - left: -10000px; + display: none; } /* custom tabs theme */ From 709c993c0743b6a68dbfcde28399b54f534b47f7 Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Tue, 18 Mar 2014 20:31:21 +0100 Subject: [PATCH 017/170] Fix new code to be Python 3 compatible. --- modeltranslation/translator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modeltranslation/translator.py b/modeltranslation/translator.py index 0e70946e..46c60a7e 100644 --- a/modeltranslation/translator.py +++ b/modeltranslation/translator.py @@ -81,8 +81,8 @@ def validate(self): if isinstance(self.required_languages, (tuple, list)): self._check_languages(self.required_languages) else: - self._check_languages(self.required_languages.iterkeys(), extra=('default',)) - for fieldnames in self.required_languages.itervalues(): + self._check_languages(self.required_languages.keys(), extra=('default',)) + for fieldnames in self.required_languages.values(): if any(f not in self.fields for f in fieldnames): raise ImproperlyConfigured( 'Fieldname in required_languages which is not in fields option.') From ae8a3318931e0b0e7f1e19db248a92ae8437af8e Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Sat, 22 Mar 2014 00:12:34 +0100 Subject: [PATCH 018/170] Fix .values() with no fields specified (close #236). --- modeltranslation/manager.py | 5 +++++ modeltranslation/tests/tests.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/modeltranslation/manager.py b/modeltranslation/manager.py index 69a30261..5ffad0d9 100644 --- a/modeltranslation/manager.py +++ b/modeltranslation/manager.py @@ -12,6 +12,7 @@ from django.utils.tree import Node from modeltranslation import settings +from modeltranslation.fields import TranslationField from modeltranslation.utils import (build_localized_fieldname, get_language, auto_populate) @@ -267,6 +268,10 @@ def raw_values(self, *fields): def values(self, *fields): if not self._rewrite: return super(MultilingualQuerySet, self).values(*fields) + if not fields: + # Emulate original queryset behaviour: get all fields that are not translation fields + fields = [f.attname for f in self.model._meta.fields + if not isinstance(f, TranslationField)] new_args = [] for key in fields: new_args.append(rewrite_lookup_key(self.model, key)) diff --git a/modeltranslation/tests/tests.py b/modeltranslation/tests/tests.py index 52be7cfc..328d1351 100644 --- a/modeltranslation/tests/tests.py +++ b/modeltranslation/tests/tests.py @@ -2671,6 +2671,36 @@ def test_constructor_inheritance(self): self.assertEqual(inst.titlea, 'title_a') self.assertEqual(inst.titleb, 'title_b') + def assertDictListEqual(self, l1, l2): + self.assertEqual(len(l1), len(l2)) + for (a, b) in zip(l1, l2): + self.assertDictEqual(a, b) + + def test_values(self): + manager = models.ManagerTestModel.objects + manager.create(title_en='en', title_de='de') + i2 = manager.create(title_en='en2', title_de='de2') + + self.assertEqual('en', get_language()) + self.assertDictListEqual(manager.values('title'), [{'title': 'en'}, {'title': 'en2'}]) + with override('de'): + self.assertDictListEqual(manager.values('title'), [{'title': 'de'}, {'title': 'de2'}]) + + # When no fields are passed, list all fields in current language. + self.assertDictListEqual(manager.values(), [ + {'id': 1, 'title': 'en', 'visits': 0, 'description': None}, + {'id': 2, 'title': 'en2', 'visits': 0, 'description': None} + ]) + + # Raw_values + self.assertDictListEqual(manager.raw_values(), manager.rewrite(False).values()) + i2.delete() + self.assertDictListEqual(manager.raw_values(), [ + {'id': 1, 'title': 'en', 'title_en': 'en', 'title_de': 'de', + 'visits': 0, 'visits_en': 0, 'visits_de': 0, + 'description': None, 'description_en': None, 'description_de': None}, + ]) + class TranslationModelFormTest(ModeltranslationTestBase): def test_fields(self): From cc6a13ddee835f164afeaa058e9c1aba86481e83 Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Sat, 22 Mar 2014 00:27:08 +0100 Subject: [PATCH 019/170] Fix test: there already was a method called test_values.... (ref #263). --- modeltranslation/tests/tests.py | 48 ++++++++++++++++----------------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/modeltranslation/tests/tests.py b/modeltranslation/tests/tests.py index 328d1351..f9dcb269 100644 --- a/modeltranslation/tests/tests.py +++ b/modeltranslation/tests/tests.py @@ -2442,6 +2442,29 @@ def test_values(self): b = list(manager.rewrite(False).values_list('title', flat=True)) self.assertEqual(a, b) + i2 = manager.create(title_en='en2', title_de='de2') + + # This is somehow repetitive... + self.assertEqual('en', get_language()) + self.assertDictListEqual(manager.values('title'), [{'title': 'en'}, {'title': 'en2'}]) + with override('de'): + self.assertDictListEqual(manager.values('title'), [{'title': 'de'}, {'title': 'de2'}]) + + # When no fields are passed, list all fields in current language. + self.assertDictListEqual(manager.values(), [ + {'id': 1, 'title': 'en', 'visits': 0, 'description': None}, + {'id': 2, 'title': 'en2', 'visits': 0, 'description': None} + ]) + + # Raw_values + self.assertDictListEqual(manager.raw_values(), manager.rewrite(False).values()) + i2.delete() + self.assertDictListEqual(manager.raw_values(), [ + {'id': 1, 'title': 'en', 'title_en': 'en', 'title_de': 'de', + 'visits': 0, 'visits_en': 0, 'visits_de': 0, + 'description': None, 'description_en': None, 'description_de': None}, + ]) + def test_custom_manager(self): """Test if user-defined manager is still working""" n = models.CustomManagerTestModel(title='') @@ -2676,31 +2699,6 @@ def assertDictListEqual(self, l1, l2): for (a, b) in zip(l1, l2): self.assertDictEqual(a, b) - def test_values(self): - manager = models.ManagerTestModel.objects - manager.create(title_en='en', title_de='de') - i2 = manager.create(title_en='en2', title_de='de2') - - self.assertEqual('en', get_language()) - self.assertDictListEqual(manager.values('title'), [{'title': 'en'}, {'title': 'en2'}]) - with override('de'): - self.assertDictListEqual(manager.values('title'), [{'title': 'de'}, {'title': 'de2'}]) - - # When no fields are passed, list all fields in current language. - self.assertDictListEqual(manager.values(), [ - {'id': 1, 'title': 'en', 'visits': 0, 'description': None}, - {'id': 2, 'title': 'en2', 'visits': 0, 'description': None} - ]) - - # Raw_values - self.assertDictListEqual(manager.raw_values(), manager.rewrite(False).values()) - i2.delete() - self.assertDictListEqual(manager.raw_values(), [ - {'id': 1, 'title': 'en', 'title_en': 'en', 'title_de': 'de', - 'visits': 0, 'visits_en': 0, 'visits_de': 0, - 'description': None, 'description_en': None, 'description_de': None}, - ]) - class TranslationModelFormTest(ModeltranslationTestBase): def test_fields(self): From 88a89b3a5e41d54cec4c9fcd968d5f04105a0bab Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Sat, 22 Mar 2014 10:25:41 +0100 Subject: [PATCH 020/170] Fix values_list without fields; refactor tests (ref #263). --- modeltranslation/manager.py | 9 +++++++-- modeltranslation/tests/tests.py | 19 +++++++++---------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/modeltranslation/manager.py b/modeltranslation/manager.py index 5ffad0d9..b9af3dfa 100644 --- a/modeltranslation/manager.py +++ b/modeltranslation/manager.py @@ -194,6 +194,9 @@ def _filter_or_exclude(self, negate, *args, **kwargs): kwargs[new_key] = self._rewrite_f(val) return super(MultilingualQuerySet, self)._filter_or_exclude(negate, *args, **kwargs) + def _get_original_fields(self): + return [f.attname for f in self.model._meta.fields if not isinstance(f, TranslationField)] + def order_by(self, *field_names): """ Change translatable field names in an ``order_by`` argument @@ -270,8 +273,7 @@ def values(self, *fields): return super(MultilingualQuerySet, self).values(*fields) if not fields: # Emulate original queryset behaviour: get all fields that are not translation fields - fields = [f.attname for f in self.model._meta.fields - if not isinstance(f, TranslationField)] + fields = self._get_original_fields() new_args = [] for key in fields: new_args.append(rewrite_lookup_key(self.model, key)) @@ -283,6 +285,9 @@ def values(self, *fields): def values_list(self, *fields, **kwargs): if not self._rewrite: return super(MultilingualQuerySet, self).values_list(*fields, **kwargs) + if not fields: + # Emulate original queryset behaviour: get all fields that are not translation fields + fields = self._get_original_fields() new_args = [] for key in fields: new_args.append(rewrite_lookup_key(self.model, key)) diff --git a/modeltranslation/tests/tests.py b/modeltranslation/tests/tests.py index f9dcb269..f1884cd7 100644 --- a/modeltranslation/tests/tests.py +++ b/modeltranslation/tests/tests.py @@ -2446,20 +2446,24 @@ def test_values(self): # This is somehow repetitive... self.assertEqual('en', get_language()) - self.assertDictListEqual(manager.values('title'), [{'title': 'en'}, {'title': 'en2'}]) + self.assertEqual(list(manager.values('title')), [{'title': 'en'}, {'title': 'en2'}]) with override('de'): - self.assertDictListEqual(manager.values('title'), [{'title': 'de'}, {'title': 'de2'}]) + self.assertEqual(list(manager.values('title')), [{'title': 'de'}, {'title': 'de2'}]) # When no fields are passed, list all fields in current language. - self.assertDictListEqual(manager.values(), [ + self.assertEqual(list(manager.values()), [ {'id': 1, 'title': 'en', 'visits': 0, 'description': None}, {'id': 2, 'title': 'en2', 'visits': 0, 'description': None} ]) + # Similar for values_list + self.assertEqual(list(manager.values_list()), [(1, 'en', 0, None), (2, 'en2', 0, None)]) + with override('de'): + self.assertEqual(list(manager.values_list()), [(1, 'de', 0, None), (2, 'de2', 0, None)]) # Raw_values - self.assertDictListEqual(manager.raw_values(), manager.rewrite(False).values()) + self.assertEqual(list(manager.raw_values()), list(manager.rewrite(False).values())) i2.delete() - self.assertDictListEqual(manager.raw_values(), [ + self.assertEqual(list(manager.raw_values()), [ {'id': 1, 'title': 'en', 'title_en': 'en', 'title_de': 'de', 'visits': 0, 'visits_en': 0, 'visits_de': 0, 'description': None, 'description_en': None, 'description_de': None}, @@ -2694,11 +2698,6 @@ def test_constructor_inheritance(self): self.assertEqual(inst.titlea, 'title_a') self.assertEqual(inst.titleb, 'title_b') - def assertDictListEqual(self, l1, l2): - self.assertEqual(len(l1), len(l2)) - for (a, b) in zip(l1, l2): - self.assertDictEqual(a, b) - class TranslationModelFormTest(ModeltranslationTestBase): def test_fields(self): From 52f94cc5880b6a1c1a89e0410edb4a1c62e6bad5 Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Sat, 22 Mar 2014 10:32:07 +0100 Subject: [PATCH 021/170] Repair tests: don't use absolute pk values. --- modeltranslation/tests/tests.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/modeltranslation/tests/tests.py b/modeltranslation/tests/tests.py index f1884cd7..3a0048ae 100644 --- a/modeltranslation/tests/tests.py +++ b/modeltranslation/tests/tests.py @@ -2417,7 +2417,7 @@ def test_order_by_meta(self): def test_values(self): manager = models.ManagerTestModel.objects - manager.create(title_en='en', title_de='de') + id1 = manager.create(title_en='en', title_de='de').pk raw_obj = manager.raw_values('title')[0] obj = manager.values('title')[0] @@ -2443,6 +2443,7 @@ def test_values(self): self.assertEqual(a, b) i2 = manager.create(title_en='en2', title_de='de2') + id2 = i2.pk # This is somehow repetitive... self.assertEqual('en', get_language()) @@ -2452,19 +2453,20 @@ def test_values(self): # When no fields are passed, list all fields in current language. self.assertEqual(list(manager.values()), [ - {'id': 1, 'title': 'en', 'visits': 0, 'description': None}, - {'id': 2, 'title': 'en2', 'visits': 0, 'description': None} + {'id': id1, 'title': 'en', 'visits': 0, 'description': None}, + {'id': id2, 'title': 'en2', 'visits': 0, 'description': None} ]) # Similar for values_list - self.assertEqual(list(manager.values_list()), [(1, 'en', 0, None), (2, 'en2', 0, None)]) + self.assertEqual(list(manager.values_list()), [(id1, 'en', 0, None), (id2, 'en2', 0, None)]) with override('de'): - self.assertEqual(list(manager.values_list()), [(1, 'de', 0, None), (2, 'de2', 0, None)]) + self.assertEqual(list(manager.values_list()), + [(id1, 'de', 0, None), (id2, 'de2', 0, None)]) # Raw_values self.assertEqual(list(manager.raw_values()), list(manager.rewrite(False).values())) i2.delete() self.assertEqual(list(manager.raw_values()), [ - {'id': 1, 'title': 'en', 'title_en': 'en', 'title_de': 'de', + {'id': id1, 'title': 'en', 'title_en': 'en', 'title_de': 'de', 'visits': 0, 'visits_en': 0, 'visits_de': 0, 'description': None, 'description_en': None, 'description_de': None}, ]) From c929131cf252daf34663861379a8c9ad13a0b046 Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Wed, 26 Mar 2014 18:54:20 +0100 Subject: [PATCH 022/170] Small docs update. --- docs/modeltranslation/caveats.rst | 4 ++-- docs/modeltranslation/contribute.rst | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/modeltranslation/caveats.rst b/docs/modeltranslation/caveats.rst index 41d60bd6..ba999d34 100644 --- a/docs/modeltranslation/caveats.rst +++ b/docs/modeltranslation/caveats.rst @@ -24,8 +24,8 @@ Outside a view (or a template), i.e. in normal Python code, a call to the ``get_language`` function still returns a value, but it might not what you expect. Since no request is involved, Django's machinery for discovering the user's preferred language is not activated. For this reason modeltranslation -adds a thin wrapper around the function which guarantees that the returned -language is listed in the ``LANGUAGES`` setting. +adds a thin wrapper (``modeltranslation.utils.get_language``) around the function +which guarantees that the returned language is listed in the ``LANGUAGES`` setting. The unittests use the ``django.utils.translation.trans_real`` functions to activate and deactive a specific language outside a view function. diff --git a/docs/modeltranslation/contribute.rst b/docs/modeltranslation/contribute.rst index 5c463aa9..6b9cc860 100644 --- a/docs/modeltranslation/contribute.rst +++ b/docs/modeltranslation/contribute.rst @@ -43,14 +43,14 @@ to be supported in early development stages of a new Django version, we aim to achieve support once it has seen its first release candidate. The supported Python versions can be derived from the supported Django versions. -Example where we support Python 2.5, 2.6 and 2.7: +Example (from the past) where we support Python 2.5, 2.6 and 2.7: * Django 1.3 (old stable) supports Python 2.5, 2.6, 2.7 * Django 1.4 (current stable) supports Python 2.5, 2.6, 2.7 * Django 1.5 (dev) supports Python 2.6, 2.7 -Python 3 is currently not supported, but should be added no later than it becomes -officially supported by Django. +Python 3 is supported since 0.7 release. Although 0.6 release supported Django 1.5 +(which started Python 3 compliance), it was not Python 3 ready yet. Unittests @@ -73,7 +73,8 @@ Continuous Integration The project uses `Travis CI`_ for continuous integration tests. Hooks provided by Github are active, so that each push and pull request is automatically run -against our `Travis CI config`_. +against our `Travis CI config`_, checking code against different databases, +Python and Django versions. Contributing Documentation From 1d7e0876ecbec36bfecaec0cd72827c5e75f9613 Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Thu, 27 Mar 2014 00:39:33 +0100 Subject: [PATCH 023/170] Satisfy new flake8 version. --- modeltranslation/tests/models.py | 24 ++++++++++----------- modeltranslation/tests/settings.py | 5 ----- modeltranslation/tests/translation.py | 30 +++++++++++++-------------- 3 files changed, 27 insertions(+), 32 deletions(-) diff --git a/modeltranslation/tests/models.py b/modeltranslation/tests/models.py index 0e9c18fc..44c065e3 100644 --- a/modeltranslation/tests/models.py +++ b/modeltranslation/tests/models.py @@ -16,7 +16,7 @@ class UniqueNullableModel(models.Model): title = models.CharField(null=True, unique=True, max_length=255) -########## Proxy model testing +# ######### Proxy model testing class ProxyTestModel(TestModel): class Meta: @@ -26,7 +26,7 @@ def get_title(self): return self.title -########## Fallback values testing +# ######### Fallback values testing class FallbackModel(models.Model): title = models.CharField(ugettext_lazy('title'), max_length=255) @@ -43,7 +43,7 @@ class FallbackModel2(models.Model): email = models.EmailField(blank=True, null=True) -########## File fields testing +# ######### File fields testing class FileFieldsModel(models.Model): title = models.CharField(ugettext_lazy('title'), max_length=255) @@ -52,7 +52,7 @@ class FileFieldsModel(models.Model): image = models.ImageField(upload_to='modeltranslation_tests', null=True, blank=True) -########## Foreign Key / OneToOneField testing +# ######### Foreign Key / OneToOneField testing class NonTranslated(models.Model): title = models.CharField(ugettext_lazy('title'), max_length=255) @@ -74,7 +74,7 @@ class OneToOneFieldModel(models.Model): non = models.OneToOneField(NonTranslated, blank=True, null=True, related_name="test_o2o") -########## Custom fields testing +# ######### Custom fields testing class OtherFieldsModel(models.Model): """ @@ -139,7 +139,7 @@ class DescriptorModel(models.Model): trans = FancyField() -########## Multitable inheritance testing +# ######### Multitable inheritance testing class MultitableModelA(models.Model): titlea = models.CharField(ugettext_lazy('title a'), max_length=255) @@ -157,7 +157,7 @@ class MultitableModelD(MultitableModelB): titled = models.CharField(ugettext_lazy('title d'), max_length=255) -########## Abstract inheritance testing +# ######### Abstract inheritance testing class AbstractModelA(models.Model): titlea = models.CharField(ugettext_lazy('title a'), max_length=255) @@ -178,7 +178,7 @@ def __init__(self, *args, **kwargs): self.titleb = 'title_b' -########## Fields inheritance testing +# ######### Fields inheritance testing class Slugged(models.Model): slug = models.CharField(max_length=255) @@ -219,7 +219,7 @@ class RichTextPage(Page, RichText): pass -########## Admin testing +# ######### Admin testing class DataModel(models.Model): data = models.TextField(blank=True, null=True) @@ -239,7 +239,7 @@ class NameModel(models.Model): slug2 = models.SlugField(max_length=100) -########## Integration testing +# ######### Integration testing class ThirdPartyModel(models.Model): name = models.CharField(max_length=20) @@ -249,7 +249,7 @@ class ThirdPartyRegisteredModel(models.Model): name = models.CharField(max_length=20) -########## Manager testing +# ######### Manager testing class ManagerTestModel(models.Model): title = models.CharField(ugettext_lazy('title'), max_length=255) @@ -291,7 +291,7 @@ class CustomManager2TestModel(models.Model): objects = CustomManager2() -########## Required fields testing +# ######### Required fields testing class RequiredModel(models.Model): non_req = models.CharField(max_length=10, blank=True) diff --git a/modeltranslation/tests/settings.py b/modeltranslation/tests/settings.py index 9f045199..f2066ced 100644 --- a/modeltranslation/tests/settings.py +++ b/modeltranslation/tests/settings.py @@ -8,11 +8,6 @@ INSTALLED_APPS = tuple(settings.INSTALLED_APPS) + ( 'modeltranslation.tests', ) -# IMO this is unimportant -#if django.VERSION[0] >= 1 and django.VERSION[1] >= 3: - #INSTALLED_APPS += ('django.contrib.staticfiles',) - -#STATIC_URL = '/static/' LANGUAGES = (('de', 'Deutsch'), ('en', 'English')) diff --git a/modeltranslation/tests/translation.py b/modeltranslation/tests/translation.py index 36fe3c42..aa3b8a3c 100644 --- a/modeltranslation/tests/translation.py +++ b/modeltranslation/tests/translation.py @@ -22,14 +22,14 @@ class UniqueNullableTranslationOptions(TranslationOptions): translator.register(UniqueNullableModel, UniqueNullableTranslationOptions) -########## Proxy model testing +# ######### Proxy model testing class ProxyTestTranslationOptions(TranslationOptions): fields = ('title', 'text', 'url', 'email',) translator.register(ProxyTestModel, ProxyTestTranslationOptions) -########## Fallback values testing +# ######### Fallback values testing class FallbackModelTranslationOptions(TranslationOptions): fields = ('title', 'text', 'url', 'email', 'description') @@ -44,14 +44,14 @@ class FallbackModel2TranslationOptions(TranslationOptions): translator.register(FallbackModel2, FallbackModel2TranslationOptions) -########## File fields testing +# ######### File fields testing class FileFieldsModelTranslationOptions(TranslationOptions): fields = ('title', 'file', 'file2', 'image',) translator.register(FileFieldsModel, FileFieldsModelTranslationOptions) -########## Foreign Key / OneToOneField testing +# ######### Foreign Key / OneToOneField testing class ForeignKeyModelTranslationOptions(TranslationOptions): fields = ('title', 'test', 'optional', 'hidden', 'non',) @@ -63,11 +63,11 @@ class OneToOneFieldModelTranslationOptions(TranslationOptions): translator.register(OneToOneFieldModel, OneToOneFieldModelTranslationOptions) -########## Custom fields testing +# ######### Custom fields testing class OtherFieldsModelTranslationOptions(TranslationOptions): -# fields = ('int', 'boolean', 'nullboolean', 'csi', 'float', 'decimal', -# 'ip', 'genericip') + # fields = ('int', 'boolean', 'nullboolean', 'csi', 'float', 'decimal', + # 'ip', 'genericip') fields = ('int', 'boolean', 'nullboolean', 'csi', 'float', 'decimal', 'ip', 'date', 'datetime', 'time',) translator.register(OtherFieldsModel, OtherFieldsModelTranslationOptions) @@ -78,7 +78,7 @@ class DescriptorModelTranslationOptions(TranslationOptions): translator.register(DescriptorModel, DescriptorModelTranslationOptions) -########## Multitable inheritance testing +# ######### Multitable inheritance testing class MultitableModelATranslationOptions(TranslationOptions): fields = ('titlea',) @@ -95,7 +95,7 @@ class MultitableModelCTranslationOptions(TranslationOptions): translator.register(MultitableModelC, MultitableModelCTranslationOptions) -########## Abstract inheritance testing +# ######### Abstract inheritance testing class AbstractModelATranslationOptions(TranslationOptions): fields = ('titlea',) @@ -107,7 +107,7 @@ class AbstractModelBTranslationOptions(TranslationOptions): translator.register(AbstractModelB, AbstractModelBTranslationOptions) -########## Fields inheritance testing +# ######### Fields inheritance testing class SluggedTranslationOptions(TranslationOptions): fields = ('slug',) @@ -134,7 +134,7 @@ class PageTranslationOptions(TranslationOptions): translator.register(RichTextPage) -########## Manager testing +# ######### Manager testing class ManagerTestModelTranslationOptions(TranslationOptions): fields = ('title', 'visits', 'description') @@ -147,7 +147,7 @@ class CustomManagerTestModelTranslationOptions(TranslationOptions): CustomManagerTestModelTranslationOptions) -########## TranslationOptions field inheritance testing +# ######### TranslationOptions field inheritance testing class FieldInheritanceATranslationOptions(TranslationOptions): fields = ['titlea'] @@ -170,14 +170,14 @@ class FieldInheritanceETranslationOptions(FieldInheritanceCTranslationOptions, fields = ('titlee',) -########## Integration testing +# ######### Integration testing class ThirdPartyTranslationOptions(TranslationOptions): fields = ('name',) translator.register(ThirdPartyRegisteredModel, ThirdPartyTranslationOptions) -########## Admin testing +# ######### Admin testing class GroupFieldsetsTranslationOptions(TranslationOptions): fields = ('title', 'text',) @@ -189,7 +189,7 @@ class NameTranslationOptions(TranslationOptions): translator.register(NameModel, NameTranslationOptions) -########## Required fields testing +# ######### Required fields testing class RequiredTranslationOptions(TranslationOptions): fields = ('non_req', 'req', 'req_reg', 'req_en_reg') From ee7c24641b4b2e85652da20b6aead85fd644b824 Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Tue, 6 May 2014 21:02:23 +0200 Subject: [PATCH 024/170] Add docs note about forms and models-yet-not-patched (close #243). --- docs/modeltranslation/forms.rst | 3 +++ docs/modeltranslation/registration.rst | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/docs/modeltranslation/forms.rst b/docs/modeltranslation/forms.rst index 64f9224a..6d98190b 100644 --- a/docs/modeltranslation/forms.rst +++ b/docs/modeltranslation/forms.rst @@ -3,6 +3,9 @@ ModelForms ========== +``ModelForms`` for multilanguage models are defined and handled as typical ``ModelForms``. +Please note, however, that they shouldn't be defined next to models (see :ref:`a note `). + Editing multilanguage models with all translation fields in the admin backend is quite sensible. However, presenting all model fields to the user on the frontend may be not the right way. Here comes the ``TranslationModelForm`` which strip out all translation fields:: diff --git a/docs/modeltranslation/registration.rst b/docs/modeltranslation/registration.rst index 09a7d3bb..a0dd9a81 100644 --- a/docs/modeltranslation/registration.rst +++ b/docs/modeltranslation/registration.rst @@ -128,6 +128,28 @@ As these fields are added to the registered model class as fully valid Django model fields, they will appear in the db schema for the model although it has not been specified on the model explicitly. +.. _register-precautions: + +Precautions regarding registration approach +******************************************* + +Be aware that registration approach (as opposed to base-class approach) to +models translation has a few caveats, though (despite many pros). + +First important thing to note is the fact that translatable models are being patched - that means +their fields list is not final until the `MT` code executes. In normal circumstances it shouldn't +affect anything - as long as ``models.py`` contain only models' related code. + +For example: consider a project when a ``ModelForm`` is declared in ``models.py`` just after +its model. When the file is executed, the form gets prepared - but it will be frozen with +old fields list (without translation fields). That's because ``ModelForm`` will be created before +`MT` would add new fields to the model (``ModelForm`` gather fields info at class creation time, not +instantiation time). Proper solution is to define the form in ``forms.py``, which wouldn't be +imported alongside with ``models.py`` (and rather imported from views file or urlconf). + +Generally, for seamless integration with `MT` (and as sensible design, anyway), +the ``models.py`` should contain only bare models and model related logic. + .. _db-fields: Committing fields to database From 5ed3a2696b7e13687c2eec2cf86e69859917b56d Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Tue, 6 May 2014 21:59:21 +0200 Subject: [PATCH 025/170] Don't set use_for_related_fields unconditionally on all managers (ref #242). --- modeltranslation/translator.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/modeltranslation/translator.py b/modeltranslation/translator.py index 46c60a7e..fd5486e6 100644 --- a/modeltranslation/translator.py +++ b/modeltranslation/translator.py @@ -161,17 +161,22 @@ def add_manager(model): """ if model._meta.abstract: return + + def patch_manager_class(manager): + if isinstance(manager, MultilingualManager): + return + if manager.__class__ is Manager: + manager.__class__ = MultilingualManager + else: + class NewMultilingualManager(MultilingualManager, manager.__class__): + use_for_related_fields = getattr( + manager.__class__, "use_for_related_fields", False) + manager.__class__ = NewMultilingualManager + for _, attname, cls in model._meta.concrete_managers + model._meta.abstract_managers: current_manager = getattr(model, attname) - if isinstance(current_manager, MultilingualManager): - continue prev_class = current_manager.__class__ - if current_manager.__class__ is Manager: - current_manager.__class__ = MultilingualManager - else: - class NewMultilingualManager(MultilingualManager, current_manager.__class__): - pass - current_manager.__class__ = NewMultilingualManager + patch_manager_class(current_manager) if model._default_manager.__class__ is prev_class: # Normally model._default_manager is a reference to one of model's managers # (and would be patched by the way). @@ -179,6 +184,7 @@ class NewMultilingualManager(MultilingualManager, current_manager.__class__): # model._default_manager is not the same instance as one of managers, but it # share the same class. model._default_manager.__class__ = current_manager.__class__ + patch_manager_class(model._base_manager) def patch_constructor(model): From f6087cf3fa6ef4db874fb3705ecc5c1d01ea2748 Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Mon, 12 May 2014 17:41:07 +0200 Subject: [PATCH 026/170] Detect custom get_queryset on managers (ref #242). --- modeltranslation/translator.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/modeltranslation/translator.py b/modeltranslation/translator.py index fd5486e6..3c33e309 100644 --- a/modeltranslation/translator.py +++ b/modeltranslation/translator.py @@ -152,6 +152,13 @@ def add_translation_fields(model, opts): model._meta._fill_fields_cache() +def has_custom_queryset(manager): + "Check whether manager (or its parents) has declared some custom get_queryset method." + old_diff = getattr(manager, 'get_query_set', None) != getattr(Manager, 'get_query_set', None) + new_diff = getattr(manager, 'get_queryset', None) != getattr(Manager, 'get_queryset', None) + return old_diff or new_diff + + def add_manager(model): """ Monkey patches the original model to use MultilingualManager instead of @@ -170,7 +177,7 @@ def patch_manager_class(manager): else: class NewMultilingualManager(MultilingualManager, manager.__class__): use_for_related_fields = getattr( - manager.__class__, "use_for_related_fields", False) + manager.__class__, "use_for_related_fields", not has_custom_queryset(manager)) manager.__class__ = NewMultilingualManager for _, attname, cls in model._meta.concrete_managers + model._meta.abstract_managers: From f34b26684cfffbdd3969c7ea1509c3069f0c433c Mon Sep 17 00:00:00 2001 From: Thom Wiggers Date: Sun, 18 May 2014 21:37:22 +0200 Subject: [PATCH 027/170] Move to get_queryset get_query_set is deprecated and raises a RemovedInDjango18 warning --- modeltranslation/manager.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/modeltranslation/manager.py b/modeltranslation/manager.py index b9af3dfa..1c1c1f65 100644 --- a/modeltranslation/manager.py +++ b/modeltranslation/manager.py @@ -305,15 +305,15 @@ class MultilingualManager(models.Manager): use_for_related_fields = True def rewrite(self, *args, **kwargs): - return self.get_query_set().rewrite(*args, **kwargs) + return self.get_queryset().rewrite(*args, **kwargs) def populate(self, *args, **kwargs): - return self.get_query_set().populate(*args, **kwargs) + return self.get_queryset().populate(*args, **kwargs) def raw_values(self, *args, **kwargs): - return self.get_query_set().raw_values(*args, **kwargs) + return self.get_queryset().raw_values(*args, **kwargs) - def get_query_set(self): + def get_queryset(self): qs = super(MultilingualManager, self).get_query_set() if qs.__class__ == models.query.QuerySet: qs.__class__ = MultilingualQuerySet @@ -325,3 +325,5 @@ class NewClass(qs.__class__, MultilingualQuerySet): qs._post_init() qs._rewrite_applied_operations() return qs + + get_query_set = get_queryset From f161a335e906b47455253119d4c8485a95456c32 Mon Sep 17 00:00:00 2001 From: Thom Wiggers Date: Sun, 18 May 2014 22:53:49 +0200 Subject: [PATCH 028/170] Fixed get_query_set in super() call Use `getattr` to check if `get_queryset` is supported --- modeltranslation/manager.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/modeltranslation/manager.py b/modeltranslation/manager.py index 1c1c1f65..8f5872c5 100644 --- a/modeltranslation/manager.py +++ b/modeltranslation/manager.py @@ -314,7 +314,11 @@ def raw_values(self, *args, **kwargs): return self.get_queryset().raw_values(*args, **kwargs) def get_queryset(self): - qs = super(MultilingualManager, self).get_query_set() + if hasattr(super(MultilingualManager, self), 'get_queryset'): + qs = super(MultilingualManager, self).get_queryset() + else: # Django 1.4 / 1.5 compat + qs = super(MultilingualManager, self).get_query_set() + if qs.__class__ == models.query.QuerySet: qs.__class__ = MultilingualQuerySet else: From 0dd826547b3381c4145b797dc36b115cfb2e86a7 Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Thu, 5 Jun 2014 22:53:35 +0200 Subject: [PATCH 029/170] Fixed sync_translation_fields to be compatible with Postgresql (close #247). --- .../management/commands/sync_translation_fields.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/modeltranslation/management/commands/sync_translation_fields.py b/modeltranslation/management/commands/sync_translation_fields.py index 4680b8c7..55f56307 100644 --- a/modeltranslation/management/commands/sync_translation_fields.py +++ b/modeltranslation/management/commands/sync_translation_fields.py @@ -124,9 +124,8 @@ def get_sync_sql(self, field_name, missing_langs, model): col_type = f.db_type(connection=connection) field_sql = [style.SQL_FIELD(qn(f.column)), style.SQL_COLTYPE(col_type)] # column creation - sql_output.append("ALTER TABLE %s ADD COLUMN %s;" % (qn(db_table), ' '.join(field_sql))) - if not f.null and lang == settings.LANGUAGE_CODE: - sql_output.append( - ("ALTER TABLE %s MODIFY COLUMN %s %s %s;" % ( - qn(db_table), qn(f.column), col_type, style.SQL_KEYWORD('NOT NULL')))) + stmt = "ALTER TABLE %s ADD COLUMN %s" % (qn(db_table), ' '.join(field_sql)) + if not f.null: + stmt += " " + style.SQL_KEYWORD('NOT NULL') + sql_output.append(stmt + ";") return sql_output From c89aac29fafae05ecc307bd01cdee042bdc55d4d Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Mon, 9 Jun 2014 00:00:50 +0200 Subject: [PATCH 030/170] Remove unused import. --- modeltranslation/management/commands/sync_translation_fields.py | 1 - 1 file changed, 1 deletion(-) diff --git a/modeltranslation/management/commands/sync_translation_fields.py b/modeltranslation/management/commands/sync_translation_fields.py index 55f56307..a6e38f8a 100644 --- a/modeltranslation/management/commands/sync_translation_fields.py +++ b/modeltranslation/management/commands/sync_translation_fields.py @@ -10,7 +10,6 @@ Credits: Heavily inspired by django-transmeta's sync_transmeta_db command. """ from optparse import make_option -from django.conf import settings from django.core.management.base import NoArgsCommand from django.core.management.color import no_style from django.db import connection, transaction From d488d8c48804e6f08e3da1164e2733eb05d484f3 Mon Sep 17 00:00:00 2001 From: deschler Date: Tue, 15 Apr 2014 01:29:20 +0200 Subject: [PATCH 031/170] Support AppConfig introduced by Django 1.7. --- modeltranslation/__init__.py | 1 + modeltranslation/admin.py | 4 +++- modeltranslation/apps.py | 11 +++++++++++ modeltranslation/models.py | 4 +++- 4 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 modeltranslation/apps.py diff --git a/modeltranslation/__init__.py b/modeltranslation/__init__.py index a043fcbb..38281bc6 100644 --- a/modeltranslation/__init__.py +++ b/modeltranslation/__init__.py @@ -4,6 +4,7 @@ https://github.com/django/django """ VERSION = (0, 7, 3, 'final', 0) +default_app_config = 'modeltranslation.apps.ModeltranslationConfig' def get_version(version=None): diff --git a/modeltranslation/admin.py b/modeltranslation/admin.py index bbbf5b9e..1503cd76 100644 --- a/modeltranslation/admin.py +++ b/modeltranslation/admin.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from copy import deepcopy +import django from django.contrib import admin from django.contrib.admin.options import BaseModelAdmin, flatten_fieldsets, InlineModelAdmin from django.contrib.contenttypes import generic @@ -9,7 +10,8 @@ # Ensure that models are registered for translation before TranslationAdmin # runs. The import is supposed to resolve a race condition between model import # and translation registration in production (see issue #19). -import modeltranslation.models # NOQA +if django.get_version() < '1.7': + import modeltranslation.models # NOQA from modeltranslation import settings as mt_settings from modeltranslation.translator import translator from modeltranslation.utils import ( diff --git a/modeltranslation/apps.py b/modeltranslation/apps.py new file mode 100644 index 00000000..7f4d49ba --- /dev/null +++ b/modeltranslation/apps.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +from django.apps import AppConfig + + +class ModeltranslationConfig(AppConfig): + name = 'modeltranslation' + verbose_name = 'Modeltranslation' + + def ready(self): + from modeltranslation.models import handle_translation_registrations + handle_translation_registrations() diff --git a/modeltranslation/models.py b/modeltranslation/models.py index 72c5e9cf..44a785c8 100644 --- a/modeltranslation/models.py +++ b/modeltranslation/models.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import django def autodiscover(): @@ -74,4 +75,5 @@ def handle_translation_registrations(*args, **kwargs): autodiscover() -handle_translation_registrations() +if django.get_version() < '1.7': + handle_translation_registrations() From 4efc7ad6c69bfc06d0291e85ec6aced4d0c9d658 Mon Sep 17 00:00:00 2001 From: deschler Date: Tue, 15 Apr 2014 01:44:10 +0200 Subject: [PATCH 032/170] Added note about INSTALLED_APPS order when admin is used. --- docs/modeltranslation/installation.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/modeltranslation/installation.rst b/docs/modeltranslation/installation.rst index 88237a42..b02b64d8 100644 --- a/docs/modeltranslation/installation.rst +++ b/docs/modeltranslation/installation.rst @@ -91,6 +91,9 @@ Make sure that the ``modeltranslation`` app is listed in your .... ) +.. important:: + If you want to use the admin integration, ``modeltranslation`` must be put + before ``django.contrib.admin``. .. _settings-languages: From 9c876b3fde3667c6972fdc0d5c9973108a71e0b3 Mon Sep 17 00:00:00 2001 From: deschler Date: Tue, 15 Apr 2014 14:31:15 +0200 Subject: [PATCH 033/170] Setup django in runtests standalone script for Django 1.7. --- runtests.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/runtests.py b/runtests.py index 6236ded4..a2fad1f7 100755 --- a/runtests.py +++ b/runtests.py @@ -2,6 +2,7 @@ import os import sys +import django from django.conf import settings from django.core.management import call_command @@ -34,18 +35,21 @@ def runtests(): # Configure test environment settings.configure( - DATABASES = DATABASES, - INSTALLED_APPS = ( + DATABASES=DATABASES, + INSTALLED_APPS=( 'modeltranslation', ), - ROOT_URLCONF = None, # tests override urlconf, but it still needs to be defined - LANGUAGES = ( + ROOT_URLCONF=None, # tests override urlconf, but it still needs to be defined + LANGUAGES=( ('en', 'English'), ), ) + if django.get_version() >= '1.7': + django.setup() failures = call_command( 'test', 'modeltranslation', interactive=False, failfast=False, verbosity=2) + sys.exit(bool(failures)) From 6b8c4299211a332f8643ba026a14582a753f3531 Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Sun, 8 Jun 2014 23:22:08 +0200 Subject: [PATCH 034/170] Add deconstruct() to field definition. --- modeltranslation/fields.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modeltranslation/fields.py b/modeltranslation/fields.py index 8189b167..f2b09e79 100644 --- a/modeltranslation/fields.py +++ b/modeltranslation/fields.py @@ -249,6 +249,9 @@ def save_form_data(self, instance, data, check=True): else: super(TranslationField, self).save_form_data(instance, data) + def deconstruct(self): + return self.translated_field.deconstruct() + def south_field_triple(self): """ Returns a suitable description of this field for South. From ad485822706da9574605c2b5bab92cde1be50348 Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Sun, 8 Jun 2014 23:22:52 +0200 Subject: [PATCH 035/170] Fix tests to work with Django 1.7. --- modeltranslation/tests/test_app/models.py | 4 ++ modeltranslation/tests/tests.py | 49 ++++++++++++++--------- 2 files changed, 33 insertions(+), 20 deletions(-) diff --git a/modeltranslation/tests/test_app/models.py b/modeltranslation/tests/test_app/models.py index f8089055..371265ec 100644 --- a/modeltranslation/tests/test_app/models.py +++ b/modeltranslation/tests/test_app/models.py @@ -2,9 +2,13 @@ class News(models.Model): + class Meta: + app_label = 'test_app' title = models.CharField(max_length=50) visits = models.SmallIntegerField(blank=True, null=True) class Other(models.Model): + class Meta: + app_label = 'test_app' name = models.CharField(max_length=50) diff --git a/modeltranslation/tests/tests.py b/modeltranslation/tests/tests.py index 3a0048ae..28e62614 100644 --- a/modeltranslation/tests/tests.py +++ b/modeltranslation/tests/tests.py @@ -15,23 +15,27 @@ from django.core.management import call_command from django.db import IntegrityError from django.db.models import Q, F -from django.db.models.loading import AppCache from django.test import TestCase, TransactionTestCase from django.test.utils import override_settings from django.utils import six from django.utils.translation import get_language, override, trans_real +try: + from django.apps import apps as django_apps + NEW_APP_CACHE = True +except ImportError: + from django.db.models.loading import AppCache + NEW_APP_CACHE = False + + from modeltranslation import admin, settings as mt_settings, translator from modeltranslation.forms import TranslationModelForm from modeltranslation.models import autodiscover -from modeltranslation.tests import models -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, auto_populate, fallbacks) +models = translation = None # None of the following tests really depend on the content of the request, # so we'll just pass in None. @@ -61,7 +65,7 @@ def default_fallback(): @override_settings(**TEST_SETTINGS) class ModeltranslationTransactionTestBase(TransactionTestCase): urls = 'modeltranslation.tests.urls' - cache = AppCache() + cache = django_apps if NEW_APP_CACHE else AppCache() synced = False @classmethod @@ -76,11 +80,9 @@ def setUpClass(cls): # In order to perform only one syncdb ModeltranslationTestBase.synced = True with override_settings(**TEST_SETTINGS): - import sys - # 1. Reload translation in case USE_I18N was False - from django.utils import translation - imp.reload(translation) + from django.utils import translation as dj_trans + imp.reload(dj_trans) # 2. Reload MT because LANGUAGES likely changed. imp.reload(mt_settings) @@ -90,20 +92,24 @@ def setUpClass(cls): # 3. Reset test models (because autodiscover have already run, those models # have translation fields, but for languages previously defined. We want # to be sure that 'de' and 'en' are available) - del cls.cache.app_models['tests'] - imp.reload(models) - cls.cache.load_app('modeltranslation.tests') - sys.modules.pop('modeltranslation.tests.translation', None) + if not NEW_APP_CACHE: + cls.cache.load_app('modeltranslation.tests') # 4. Autodiscover - from modeltranslation import models as aut_models - imp.reload(aut_models) + from modeltranslation.models import handle_translation_registrations + handle_translation_registrations() # 5. Syncdb (``migrate=False`` in case of south) from django.db import connections, DEFAULT_DB_ALIAS call_command('syncdb', verbosity=0, migrate=False, interactive=False, database=connections[DEFAULT_DB_ALIAS].alias, load_initial_data=False) + # A rather dirty trick to import models into module namespace, but not before + # tests app has been added into INSTALLED_APPS and loaded + # (that's why this is not imported in normal import section) + global models, translation + from modeltranslation.tests import models, translation # NOQA + def setUp(self): self._old_language = get_language() trans_real.activate('de') @@ -151,7 +157,10 @@ def tearDownClass(cls): def tearDown(self): import sys # Rollback model classes - del self.cache.app_models['test_app'] + if NEW_APP_CACHE: + del self.cache.all_models['test_app'] + else: + del self.cache.app_models['test_app'] from .test_app import models imp.reload(models) # Delete translation modules from import cache @@ -412,7 +421,7 @@ def test_fallback_values_2(self): self.assertEqual(n.title, '') # Falling back to default field value self.assertEqual( n.text, - FallbackModel2TranslationOptions.fallback_values['text']) + translation.FallbackModel2TranslationOptions.fallback_values['text']) def _compare_instances(self, x, y, field): self.assertEqual(getattr(x, field), getattr(y, field), @@ -1840,7 +1849,7 @@ class ModelInheritanceFieldAggregationTest(ModeltranslationTestBase): in modeltranslation. """ def test_field_aggregation(self): - clsb = FieldInheritanceCTranslationOptions + clsb = translation.FieldInheritanceCTranslationOptions self.assertTrue('titlea' in clsb.fields) self.assertTrue('titleb' in clsb.fields) self.assertTrue('titlec' in clsb.fields) @@ -1848,7 +1857,7 @@ def test_field_aggregation(self): self.assertEqual(tuple, type(clsb.fields)) def test_multi_inheritance(self): - clsb = FieldInheritanceETranslationOptions + clsb = translation.FieldInheritanceETranslationOptions self.assertTrue('titlea' in clsb.fields) self.assertTrue('titleb' in clsb.fields) self.assertTrue('titlec' in clsb.fields) From 5c014bfd0b0d760d7496b6e5297c2fb838830027 Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Sun, 8 Jun 2014 23:58:53 +0200 Subject: [PATCH 036/170] Update build configurations. --- .travis.yml | 19 +++++++++++++++++-- tox.ini | 21 +++++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 95cf965f..59d53177 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,6 +14,9 @@ env: - DJANGO=1.6 DB=sqlite - DJANGO=1.6 DB=postgres - DJANGO=1.6 DB=mysql + - DJANGO=https://www.djangoproject.com/download/1.7.b4/tarball DB=sqlite + - DJANGO=https://www.djangoproject.com/download/1.7.b4/tarball DB=postgres + - DJANGO=https://www.djangoproject.com/download/1.7.b4/tarball DB=mysql matrix: exclude: - python: "3.2" @@ -29,6 +32,13 @@ matrix: - python: "3.3" env: DJANGO=1.4 DB=mysql + - python: "2.6" + env: DJANGO=https://www.djangoproject.com/download/1.7.b4/tarball DB=sqlite + - python: "2.6" + env: DJANGO=https://www.djangoproject.com/download/1.7.b4/tarball DB=postgres + - python: "2.6" + env: DJANGO=https://www.djangoproject.com/download/1.7.b4/tarball DB=mysql + - python: "3.2" env: DJANGO=1.5 DB=mysql - python: "3.3" @@ -37,15 +47,20 @@ matrix: env: DJANGO=1.6 DB=mysql - python: "3.3" env: DJANGO=1.6 DB=mysql -before_script: + - python: "3.2" + env: DJANGO=https://www.djangoproject.com/download/1.7.b4/tarball DB=mysql + - python: "3.3" + env: DJANGO=https://www.djangoproject.com/download/1.7.b4/tarball DB=mysql +before_install: + - pip install -q flake8 --use-mirrors - PYFLAKES_NODOCTEST=1 flake8 --max-line-length=100 modeltranslation +before_script: - mysql -e 'create database modeltranslation;' - psql -c 'create database modeltranslation;' -U postgres install: - if [[ $DB == mysql ]]; then pip install -q mysql-python --use-mirrors; fi - if [[ $DB == postgres ]]; then pip install -q psycopg2 --use-mirrors; fi - pip install -q Pillow --use-mirrors - - pip install -q flake8 --use-mirrors - IDJANGO=$(./travis.py $DJANGO) - pip install -q $IDJANGO - pip install -e . --use-mirrors diff --git a/tox.ini b/tox.ini index 18ee6b13..e030a48d 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,9 @@ [tox] distribute = False envlist = + py33-1.7.X, + py32-1.7.X, + py27-1.7.X, py33-1.6.X, py32-1.6.X, py27-1.6.X, @@ -19,6 +22,24 @@ commands = {envpython} runtests.py +[testenv:py33-1.7.X] +basepython = python3.3 +deps = + https://www.djangoproject.com/download/1.7.b4/tarball + Pillow + +[testenv:py32-1.7.X] +basepython = python3.2 +deps = + https://www.djangoproject.com/download/1.7.b4/tarball + Pillow + +[testenv:py27-1.7.X] +basepython = python2.7 +deps = + https://www.djangoproject.com/download/1.7.b4/tarball + Pillow + [testenv:py33-1.6.X] basepython = python3.3 deps = From 2cd9467716346bf504f286beeafa813325a091c1 Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Mon, 9 Jun 2014 22:52:06 +0200 Subject: [PATCH 037/170] Fix ProxyModel handling. --- modeltranslation/translator.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/modeltranslation/translator.py b/modeltranslation/translator.py index 3c33e309..e3488406 100644 --- a/modeltranslation/translator.py +++ b/modeltranslation/translator.py @@ -387,7 +387,10 @@ def register(self, model_or_iterable, opts_class=None, **options): opts.registered = True # Add translation fields to the model. - add_translation_fields(model, opts) + if model._meta.proxy: + delete_cache_fields(model) + else: + add_translation_fields(model, opts) # Delete all fields cache for related model (parent and children) for related_obj in model._meta.get_all_related_objects(): From 7b5e0938a7e25e15ad4d2fdb63e229ba1d0e38d3 Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Mon, 9 Jun 2014 22:59:19 +0200 Subject: [PATCH 038/170] Adapt MultilingualManager to use new Lookups from Django 1.7. --- modeltranslation/manager.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/modeltranslation/manager.py b/modeltranslation/manager.py index 8f5872c5..7e7842e1 100644 --- a/modeltranslation/manager.py +++ b/modeltranslation/manager.py @@ -10,6 +10,12 @@ from django.db.models.fields.related import RelatedField, RelatedObject from django.db.models.sql.where import Constraint from django.utils.tree import Node +try: + from django.db.models.lookups import Lookup + from django.db.models.sql.datastructures import Col + NEW_LOOKUPS = True +except ImportError: + NEW_LOOKUPS = False from modeltranslation import settings from modeltranslation.fields import TranslationField @@ -143,11 +149,26 @@ def _rewrite_applied_operations(self): self._rewrite_where(self.query.having) self._rewrite_order() + # This method was not present in django-linguo + def _rewrite_col(self, col): + """Django 1.7 column name rewriting""" + if isinstance(col, Col): + new_name = rewrite_lookup_key(self.model, col.target.name) + if col.target.name != new_name: + new_field = self.model._meta.get_field(new_name) + if col.target is col.source: + col.source = new_field + col.target = new_field + elif hasattr(col, 'col'): + self._rewrite_col(col.col) + elif hasattr(col, 'lhs'): + self._rewrite_col(col.lhs) + def _rewrite_where(self, q): """ Rewrite field names inside WHERE tree. """ - if isinstance(q, tuple) and isinstance(q[0], Constraint): + if not NEW_LOOKUPS and isinstance(q, tuple) and isinstance(q[0], Constraint): c = q[0] if c.field is None: c.field = get_field_by_colum_name(self.model, c.col) @@ -155,6 +176,8 @@ def _rewrite_where(self, q): if c.field.name != new_name: c.field = self.model._meta.get_field(new_name) c.col = c.field.column + elif NEW_LOOKUPS and isinstance(q, Lookup): + self._rewrite_col(q.lhs) if isinstance(q, Node): for child in q.children: self._rewrite_where(child) From 973bb78bda5f68a26d1c3b71ad34a964caa96272 Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Mon, 9 Jun 2014 23:31:44 +0200 Subject: [PATCH 039/170] Fix Django 1.7 DeprecationWarnings. --- modeltranslation/tests/models.py | 11 +++++++---- modeltranslation/tests/tests.py | 5 +++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/modeltranslation/tests/models.py b/modeltranslation/tests/models.py index 44c065e3..288fb601 100644 --- a/modeltranslation/tests/models.py +++ b/modeltranslation/tests/models.py @@ -261,9 +261,11 @@ class Meta: class CustomManager(models.Manager): - def get_query_set(self): - return (super(CustomManager, self).get_query_set().filter(title__contains='a') - .exclude(description__contains='x')) + def get_queryset(self): + sup = super(CustomManager, self) + queryset = sup.get_queryset() if hasattr(sup, 'get_queryset') else sup.get_query_set() + return queryset.filter(title__contains='a').exclude(description__contains='x') + get_query_set = get_queryset def foo(self): return 'bar' @@ -282,8 +284,9 @@ class CustomQuerySet(models.query.QuerySet): class CustomManager2(models.Manager): - def get_query_set(self): + def get_queryset(self): return CustomQuerySet(self.model, using=self._db) + get_query_set = get_queryset class CustomManager2TestModel(models.Model): diff --git a/modeltranslation/tests/tests.py b/modeltranslation/tests/tests.py index 28e62614..12e3b0bb 100644 --- a/modeltranslation/tests/tests.py +++ b/modeltranslation/tests/tests.py @@ -5,6 +5,7 @@ import shutil import imp +import django from django import forms from django.conf import settings as django_settings from django.contrib.admin.sites import AdminSite @@ -2295,6 +2296,8 @@ def test_form(self): class CreationForm(forms.ModelForm): class Meta: model = self.model + if django.get_version() >= '1.6': + fields = '__all__' creation_form = CreationForm({'name': 'abc'}) inst = creation_form.save() @@ -2715,6 +2718,8 @@ def test_fields(self): class TestModelForm(TranslationModelForm): class Meta: model = models.TestModel + if django.get_version() >= '1.6': + fields = '__all__' form = TestModelForm() self.assertEqual(list(form.base_fields), From ad2985b0bf90ffa22d1c7f3889d8ef1eee25a5f9 Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Mon, 9 Jun 2014 23:40:25 +0200 Subject: [PATCH 040/170] Update docs about 1.7 compatibility and bumb the version to 0.8a (close #237). --- docs/modeltranslation/installation.rst | 8 +++++++- modeltranslation/__init__.py | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/modeltranslation/installation.rst b/docs/modeltranslation/installation.rst index b02b64d8..2c46d31a 100644 --- a/docs/modeltranslation/installation.rst +++ b/docs/modeltranslation/installation.rst @@ -9,7 +9,13 @@ Requirements +------------------+------------+-----------+ | Modeltranslation | Python | Django | +==================+============+===========+ -| >=0.7 | 3.2 - 3.3 | 1.5 - 1.6 | +| >=0.8 | 3.2 - 3.3 | 1.5 - 1.7 | +| +------------+-----------+ +| | 2.7 | 1.7 | +| +------------+-----------+ +| | 2.6 - 2.7 | 1.4 - 1.6 | ++------------------+------------+-----------+ +| ==0.7 | 3.2 - 3.3 | 1.5 - 1.6 | | +------------+-----------+ | | 2.6 - 2.7 | 1.4 - 1.6 | +------------------+------------+-----------+ diff --git a/modeltranslation/__init__.py b/modeltranslation/__init__.py index 38281bc6..5cd2d6c6 100644 --- a/modeltranslation/__init__.py +++ b/modeltranslation/__init__.py @@ -3,7 +3,7 @@ Version code adopted from Django development version. https://github.com/django/django """ -VERSION = (0, 7, 3, 'final', 0) +VERSION = (0, 8, 0, 'alpha', 0) default_app_config = 'modeltranslation.apps.ModeltranslationConfig' From 41f8c68ed4a5dec8668847bebd6daae31201aea7 Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Sat, 21 Jun 2014 23:36:01 +0200 Subject: [PATCH 041/170] Update docs; fix type coercion. --- docs/modeltranslation/registration.rst | 2 ++ modeltranslation/translator.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/modeltranslation/registration.rst b/docs/modeltranslation/registration.rst index a0dd9a81..7db585b4 100644 --- a/docs/modeltranslation/registration.rst +++ b/docs/modeltranslation/registration.rst @@ -192,6 +192,8 @@ can use the ``update_translation_fields`` command below. See Required fields --------------- +.. versionadded:: 0.8 + By default, all translation fields are optional (not required). It can be changed using special attribute on ``TranslationOptions``, though:: diff --git a/modeltranslation/translator.py b/modeltranslation/translator.py index e3488406..bc9393ca 100644 --- a/modeltranslation/translator.py +++ b/modeltranslation/translator.py @@ -88,7 +88,7 @@ def validate(self): 'Fieldname in required_languages which is not in fields option.') def _check_languages(self, languages, extra=()): - correct = mt_settings.AVAILABLE_LANGUAGES + list(extra) + correct = list(mt_settings.AVAILABLE_LANGUAGES) + list(extra) if any(l not in correct for l in languages): raise ImproperlyConfigured( 'Language in required_languages which is not in AVAILABLE_LANGUAGES.') From c607ed3862bf4c9d27744152adb4a11d912c041b Mon Sep 17 00:00:00 2001 From: deschler Date: Sun, 22 Jun 2014 13:43:07 +0200 Subject: [PATCH 042/170] Prepared 0.8b1 release. --- AUTHORS.rst | 3 +++ CHANGELOG.txt | 39 ++++++++++++++++++++++++++++++++++++ PKG-INFO | 2 +- modeltranslation/__init__.py | 2 +- 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 73db49ee..196e415a 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -27,6 +27,9 @@ Contributors * Konrad Wojas * Bas Peschier * Oleg Prans +* Francesc Arpí Roca +* Mathieu Leplatre +* Thom Wiggers * And many more ... (if you miss your name here, please let us know!) .. _django-linguo: https://github.com/zmathew/django-linguo diff --git a/CHANGELOG.txt b/CHANGELOG.txt index eb1eb231..c7b4a729 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,42 @@ +v0.8b1 +====== +Date: 2014-06-22 + + ADDED: Detect custom get_queryset on managers. + (resolves issue #242, + thanks Jacek Tomaszewski) + ADDED: Support for Django 1.7 and the new app-loading refactor. + (resolves issue #237) + + FIXED: Fixed sync_translation_fields to be compatible with PostgreSQL. + (resolves issue #247, + thanks Jacek Tomaszewski) + FIXED: Fieldset headers are not capitalized when group_fieldsets is enabled. + (resolves issue #234, + thanks Jacek Tomaszewski) + FIXED: Exclude for nullable field manager rewriting. + (resolves issue #231, + thanks Jacek Tomaszewski) + FIXED: Use AVAILABLE_LANGUAGES in sync_translation_fields management + command to detect missing fields. + (resolves issue #227, + thanks Mathieu Leplatre) + FIXED: Take db_column into account while syncing fields + (resolves issue #225, + thanks Mathieu Leplatre) + +CHANGED: Moved to get_queryset, which resolves a deprecation warning. + (resolves issue #244, + thanks Thom Wiggers) +CHANGED: Considered iframes in tabbed_translation_fields.js to support + third party apps like django-summernote. + (resolves issue #229, + thanks Francesc Arpí Roca) +CHANGED: Removed the http protocol from jquery-ui url in admin Media class. + (resolves issue #224, + thanks Francesc Arpí Roca) + + v0.7.3 ====== Date: 2014-01-05 diff --git a/PKG-INFO b/PKG-INFO index 56638790..62b7b098 100644 --- a/PKG-INFO +++ b/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 1.0 Name: django-modeltranslation -Version: 0.7.3 +Version: 0.8b1 Summary: Translates Django models using a registration approach. Home-page: https://github.com/deschler/django-modeltranslation Author: Peter Eschler, diff --git a/modeltranslation/__init__.py b/modeltranslation/__init__.py index 5cd2d6c6..5215da39 100644 --- a/modeltranslation/__init__.py +++ b/modeltranslation/__init__.py @@ -3,7 +3,7 @@ Version code adopted from Django development version. https://github.com/django/django """ -VERSION = (0, 8, 0, 'alpha', 0) +VERSION = (0, 8, 0, 'beta', 1) default_app_config = 'modeltranslation.apps.ModeltranslationConfig' From 7dcd7790d7d8d6e38a98f4ab13cb5a3d83e3f8ab Mon Sep 17 00:00:00 2001 From: deschler Date: Sun, 22 Jun 2014 13:51:47 +0200 Subject: [PATCH 043/170] Required Django >=1.4. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8a994900..067d5eac 100755 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ 'modeltranslation.management.commands'], package_data={'modeltranslation': ['static/modeltranslation/css/*.css', 'static/modeltranslation/js/*.js']}, - requires=['django(>=1.3)'], + requires=['django(>=1.4)'], download_url='https://github.com/deschler/django-modeltranslation/archive/%s.tar.gz' % version, classifiers=[ 'Programming Language :: Python', From 3dcffd7f1bd8e0834b20cd2cb93d5a016a3fff04 Mon Sep 17 00:00:00 2001 From: deschler Date: Sun, 22 Jun 2014 13:53:09 +0200 Subject: [PATCH 044/170] Removed classifier for Python 2.5 which is no longer supported. --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 067d5eac..bc00bfd8 100755 --- a/setup.py +++ b/setup.py @@ -29,7 +29,6 @@ download_url='https://github.com/deschler/django-modeltranslation/archive/%s.tar.gz' % version, classifiers=[ 'Programming Language :: Python', - 'Programming Language :: Python :: 2.5', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', From 9f10ee4ef18273f2280c3a3e053cf5258bfd0508 Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Sun, 22 Jun 2014 14:20:51 +0200 Subject: [PATCH 045/170] Update changelog for 0.8b1. --- CHANGELOG.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index c7b4a729..3f86a06d 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -7,10 +7,14 @@ Date: 2014-06-22 thanks Jacek Tomaszewski) ADDED: Support for Django 1.7 and the new app-loading refactor. (resolves issue #237) + ADDED: Added required_languages TranslationOptions + (resolves issue #143) FIXED: Fixed sync_translation_fields to be compatible with PostgreSQL. (resolves issue #247, thanks Jacek Tomaszewski) + FIXED: Manager .values() with no fields specified behaves as expected. + (resolves issue #247) FIXED: Fieldset headers are not capitalized when group_fieldsets is enabled. (resolves issue #234, thanks Jacek Tomaszewski) From 8e1066474565957ecf731228ed37360d86321e64 Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Tue, 24 Jun 2014 20:10:47 +0200 Subject: [PATCH 046/170] Support AppConfigs in INSTALLED_APPS (close #252). Thanks Warnar Boekkooi for initial commit. --- modeltranslation/models.py | 9 +++++++-- modeltranslation/tests/tests.py | 6 ++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/modeltranslation/models.py b/modeltranslation/models.py index 44a785c8..9082e9af 100644 --- a/modeltranslation/models.py +++ b/modeltranslation/models.py @@ -17,8 +17,13 @@ def autodiscover(): from modeltranslation.translator import translator from modeltranslation.settings import TRANSLATION_FILES, DEBUG - for app in settings.INSTALLED_APPS: - mod = import_module(app) + if django.get_version() < '1.7': + mods = [(app, import_module(app)) for app in settings.INSTALLED_APPS] + else: + from django.apps import apps + mods = [(app_config.name, app_config.module) for app_config in apps.get_app_configs()] + + for (app, mod) in mods: # Attempt to import the app's translation module. module = '%s.translation' % app before_import_registry = copy.copy(translator._registry) diff --git a/modeltranslation/tests/tests.py b/modeltranslation/tests/tests.py index 12e3b0bb..081d6513 100644 --- a/modeltranslation/tests/tests.py +++ b/modeltranslation/tests/tests.py @@ -95,6 +95,12 @@ def setUpClass(cls): # to be sure that 'de' and 'en' are available) if not NEW_APP_CACHE: cls.cache.load_app('modeltranslation.tests') + else: + del cls.cache.all_models['tests'] + import sys + sys.modules.pop('modeltranslation.tests.models', None) + sys.modules.pop('modeltranslation.tests.translation', None) + cls.cache.get_app_config('tests').import_models(cls.cache.all_models['tests']) # 4. Autodiscover from modeltranslation.models import handle_translation_registrations From 011d39f32f98adf04f3c73dffcd6ed1e6af7a71c Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Mon, 30 Jun 2014 21:39:20 +0200 Subject: [PATCH 047/170] Add docs section about debug toolbar. --- docs/modeltranslation/installation.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/modeltranslation/installation.rst b/docs/modeltranslation/installation.rst index 2c46d31a..2a888ecd 100644 --- a/docs/modeltranslation/installation.rst +++ b/docs/modeltranslation/installation.rst @@ -101,6 +101,12 @@ Make sure that the ``modeltranslation`` app is listed in your If you want to use the admin integration, ``modeltranslation`` must be put before ``django.contrib.admin``. +.. important:: + If you want to use the ``django-debug-toolbar`` together with `MT`, put ``debug_toolbar`` + as first entry in ``INSTALLED_APPS`` or use + `explicit setup + `_. + .. _settings-languages: ``LANGUAGES`` From 58e06c464dbe4a0d536a4b63eae474466a4990bc Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Sun, 6 Jul 2014 14:39:53 +0200 Subject: [PATCH 048/170] Fix MultilingualManager MRO (close #204). --- modeltranslation/manager.py | 58 +++++++++++++++++++++++--------- modeltranslation/tests/models.py | 5 +++ modeltranslation/tests/tests.py | 6 ++++ modeltranslation/translator.py | 6 ++-- 4 files changed, 57 insertions(+), 18 deletions(-) diff --git a/modeltranslation/manager.py b/modeltranslation/manager.py index 7e7842e1..6fefb167 100644 --- a/modeltranslation/manager.py +++ b/modeltranslation/manager.py @@ -324,24 +324,23 @@ def dates(self, field_name, *args, **kwargs): return super(MultilingualQuerySet, self).dates(new_key, *args, **kwargs) -class MultilingualManager(models.Manager): - use_for_related_fields = True - - def rewrite(self, *args, **kwargs): - return self.get_queryset().rewrite(*args, **kwargs) - - def populate(self, *args, **kwargs): - return self.get_queryset().populate(*args, **kwargs) - - def raw_values(self, *args, **kwargs): - return self.get_queryset().raw_values(*args, **kwargs) - +def get_queryset(obj): + if hasattr(obj, 'get_queryset'): + return obj.get_queryset() + else: # Django 1.4 / 1.5 compat + return obj.get_query_set() + + +class MultilingualQuerysetManager(models.Manager): + """ + This class gets hooked in MRO just before plain Manager, so that every call to + get_queryset returns MultilingualQuerySet. + """ def get_queryset(self): - if hasattr(super(MultilingualManager, self), 'get_queryset'): - qs = super(MultilingualManager, self).get_queryset() - else: # Django 1.4 / 1.5 compat - qs = super(MultilingualManager, self).get_query_set() + qs = get_queryset(super(MultilingualQuerysetManager, self)) + return self._patch_queryset(qs) + def _patch_queryset(self, qs): if qs.__class__ == models.query.QuerySet: qs.__class__ = MultilingualQuerySet else: @@ -354,3 +353,30 @@ class NewClass(qs.__class__, MultilingualQuerySet): return qs get_query_set = get_queryset + + +class MultilingualManager(MultilingualQuerysetManager): + use_for_related_fields = True + + def rewrite(self, *args, **kwargs): + return self.get_queryset().rewrite(*args, **kwargs) + + def populate(self, *args, **kwargs): + return self.get_queryset().populate(*args, **kwargs) + + def raw_values(self, *args, **kwargs): + return self.get_queryset().raw_values(*args, **kwargs) + + def get_queryset(self): + """ + This method is repeated because some managers that don't use super() or alter queryset class + may return queryset that is not subclass of MultilingualQuerySet. + """ + qs = get_queryset(super(MultilingualManager, self)) + if isinstance(qs, MultilingualQuerySet): + # Is already patched by MultilingualQuerysetManager - in most of the cases + # when custom managers use super() properly in get_queryset. + return qs + return self._patch_queryset(qs) + + get_query_set = get_queryset diff --git a/modeltranslation/tests/models.py b/modeltranslation/tests/models.py index 288fb601..831b4f4d 100644 --- a/modeltranslation/tests/models.py +++ b/modeltranslation/tests/models.py @@ -267,6 +267,11 @@ def get_queryset(self): return queryset.filter(title__contains='a').exclude(description__contains='x') get_query_set = get_queryset + def custom_qs(self): + sup = super(CustomManager, self) + queryset = sup.get_queryset() if hasattr(sup, 'get_queryset') else sup.get_query_set() + return queryset + def foo(self): return 'bar' diff --git a/modeltranslation/tests/tests.py b/modeltranslation/tests/tests.py index 081d6513..e1f705eb 100644 --- a/modeltranslation/tests/tests.py +++ b/modeltranslation/tests/tests.py @@ -2510,6 +2510,12 @@ def test_custom_manager(self): with override('de'): self.assertEqual(1, models.CustomManagerTestModel.objects.count()) + def test_custom_manager_custom_method_name(self): + """Test if custom method also returns MultilingualQuerySet""" + from modeltranslation.manager import MultilingualQuerySet + qs = models.CustomManagerTestModel.objects.custom_qs() + self.assertIsInstance(qs, MultilingualQuerySet) + def test_non_objects_manager(self): """Test if managers other than ``objects`` are patched too""" from modeltranslation.manager import MultilingualManager diff --git a/modeltranslation/translator.py b/modeltranslation/translator.py index bc9393ca..5f4be65b 100644 --- a/modeltranslation/translator.py +++ b/modeltranslation/translator.py @@ -9,7 +9,8 @@ from modeltranslation.fields import (NONE, create_translation_field, TranslationFieldDescriptor, TranslatedRelationIdDescriptor, LanguageCacheSingleObjectDescriptor) -from modeltranslation.manager import MultilingualManager, rewrite_lookup_key +from modeltranslation.manager import (MultilingualManager, MultilingualQuerysetManager, + rewrite_lookup_key) from modeltranslation.utils import build_localized_fieldname, parse_field @@ -175,7 +176,8 @@ def patch_manager_class(manager): if manager.__class__ is Manager: manager.__class__ = MultilingualManager else: - class NewMultilingualManager(MultilingualManager, manager.__class__): + class NewMultilingualManager(MultilingualManager, manager.__class__, + MultilingualQuerysetManager): use_for_related_fields = getattr( manager.__class__, "use_for_related_fields", not has_custom_queryset(manager)) manager.__class__ = NewMultilingualManager From 7a7b3fbc3bc01c5fb819b9e1e28d0dbcd92c320e Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Sun, 6 Jul 2014 19:36:46 +0200 Subject: [PATCH 049/170] Rewrite field names in select_related; fix deffered models registry lookup (close #248). --- modeltranslation/manager.py | 17 +++++++++++++++++ modeltranslation/tests/tests.py | 19 +++++++++++++++++++ modeltranslation/translator.py | 2 ++ 3 files changed, 38 insertions(+) diff --git a/modeltranslation/manager.py b/modeltranslation/manager.py index 6fefb167..be0522e5 100644 --- a/modeltranslation/manager.py +++ b/modeltranslation/manager.py @@ -148,6 +148,16 @@ def _rewrite_applied_operations(self): self._rewrite_where(self.query.where) self._rewrite_where(self.query.having) self._rewrite_order() + self._rewrite_select_related() + + # This method was not present in django-linguo + def select_related(self, *fields, **kwargs): + if not self._rewrite: + return super(MultilingualQuerySet, self).select_related(*fields, **kwargs) + new_args = [] + for key in fields: + new_args.append(rewrite_order_lookup_key(self.model, key)) + return super(MultilingualQuerySet, self).select_related(*new_args, **kwargs) # This method was not present in django-linguo def _rewrite_col(self, col): @@ -186,6 +196,13 @@ def _rewrite_order(self): self.query.order_by = [rewrite_order_lookup_key(self.model, field_name) for field_name in self.query.order_by] + def _rewrite_select_related(self): + if isinstance(self.query.select_related, dict): + new = [] + for field_name, value in self.query.select_related.items(): + new[rewrite_order_lookup_key(self.model, field_name)] = value + self.query.select_related = new + # This method was not present in django-linguo def _rewrite_q(self, q): """Rewrite field names inside Q call.""" diff --git a/modeltranslation/tests/tests.py b/modeltranslation/tests/tests.py index e1f705eb..a2feff15 100644 --- a/modeltranslation/tests/tests.py +++ b/modeltranslation/tests/tests.py @@ -2718,6 +2718,25 @@ def test_deferred(self): self.assertDeferred(True, 'title', 'title_de') self.assertDeferred(True, 'text', 'email', 'url') + def test_deferred_fk(self): + """ + Check if ``select_related`` is rewritten and also + if ``only`` and ``defer`` are working with deferred classes + """ + test = models.TestModel.objects.create(title_de='title_de', title_en='title_en') + with auto_populate('all'): + models.ForeignKeyModel.objects.create(test=test) + + item = models.ForeignKeyModel.objects.select_related("test").defer("test__text")[0] + self.assertTrue(item.test.__class__._deferred) + self.assertEqual('title_en', item.test.title) + self.assertEqual('title_en', item.test.__class__.objects.only('title')[0].title) + with override('de'): + item = models.ForeignKeyModel.objects.select_related("test").defer("test__text")[0] + self.assertTrue(item.test.__class__._deferred) + self.assertEqual('title_de', item.test.title) + self.assertEqual('title_de', item.test.__class__.objects.only('title')[0].title) + def test_constructor_inheritance(self): inst = models.AbstractModelB() # Check if fields assigned in constructor hasn't been ignored. diff --git a/modeltranslation/translator.py b/modeltranslation/translator.py index 5f4be65b..d0335790 100644 --- a/modeltranslation/translator.py +++ b/modeltranslation/translator.py @@ -486,6 +486,8 @@ def _get_options_for_model(self, model, opts_class=None, **options): Returns an instance of translation options with translated fields defined for the ``model`` and inherited from superclasses. """ + if model._deferred: + model = model.__bases__[0] if model not in self._registry: # Create a new type for backwards compatibility. opts = type("%sTranslationOptions" % model.__name__, From 17f3d96ccce2d2368e4cde4db24ed67349adb5a5 Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Wed, 9 Jul 2014 23:17:06 +0200 Subject: [PATCH 050/170] Rewrite spanned queries on all levels for defer/only (close #248). Also: - optimize fields to related model access - more elegant way to access DefferedModel real class - fix select_related rewriting and add some code to contemplate possible solutions - update docs about select_related - add 2 test for implemented features --- docs/modeltranslation/usage.rst | 1 + modeltranslation/manager.py | 67 +++++++++++++++++++++++--------- modeltranslation/tests/models.py | 1 + modeltranslation/tests/tests.py | 33 ++++++++++++++++ modeltranslation/translator.py | 2 +- 5 files changed, 84 insertions(+), 20 deletions(-) diff --git a/docs/modeltranslation/usage.rst b/docs/modeltranslation/usage.rst index 037c01a7..1ad3f884 100644 --- a/docs/modeltranslation/usage.rst +++ b/docs/modeltranslation/usage.rst @@ -125,6 +125,7 @@ These manager methods perform rewriting: - ``only()``, ``defer()`` - ``values()``, ``values_list()`` - ``dates()`` +- ``select_related()`` - ``create()``, with optional auto-population_ feature In order not to introduce differences between ``X.objects.create(...)`` and ``X(...)``, model diff --git a/modeltranslation/manager.py b/modeltranslation/manager.py index be0522e5..5a86cf60 100644 --- a/modeltranslation/manager.py +++ b/modeltranslation/manager.py @@ -5,6 +5,8 @@ https://github.com/zmathew/django-linguo """ +import itertools + from django.db import models from django.db.models import FieldDoesNotExist from django.db.models.fields.related import RelatedField, RelatedObject @@ -46,14 +48,47 @@ def rewrite_lookup_key(model, lookup_key): 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: - # Check ``original key``, as pieces[0] may have been already rewritten. - if original_key == field_to_trans: - pieces[1] = rewrite_lookup_key(transmodel, pieces[1]) - break + # Check ``original key``, as pieces[0] may have been already rewritten. + if original_key in fields_to_trans_models: + transmodel = fields_to_trans_models[original_key] + pieces[1] = rewrite_lookup_key(transmodel, pieces[1]) return '__'.join(pieces) +def append_translated(model, fields): + "If translated field is encountered, add also all its translation fields." + fields = set(fields) + from modeltranslation.translator import translator + opts = translator.get_options_for_model(model) + for key, translated in opts.fields.items(): + if key in fields: + fields = fields.union(f.name for f in translated) + return fields + + +def append_lookup_key(model, lookup_key): + "Transform spanned__lookup__key into all possible translation versions, on all levels" + pieces = lookup_key.split('__', 1) + + fields = append_translated(model, (pieces[0],)) + + 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) + if pieces[0] in fields_to_trans_models: + transmodel = fields_to_trans_models[pieces[0]] + rest = append_lookup_key(transmodel, pieces[1]) + fields = set('__'.join(pr) for pr in itertools.product(fields, rest)) + else: + fields = set('%s__%s' % (f, pieces[1]) for f in fields) + return fields + + +def append_lookup_keys(model, fields): + union = lambda x, y: x.union(y) + return reduce(union, (append_lookup_key(model, field) for field in fields), set()) + + def rewrite_order_lookup_key(model, lookup_key): if lookup_key.startswith('-'): return '-' + rewrite_lookup_key(model, lookup_key[1:]) @@ -76,7 +111,7 @@ def get_fields_to_translatable_models(model): if isinstance(field_object, RelatedObject): if get_translatable_fields_for_model(field_object.model) is not None: results.append((field_name, field_object.model)) - _F2TM_CACHE[model] = results + _F2TM_CACHE[model] = dict(results) return _F2TM_CACHE[model] _C2F_CACHE = {} @@ -154,9 +189,13 @@ def _rewrite_applied_operations(self): def select_related(self, *fields, **kwargs): if not self._rewrite: return super(MultilingualQuerySet, self).select_related(*fields, **kwargs) + # TO CONSIDER: whether this should rewrite only current language, or all languages? + # fk -> [fk, fk_en] (with en=active) VS fk -> [fk, fk_en, fk_de, fk_fr ...] (for all langs) + + # new_args = append_lookup_keys(self.model, fields) new_args = [] for key in fields: - new_args.append(rewrite_order_lookup_key(self.model, key)) + new_args.append(rewrite_lookup_key(self.model, key)) return super(MultilingualQuerySet, self).select_related(*new_args, **kwargs) # This method was not present in django-linguo @@ -283,24 +322,14 @@ def get_or_create(self, **kwargs): with auto_populate(self._populate_mode): return super(MultilingualQuerySet, self).get_or_create(**kwargs) - def _append_translated(self, fields): - "If translated field is encountered, add also all its translation fields." - fields = set(fields) - from modeltranslation.translator import translator - opts = translator.get_options_for_model(self.model) - for key, translated in opts.fields.items(): - if key in fields: - fields = fields.union(f.name for f in translated) - return fields - # This method was not present in django-linguo def defer(self, *fields): - fields = self._append_translated(fields) + fields = append_lookup_keys(self.model, fields) return super(MultilingualQuerySet, self).defer(*fields) # This method was not present in django-linguo def only(self, *fields): - fields = self._append_translated(fields) + fields = append_lookup_keys(self.model, fields) return super(MultilingualQuerySet, self).only(*fields) # This method was not present in django-linguo diff --git a/modeltranslation/tests/models.py b/modeltranslation/tests/models.py index 831b4f4d..e00ca86a 100644 --- a/modeltranslation/tests/models.py +++ b/modeltranslation/tests/models.py @@ -64,6 +64,7 @@ class ForeignKeyModel(models.Model): optional = models.ForeignKey(TestModel, blank=True, null=True) hidden = models.ForeignKey(TestModel, blank=True, null=True, related_name="+") non = models.ForeignKey(NonTranslated, blank=True, null=True, related_name="test_fks") + untrans = models.ForeignKey(TestModel, blank=True, null=True, related_name="test_fks_un") class OneToOneFieldModel(models.Model): diff --git a/modeltranslation/tests/tests.py b/modeltranslation/tests/tests.py index a2feff15..f8df3514 100644 --- a/modeltranslation/tests/tests.py +++ b/modeltranslation/tests/tests.py @@ -2737,6 +2737,39 @@ def test_deferred_fk(self): self.assertEqual('title_de', item.test.title) self.assertEqual('title_de', item.test.__class__.objects.only('title')[0].title) + def test_deferred_spanning(self): + test = models.TestModel.objects.create(title_de='title_de', title_en='title_en') + with auto_populate('all'): + models.ForeignKeyModel.objects.create(test=test) + + item1 = models.ForeignKeyModel.objects.select_related("test").defer("test__text")[0].test + item2 = models.TestModel.objects.defer("text")[0] + self.assertIs(item1.__class__, item2.__class__) + # DeferredAttribute descriptors are present + self.assertIn('text_en', dir(item1.__class__)) + self.assertIn('text_de', dir(item1.__class__)) + + def test_translation_fields_appending(self): + from modeltranslation.manager import append_lookup_keys, append_lookup_key + self.assertEqual(set(['untrans']), append_lookup_key(models.ForeignKeyModel, 'untrans')) + self.assertEqual(set(['title', 'title_en', 'title_de']), + append_lookup_key(models.ForeignKeyModel, 'title')) + self.assertEqual(set(['test', 'test_en', 'test_de']), + append_lookup_key(models.ForeignKeyModel, 'test')) + self.assertEqual(set(['title__eq', 'title_en__eq', 'title_de__eq']), + append_lookup_key(models.ForeignKeyModel, 'title__eq')) + self.assertEqual(set(['test__smt', 'test_en__smt', 'test_de__smt']), + append_lookup_key(models.ForeignKeyModel, 'test__smt')) + big_set = set(['test__url', 'test__url_en', 'test__url_de', + 'test_en__url', 'test_en__url_en', 'test_en__url_de', + 'test_de__url', 'test_de__url_en', 'test_de__url_de']) + self.assertEqual(big_set, append_lookup_key(models.ForeignKeyModel, 'test__url')) + self.assertEqual(set(['untrans__url', 'untrans__url_en', 'untrans__url_de']), + append_lookup_key(models.ForeignKeyModel, 'untrans__url')) + + self.assertEqual(big_set.union(['title', 'title_en', 'title_de']), + append_lookup_keys(models.ForeignKeyModel, ['test__url', 'title'])) + def test_constructor_inheritance(self): inst = models.AbstractModelB() # Check if fields assigned in constructor hasn't been ignored. diff --git a/modeltranslation/translator.py b/modeltranslation/translator.py index d0335790..c50a5c45 100644 --- a/modeltranslation/translator.py +++ b/modeltranslation/translator.py @@ -487,7 +487,7 @@ def _get_options_for_model(self, model, opts_class=None, **options): defined for the ``model`` and inherited from superclasses. """ if model._deferred: - model = model.__bases__[0] + model = model._meta.proxy_for_model if model not in self._registry: # Create a new type for backwards compatibility. opts = type("%sTranslationOptions" % model.__name__, From 5b2492cdc0a3b3ec3fca0bc2155990e855dd7c62 Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Thu, 10 Jul 2014 09:43:49 +0200 Subject: [PATCH 051/170] Fix reduce usage under Python3. --- modeltranslation/manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modeltranslation/manager.py b/modeltranslation/manager.py index 5a86cf60..4e5de319 100644 --- a/modeltranslation/manager.py +++ b/modeltranslation/manager.py @@ -11,6 +11,7 @@ from django.db.models import FieldDoesNotExist from django.db.models.fields.related import RelatedField, RelatedObject from django.db.models.sql.where import Constraint +from django.utils.six import moves from django.utils.tree import Node try: from django.db.models.lookups import Lookup @@ -85,8 +86,7 @@ def append_lookup_key(model, lookup_key): def append_lookup_keys(model, fields): - union = lambda x, y: x.union(y) - return reduce(union, (append_lookup_key(model, field) for field in fields), set()) + return moves.reduce(set.union, (append_lookup_key(model, field) for field in fields), set()) def rewrite_order_lookup_key(model, lookup_key): From e4e325a8e768b1f88d0ea9514e75d2bd0fe9826b Mon Sep 17 00:00:00 2001 From: Dirk Eschler Date: Fri, 11 Jul 2014 12:37:41 +0200 Subject: [PATCH 052/170] Fixed translation fields in migrations don't include language code (close #253). --- modeltranslation/fields.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modeltranslation/fields.py b/modeltranslation/fields.py index f2b09e79..38a28a03 100644 --- a/modeltranslation/fields.py +++ b/modeltranslation/fields.py @@ -250,7 +250,8 @@ def save_form_data(self, instance, data, check=True): super(TranslationField, self).save_form_data(instance, data) def deconstruct(self): - return self.translated_field.deconstruct() + name, path, args, kwargs = self.translated_field.deconstruct() + return unicode(self.name), path, args, kwargs def south_field_triple(self): """ From fd911bb37b4092a43671311c9ee19d8293a2f39e Mon Sep 17 00:00:00 2001 From: Dirk Eschler Date: Fri, 11 Jul 2014 13:31:57 +0200 Subject: [PATCH 053/170] Fixed unicode call in deconstruct to maintain Python3 compatibility. --- modeltranslation/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modeltranslation/fields.py b/modeltranslation/fields.py index 38a28a03..77354d0c 100644 --- a/modeltranslation/fields.py +++ b/modeltranslation/fields.py @@ -251,7 +251,7 @@ def save_form_data(self, instance, data, check=True): def deconstruct(self): name, path, args, kwargs = self.translated_field.deconstruct() - return unicode(self.name), path, args, kwargs + return self.name.decode('utf-8'), path, args, kwargs def south_field_triple(self): """ From ee32cce0c23a4b713c14996bed04b5da295c694f Mon Sep 17 00:00:00 2001 From: Dirk Eschler Date: Fri, 11 Jul 2014 13:53:31 +0200 Subject: [PATCH 054/170] Added Python 3.4 and changed Django to 1.7rc1 (ref #254). --- .travis.yml | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index 59d53177..523d5c67 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ python: - "2.7" - "3.2" - "3.3" + - "3.4" env: - DJANGO=1.4 DB=sqlite - DJANGO=1.4 DB=postgres @@ -14,9 +15,9 @@ env: - DJANGO=1.6 DB=sqlite - DJANGO=1.6 DB=postgres - DJANGO=1.6 DB=mysql - - DJANGO=https://www.djangoproject.com/download/1.7.b4/tarball DB=sqlite - - DJANGO=https://www.djangoproject.com/download/1.7.b4/tarball DB=postgres - - DJANGO=https://www.djangoproject.com/download/1.7.b4/tarball DB=mysql + - DJANGO=https://www.djangoproject.com/download/1.7c1/tarball DB=sqlite + - DJANGO=https://www.djangoproject.com/download/1.7c1/tarball DB=postgres + - DJANGO=https://www.djangoproject.com/download/1.7c1/tarball DB=mysql matrix: exclude: - python: "3.2" @@ -31,26 +32,38 @@ matrix: env: DJANGO=1.4 DB=postgres - python: "3.3" env: DJANGO=1.4 DB=mysql + - python: "3.4" + env: DJANGO=1.4 DB=sqlite + - python: "3.4" + env: DJANGO=1.4 DB=postgres + - python: "3.4" + env: DJANGO=1.4 DB=mysql - python: "2.6" - env: DJANGO=https://www.djangoproject.com/download/1.7.b4/tarball DB=sqlite + env: DJANGO=https://www.djangoproject.com/download/1.7c1/tarball DB=sqlite - python: "2.6" - env: DJANGO=https://www.djangoproject.com/download/1.7.b4/tarball DB=postgres + env: DJANGO=https://www.djangoproject.com/download/1.7c1/tarball DB=postgres - python: "2.6" - env: DJANGO=https://www.djangoproject.com/download/1.7.b4/tarball DB=mysql + env: DJANGO=https://www.djangoproject.com/download/1.7c1/tarball DB=mysql - python: "3.2" env: DJANGO=1.5 DB=mysql - python: "3.3" env: DJANGO=1.5 DB=mysql + - python: "3.4" + env: DJANGO=1.5 DB=mysql - python: "3.2" env: DJANGO=1.6 DB=mysql - python: "3.3" env: DJANGO=1.6 DB=mysql + - python: "3.4" + env: DJANGO=1.6 DB=mysql - python: "3.2" - env: DJANGO=https://www.djangoproject.com/download/1.7.b4/tarball DB=mysql + env: DJANGO=https://www.djangoproject.com/download/1.7c1/tarball DB=mysql - python: "3.3" - env: DJANGO=https://www.djangoproject.com/download/1.7.b4/tarball DB=mysql + env: DJANGO=https://www.djangoproject.com/download/1.7c1/tarball DB=mysql + - python: "3.4" + env: DJANGO=https://www.djangoproject.com/download/1.7c1/tarball DB=mysql before_install: - pip install -q flake8 --use-mirrors - PYFLAKES_NODOCTEST=1 flake8 --max-line-length=100 modeltranslation From f7acd8a688a2a953f9505dcaea2907b5ea3bdc57 Mon Sep 17 00:00:00 2001 From: Dirk Eschler Date: Fri, 11 Jul 2014 14:40:38 +0200 Subject: [PATCH 055/170] Used six compatibility layer (ref #253). --- modeltranslation/fields.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modeltranslation/fields.py b/modeltranslation/fields.py index 77354d0c..ea78064e 100644 --- a/modeltranslation/fields.py +++ b/modeltranslation/fields.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +import six + from django import forms from django.core.exceptions import ImproperlyConfigured from django.db.models import fields @@ -251,7 +253,7 @@ def save_form_data(self, instance, data, check=True): def deconstruct(self): name, path, args, kwargs = self.translated_field.deconstruct() - return self.name.decode('utf-8'), path, args, kwargs + return six.text_type(self.name), path, args, kwargs def south_field_triple(self): """ From 3e53ec2be1224e6b3f111f5faa4230c0412370c3 Mon Sep 17 00:00:00 2001 From: Dirk Eschler Date: Fri, 11 Jul 2014 14:43:47 +0200 Subject: [PATCH 056/170] Indicated Python 3.4 support (close #254). --- docs/modeltranslation/installation.rst | 2 +- setup.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/modeltranslation/installation.rst b/docs/modeltranslation/installation.rst index 2a888ecd..d061bbca 100644 --- a/docs/modeltranslation/installation.rst +++ b/docs/modeltranslation/installation.rst @@ -9,7 +9,7 @@ Requirements +------------------+------------+-----------+ | Modeltranslation | Python | Django | +==================+============+===========+ -| >=0.8 | 3.2 - 3.3 | 1.5 - 1.7 | +| >=0.8 | 3.2 - 3.4 | 1.5 - 1.7 | | +------------+-----------+ | | 2.7 | 1.7 | | +------------+-----------+ diff --git a/setup.py b/setup.py index bc00bfd8..296361bf 100755 --- a/setup.py +++ b/setup.py @@ -34,6 +34,7 @@ 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.2', 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', 'Operating System :: OS Independent', 'Environment :: Web Environment', 'Intended Audience :: Developers', From c4d6c82f95b98a6b56ee186c373ba0cd9729848c Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Fri, 11 Jul 2014 15:04:03 +0200 Subject: [PATCH 057/170] Fix six import (ref #253). --- modeltranslation/fields.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/modeltranslation/fields.py b/modeltranslation/fields.py index ea78064e..cc9f9b6e 100644 --- a/modeltranslation/fields.py +++ b/modeltranslation/fields.py @@ -1,9 +1,8 @@ # -*- coding: utf-8 -*- -import six - from django import forms from django.core.exceptions import ImproperlyConfigured from django.db.models import fields +from django.utils import six from modeltranslation import settings as mt_settings from modeltranslation.utils import ( From 3281547a9e5e7066b78d283ecb05ce3b976eb627 Mon Sep 17 00:00:00 2001 From: Dirk Eschler Date: Mon, 14 Jul 2014 11:07:17 +0200 Subject: [PATCH 058/170] Respect null option of TranslationField in migrations. --- modeltranslation/fields.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modeltranslation/fields.py b/modeltranslation/fields.py index cc9f9b6e..f77f419d 100644 --- a/modeltranslation/fields.py +++ b/modeltranslation/fields.py @@ -252,6 +252,8 @@ def save_form_data(self, instance, data, check=True): def deconstruct(self): name, path, args, kwargs = self.translated_field.deconstruct() + if self.null is True: + kwargs.update({'null': True}) return six.text_type(self.name), path, args, kwargs def south_field_triple(self): From 1f419ee5f49f1038779c38af2f4d5f93c2d95e54 Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Wed, 16 Jul 2014 09:47:17 +0200 Subject: [PATCH 059/170] Fix dict iteration Exception under Python3 (close #256). --- modeltranslation/manager.py | 4 ++-- modeltranslation/tests/tests.py | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/modeltranslation/manager.py b/modeltranslation/manager.py index 4e5de319..03392c0f 100644 --- a/modeltranslation/manager.py +++ b/modeltranslation/manager.py @@ -267,7 +267,7 @@ def _filter_or_exclude(self, negate, *args, **kwargs): if not self._rewrite: return super(MultilingualQuerySet, self)._filter_or_exclude(negate, *args, **kwargs) args = map(self._rewrite_q, args) - for key, val in kwargs.items(): + for key, val in list(kwargs.items()): new_key = rewrite_lookup_key(self.model, key) del kwargs[key] kwargs[new_key] = self._rewrite_f(val) @@ -291,7 +291,7 @@ def order_by(self, *field_names): def update(self, **kwargs): if not self._rewrite: return super(MultilingualQuerySet, self).update(**kwargs) - for key, val in kwargs.items(): + for key, val in list(kwargs.items()): new_key = rewrite_lookup_key(self.model, key) del kwargs[key] kwargs[new_key] = self._rewrite_f(val) diff --git a/modeltranslation/tests/tests.py b/modeltranslation/tests/tests.py index f8df3514..5cc6e43d 100644 --- a/modeltranslation/tests/tests.py +++ b/modeltranslation/tests/tests.py @@ -2366,6 +2366,10 @@ def test_filter_update(self): self.assertEqual('title en', m.title_en) self.assertEqual('new', m.title_de) + # Test Python3 "dictionary changed size during iteration" + self.assertEqual(1, models.ManagerTestModel.objects.filter(title='en', + title_en='en').count()) + def test_q(self): """Test if Q queries are rewritten.""" n = models.ManagerTestModel(title='') From ca453533e4c83fc78b22be8afd95d3b7efd4aa73 Mon Sep 17 00:00:00 2001 From: Dirk Eschler Date: Fri, 18 Jul 2014 10:57:55 +0200 Subject: [PATCH 060/170] Prepared 0.8b2 release. --- AUTHORS.rst | 1 + CHANGELOG.txt | 23 +++++++++++++++++++++++ PKG-INFO | 2 +- modeltranslation/__init__.py | 2 +- 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 196e415a..ef41b26b 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -30,6 +30,7 @@ Contributors * Francesc Arpí Roca * Mathieu Leplatre * Thom Wiggers +* Warnar Boekkooi * And many more ... (if you miss your name here, please let us know!) .. _django-linguo: https://github.com/zmathew/django-linguo diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 3f86a06d..8975869a 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,26 @@ +v0.8b2 +====== +Date: 2014-07-18 + + ADDED: Explicit support for Python 3.4 (should have already worked for + older versions that supported Python 3). + (resolves issue #254) + ADDED: Support for Django 1.7 migrations. + + FIXED: Dict iteration Exception under Python 3. + (resolves issue #256, + thanks Jacek Tomaszewski) + FIXED: Reduce usage under Python 3. + (thanks Jacek Tomaszewski) + FIXED: Support for AppConfigs in INSTALLED_APPS + (resolves issue #252, + thanks Warnar Boekkooi, Jacek Tomaszewski) + FIXED: Rewrite field names in select_related. Fix deffered models registry. + Rewrite spanned queries on all levels for defer/only. + (resolves issue #248, + thanks Jacek Tomaszewski) + + v0.8b1 ====== Date: 2014-06-22 diff --git a/PKG-INFO b/PKG-INFO index 62b7b098..1580d877 100644 --- a/PKG-INFO +++ b/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 1.0 Name: django-modeltranslation -Version: 0.8b1 +Version: 0.8b2 Summary: Translates Django models using a registration approach. Home-page: https://github.com/deschler/django-modeltranslation Author: Peter Eschler, diff --git a/modeltranslation/__init__.py b/modeltranslation/__init__.py index 5215da39..07ffc579 100644 --- a/modeltranslation/__init__.py +++ b/modeltranslation/__init__.py @@ -3,7 +3,7 @@ Version code adopted from Django development version. https://github.com/django/django """ -VERSION = (0, 8, 0, 'beta', 1) +VERSION = (0, 8, 0, 'beta', 2) default_app_config = 'modeltranslation.apps.ModeltranslationConfig' From 756de544aa7bbb2d965c8e02f1206c1cb330ddc2 Mon Sep 17 00:00:00 2001 From: deschler Date: Sun, 20 Jul 2014 01:58:00 +0200 Subject: [PATCH 061/170] General documentation improvements. --- README.rst | 13 +++--- docs/modeltranslation/admin.rst | 4 +- docs/modeltranslation/commands.rst | 28 +++++++------ docs/modeltranslation/forms.rst | 29 +++++++------- docs/modeltranslation/installation.rst | 35 ++++++++-------- docs/modeltranslation/registration.rst | 53 +++++++++++++------------ docs/modeltranslation/usage.rst | 55 +++++++++++++------------- 7 files changed, 112 insertions(+), 105 deletions(-) diff --git a/README.rst b/README.rst index e2f57340..11412881 100644 --- a/README.rst +++ b/README.rst @@ -2,7 +2,7 @@ Modeltranslation ================ -The modeltranslation application can be used to translate dynamic content of +The modeltranslation application is used to translate dynamic content of existing Django models to an arbitrary number of languages without having to change the original model classes. It uses a registration approach (comparable to Django's admin app) to be able to add translations to existing or new @@ -21,11 +21,12 @@ model class. Features ======== -- Add translations without changing existing models -- Fast, because translation fields are stored in the same table -- Supports inherited models -- Django admin support -- Unlimited number of target languages +- Add translations without changing existing models or views +- Translation fields are stored in the same table (no expensive joins) +- Supports inherited models (abstract and multi-table inheritance) +- Handle more than just text fields +- Django admin integration +- Flexible fallbacks, auto-population and more! Project Home diff --git a/docs/modeltranslation/admin.rst b/docs/modeltranslation/admin.rst index b7f6c4b6..99423b83 100644 --- a/docs/modeltranslation/admin.rst +++ b/docs/modeltranslation/admin.rst @@ -30,7 +30,7 @@ formfield_for_dbfield The ``TranslationBaseModelAdmin`` class, which ``TranslationAdmin`` and all inline related classes in modeltranslation derive from, implements a special -method which is ``def formfield_for_dbfield(self, db_field, **kwargs)``. This +method which is ``formfield_for_dbfield(self, db_field, **kwargs)``. This method does the following: 1. Copies the widget of the original field to each of its translation fields. @@ -78,7 +78,7 @@ TranslationAdmin in Combination with Other Admin Classes If there already exists a custom admin class for a translated model and you don't want or can't edit that class directly there is another solution. -Taken a (fictional) reusable blog app which defines a model ``Entry`` and a +Taken a reusable blog app which defines a model ``Entry`` and a corresponding admin class called ``EntryAdmin``. This app is not yours and you don't want to touch it at all. diff --git a/docs/modeltranslation/commands.rst b/docs/modeltranslation/commands.rst index fc1a06e5..fe76dec7 100644 --- a/docs/modeltranslation/commands.rst +++ b/docs/modeltranslation/commands.rst @@ -8,19 +8,19 @@ Management Commands The ``update_translation_fields`` Command ----------------------------------------- -In case the modeltranslation app was installed on an existing project and you +In case modeltranslation was installed in an existing project and you have specified to translate fields of models which are already synced to the database, you have to update your database schema (see :ref:`db-fields`). Unfortunately the newly added translation fields on the model will be empty then, and your templates will show the translated value of the fields (see -Rule 1) which will be empty in this case. To correctly initialize the -default translation field you can use the ``update_translation_fields`` +:ref:`Rule 1 `) which will be empty in this case. To correctly initialize +the default translation field you can use the ``update_translation_fields`` command: .. code-block:: console - $ ./manage.py update_translation_fields + $ python manage.py update_translation_fields Taken the news example used throughout the documentation this command will copy the value from the news object's ``title`` field to the default translation @@ -47,11 +47,12 @@ The ``sync_translation_fields`` Command .. code-block:: console - $ ./manage.py sync_translation_fields + $ python manage.py sync_translation_fields This command compares the database and translated models definitions (finding new translation fields) and provides SQL statements to alter tables. You should run this command after adding -new language or deciding to translate new field in a ``TranslationOptions``. +a new language to your ``settings.LANGUAGES`` or a new field to the ``TranslationOptions`` of +a registered model. However, if you are using South in your project, in most cases it's recommended to use migration instead of ``sync_translation_fields``. See :ref:`db-fields` for detailed info and use cases. @@ -62,28 +63,29 @@ The ``loaddata`` Command .. versionadded:: 0.7 -It is just extension to original ``loaddata`` command which adds an optional ``populate`` keyword. -If specified, then normal loading command will be run under selected auto-population modes. +An extended version of Django's original ``loaddata`` command which adds an optional +``populate`` keyword. If the keyword is specified, the normal loading command will be +run under the selected auto-population modes. By default no auto-population is performed. .. code-block:: console - $ ./manage.py loaddata --populate=all fixtures.json + $ python manage.py loaddata --populate=all fixtures.json Allowed modes are listed :ref:`here `. To choose ``False`` (turn off auto-population) specify ``'0'`` or ``'false'``: .. code-block:: console - $ ./manage.py loaddata --populate=false fixtures.json - $ ./manage.py loaddata --populate=0 fixtures.json + $ python manage.py loaddata --populate=false fixtures.json + $ python manage.py loaddata --populate=0 fixtures.json .. note:: - If ``populate`` is not specified, then current auto-population mode is used. *Current* means + If ``populate`` is not specified, the current auto-population mode is used. *Current* means the one set by :ref:`settings `. Moreover, this ``loaddata`` command version can override the nasty habit of changing locale to -`en-us`. By default, it will retain proper locale. To get back to old behaviour, set +`en-us`. By default, it will retain the proper locale. To get the old behaviour back, set :ref:`settings-modeltranslation_loaddata_retain_locale` to ``False``. diff --git a/docs/modeltranslation/forms.rst b/docs/modeltranslation/forms.rst index 6d98190b..45787492 100644 --- a/docs/modeltranslation/forms.rst +++ b/docs/modeltranslation/forms.rst @@ -4,7 +4,8 @@ ModelForms ========== ``ModelForms`` for multilanguage models are defined and handled as typical ``ModelForms``. -Please note, however, that they shouldn't be defined next to models (see :ref:`a note `). +Please note, however, that they shouldn't be defined next to models +(see :ref:`a note `). Editing multilanguage models with all translation fields in the admin backend is quite sensible. However, presenting all model fields to the user on the frontend may be not the right way. @@ -37,18 +38,18 @@ In most cases formfields for translation fields behave as expected. However, the problem with ``models.CharField`` - probably the most commonly translated field type. The problem is that default formfield for ``CharField`` stores empty values as empty strings -(``''``), even if field is nullable +(``''``), even if the field is nullable (see django `ticket #9590 `_). -Thus formfields for translation fields are patched by `MT`. Following rules apply: +Thus formfields for translation fields are patched by modeltranslation. The following rules apply: .. _formfield_rules: -- If original field is not nullable, empty value would be saved as ``''``; -- If original field is nullable, empty value would be saved as ``None``. +- If the original field is not nullable, an empty value is saved as ``''``; +- If the original field is nullable, an empty value is saved as ``None``. To deal with complex cases, these rules can be overridden per model or even per field -(using ``TranslationOptions``):: +using ``TranslationOptions``:: class NewsTranslationOptions(TranslationOptions): fields = ('title', 'text',) @@ -68,9 +69,9 @@ This configuration is especially useful for fields with unique constraints:: slug = models.SlugField(max_length=30, unique=True) Because the ``slug`` field is not nullable, its translation fields would store empty values as -``''`` and that would result in error when 2 or more ``Categories`` are saved with +``''`` and that would result in an error when two or more ``Categories`` are saved with ``slug_en`` empty - unique constraints wouldn't be satisfied. Instead, ``None`` should be stored, -as several ``None`` values in database don't violate uniqueness:: +as several ``None`` values in the database don't violate uniqueness:: class CategoryTranslationOptions(TranslationOptions): fields = ('name', 'slug') @@ -82,15 +83,15 @@ as several ``None`` values in database don't violate uniqueness:: None-checkbox widget ******************** -Maybe there is a situation when somebody want to store in a field both empty strings and ``None`` -values. For such a scenario there is third configuration value: ``'both'``:: +Maybe there is a situation where you want to store both - empty strings and ``None`` +values - in a field. For such a scenario there is a third configuration value: ``'both'``:: class NewsTranslationOptions(TranslationOptions): fields = ('title', 'text',) empty_values = {'title': None, 'text': 'both'} -It results in special widget with a None-checkbox to null a field. It's not recommended in frontend -as users may be confused what this `None` is. Probably only useful place for this widget is admin -backend; see :ref:`admin-formfield`. +It results in a special widget with a None-checkbox to null a field. It's not recommended in +frontend as users may be confused what this `None` is. The only useful place for this widget might +be the admin backend; see :ref:`admin-formfield`. -To sum up, only valid ``empty_values`` values are: ``None``, ``''`` and ``'both'``. +To sum it up, the valid values for ``empty_values`` are: ``None``, ``''`` and ``'both'``. diff --git a/docs/modeltranslation/installation.rst b/docs/modeltranslation/installation.rst index d061bbca..4b8ffead 100644 --- a/docs/modeltranslation/installation.rst +++ b/docs/modeltranslation/installation.rst @@ -59,20 +59,20 @@ Setup To setup the application please follow these steps. Each step is described in detail in the following sections: -1. Add the ``modeltranslation`` app to the ``INSTALLED_APPS`` variable of your +1. Add ``modeltranslation`` to the ``INSTALLED_APPS`` variable of your project's ``settings.py``. -#. Set ``USE_I18N = True`` in ``settings.py``. +2. Set ``USE_I18N = True`` in ``settings.py``. -#. Configure your ``LANGUAGES`` in ``settings.py``. +3. Configure your ``LANGUAGES`` in ``settings.py``. -#. Create a ``translation.py`` in your app directory and register +4. Create a ``translation.py`` in your app directory and register ``TranslationOptions`` for every model you want to translate. -#. Sync the database using ``./manage.py syncdb`` (note that this only applies - if the models registered in the ``translation.py`` did not have been - synced to the database before. If they did - read :ref:`further down ` what to do - in that case. +5. Sync the database using ``python manage.py syncdb``. + + .. note:: This only applies if the models registered in ``translation.py`` haven't been + synced to the database before. If they have, please read :ref:`db-fields`. Configuration @@ -94,6 +94,7 @@ Make sure that the ``modeltranslation`` app is listed in your INSTALLED_APPS = ( ... 'modeltranslation', + 'django.contrib.admin', # optional .... ) @@ -102,9 +103,9 @@ Make sure that the ``modeltranslation`` app is listed in your before ``django.contrib.admin``. .. important:: - If you want to use the ``django-debug-toolbar`` together with `MT`, put ``debug_toolbar`` - as first entry in ``INSTALLED_APPS`` or use - `explicit setup + If you want to use the ``django-debug-toolbar`` together with + modeltranslation, put ``debug_toolbar`` as first entry in + ``INSTALLED_APPS`` or use `explicit setup `_. .. _settings-languages: @@ -115,10 +116,10 @@ Make sure that the ``modeltranslation`` app is listed in your The ``LANGUAGES`` variable must contain all languages used for translation. The first language is treated as the *default language*. -The modeltranslation application uses the list of languages to add localized -fields to the models registered for translation. To use the languages ``de`` -and ``en`` in your project, set the ``LANGUAGES`` variable like this (where -``de`` is the default language):: +Modeltranslation uses the list of languages to add localized fields to the +models registered for translation. To use the languages ``de`` and ``en`` in +your project, set the ``LANGUAGES`` variable like this (where ``de`` is the +default language):: gettext = lambda s: s LANGUAGES = ( @@ -144,7 +145,7 @@ and ``en`` in your project, set the ``LANGUAGES`` variable like this (where in your project. When it isn't present (and neither is ``MODELTRANSLATION_LANGUAGES``), it defaults to Django's `global LANGUAGES setting `_ - instead, and that are quite a number of languages! + instead, and that are quite a few languages! Advanced Settings @@ -379,4 +380,4 @@ Default: ``True`` Control if the ``loaddata`` command should leave the settings-defined locale alone. Setting it to ``False`` will result in previous behaviour of ``loaddata``: inserting fixtures to database -under `en-us` locale. +under ``en-us`` locale. diff --git a/docs/modeltranslation/registration.rst b/docs/modeltranslation/registration.rst index 7db585b4..ba10c079 100644 --- a/docs/modeltranslation/registration.rst +++ b/docs/modeltranslation/registration.rst @@ -12,7 +12,7 @@ steps: 1. Create a ``translation.py`` in your app directory. 2. Create a translation option class for every model to translate. -3. Register the model and the translation option class at the +3. Register the model and the translation option class at ``modeltranslation.translator.translator``. The modeltranslation application reads the ``translation.py`` file in your @@ -33,9 +33,9 @@ Instead of a news, this could be any Django model class:: title = models.CharField(max_length=255) text = models.TextField() -In order to tell the modeltranslation app to translate the ``title`` and -``text`` field, create a ``translation.py`` file in your news app directory and -add the following:: +In order to tell modeltranslation to translate the ``title`` and ``text`` fields, +create a ``translation.py`` file in your news app directory and add the +following:: from modeltranslation.translator import translator, TranslationOptions from news.models import News @@ -86,7 +86,7 @@ say it in code:: Of course multiple inheritance and inheritance chains (A > B > C) also work as expected. -.. note:: When upgrading from a previous modeltranslation version, please +.. note:: When upgrading from a previous modeltranslation version (<0.5), please review your ``TranslationOptions`` classes and see if introducing `fields inheritance` broke the project (if you had always subclassed ``TranslationOptions`` only, there is no risk). @@ -137,17 +137,18 @@ Be aware that registration approach (as opposed to base-class approach) to models translation has a few caveats, though (despite many pros). First important thing to note is the fact that translatable models are being patched - that means -their fields list is not final until the `MT` code executes. In normal circumstances it shouldn't -affect anything - as long as ``models.py`` contain only models' related code. +their fields list is not final until the modeltranslation code executes. In normal circumstances +it shouldn't affect anything - as long as ``models.py`` contain only models' related code. -For example: consider a project when a ``ModelForm`` is declared in ``models.py`` just after +For example: consider a project where a ``ModelForm`` is declared in ``models.py`` just after its model. When the file is executed, the form gets prepared - but it will be frozen with -old fields list (without translation fields). That's because ``ModelForm`` will be created before -`MT` would add new fields to the model (``ModelForm`` gather fields info at class creation time, not -instantiation time). Proper solution is to define the form in ``forms.py``, which wouldn't be -imported alongside with ``models.py`` (and rather imported from views file or urlconf). +old fields list (without translation fields). That's because the ``ModelForm`` will be created +before modeltranslation would add new fields to the model (``ModelForm`` gather fields info at class +creation time, not instantiation time). Proper solution is to define the form in ``forms.py``, +which wouldn't be imported alongside with ``models.py`` (and rather imported from views file or +urlconf). -Generally, for seamless integration with `MT` (and as sensible design, anyway), +Generally, for seamless integration with modeltranslation (and as sensible design anyway), the ``models.py`` should contain only bare models and model related logic. .. _db-fields: @@ -168,22 +169,22 @@ fields) and apply it. If not, you can use a little helper: :ref:`commands-sync_translation_fields` which can execute schema-ALTERing SQL to add new fields. Use either of these two solutions, not both. -If you are adding translation fields to third-party app that is using South, -things get more complicated. In order to be able to update the app in future, +If you are adding translation fields to a third-party app that is using South, +things get more complicated. In order to be able to update the app in the future, and to feel comfortable, you should use the ``sync_translation_fields`` command. Although it's possible to introduce new fields in a migration, it's nasty and involves copying migration files, using ``SOUTH_MIGRATION_MODULES`` setting, and passing ``--delete-ghost-migrations`` flag, so we don't recommend it. Invoking ``sync_translation_fields`` is plain easier. -Note that all added fields are by default -declared ``blank=True`` and ``null=True`` no matter if the original field is -required or not. In other words - all translations are optional, unless an explicit option -is provided - see below. +Note that all added fields are by default declared ``blank=True`` and +``null=True`` no matter if the original field is required or not. In other +words - all translations are optional, unless an explicit option is +provided - see below. -To populate the default translation fields added by the modeltranslation application -with values from existing database fields, you -can use the ``update_translation_fields`` command below. See +To populate the default translation fields added by modeltranslation with +values from existing database fields, you can use the +``update_translation_fields`` command below. See :ref:`commands-update_translation_fields` for more info on this. @@ -194,14 +195,14 @@ Required fields .. versionadded:: 0.8 -By default, all translation fields are optional (not required). It can be changed using special -attribute on ``TranslationOptions``, though:: +By default, all translation fields are optional (not required). This can be +changed using a special attribute on ``TranslationOptions``:: class NewsTranslationOptions(TranslationOptions): fields = ('title', 'text',) required_languages = ('en', 'de') -It quite self-explanatory: for German and English, all translation fields are required. For other +It's quite self-explanatory: for German and English, all translation fields are required. For other languages - optional. A more fine-grained control is available:: @@ -216,7 +217,7 @@ For German, all fields (both ``title`` and ``text``) are required; for all other .. note:: Requirement is enforced by ``blank=False``. Please remember that it will trigger validation only in modelforms and admin (as always in Django). Manual model validation can be performed via - ``full_clean()`` model method. + the ``full_clean()`` model method. The required fields are still ``null=True``, though. diff --git a/docs/modeltranslation/usage.rst b/docs/modeltranslation/usage.rst index 1ad3f884..cb3df69b 100644 --- a/docs/modeltranslation/usage.rst +++ b/docs/modeltranslation/usage.rst @@ -3,7 +3,7 @@ Accessing Translated and Translation Fields =========================================== -The modeltranslation app changes the behaviour of the translated fields. To +Modeltranslation changes the behaviour of the translated fields. To explain this consider the news example from the :ref:`registration` chapter again. The original ``News`` model looked like this:: @@ -11,7 +11,7 @@ again. The original ``News`` model looked like this:: title = models.CharField(max_length=255) text = models.TextField() -Now that it is registered with the modeltranslation app the model looks +Now that it is registered with modeltranslation the model looks like this - note the additional fields automatically added by the app:: class News(models.Model): @@ -160,21 +160,22 @@ Moreover, some fields can be explicitly assigned different values:: It will result in ``title_de == 'enigma'`` and other ``title_?? == '-- no translation yet --'``. -There is another way of altering the current population status, an ``auto_populate`` context manager:: +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 +Auto-population takes 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 + call_command('loaddata', 'fixture.json') # Some fixture loading - z = News(title='bar') - print z.title_en, z.title_de # prints 'bar bar' + 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: @@ -186,7 +187,7 @@ It controls the default population behaviour. Auto-population modes ^^^^^^^^^^^^^^^^^^^^^ -There are 4 different population modes: +There are four different population modes: ``False`` [set by default] @@ -212,17 +213,17 @@ There are 4 different population modes: Falling back ------------ -Modeltranslation provides mechanism to control behaviour of data access in case of empty +Modeltranslation provides a mechanism to control behaviour of data access in case of empty translation values. This mechanism affects field access. -Consider ``News`` example: a creator of some news hasn't specified it's german title and content, -but only english ones. Then if a german visitor is viewing site, we would rather show him english -title/content of the news than display empty strings. This is called *fallback*. :: +Consider the ``News`` example: a creator of some news hasn't specified its German title and +content, but only English ones. Then if a German visitor is viewing the site, we would rather show +him English title/content of the news than display empty strings. This is called *fallback*. :: News.title_en = 'English title' News.title_de = '' print News.title - # If current active language is german, it should display title_de field value (''). + # If current active language is German, it should display the title_de field value (''). # But if fallback is enabled, it would display 'English title' instead. There are several ways of controlling fallback, described below. @@ -234,17 +235,17 @@ Fallback languages .. versionadded:: 0.5 -:ref:`settings-modeltranslation_fallback_languages` setting allows to set order of *fallback -languages*. By default it is only ``DEFAULT_LANGUAGE``. +:ref:`settings-modeltranslation_fallback_languages` setting allows to set the order of *fallback +languages*. By default that's the ``DEFAULT_LANGUAGE``. For example, setting :: MODELTRANSLATION_FALLBACK_LANGUAGES = ('en', 'de', 'fr') -means: if current active language field value is unset, try english value. If it is also unset, -try german, and so on - until some language yield non-empty value of the field. +means: if current active language field value is unset, try English value. If it is also unset, +try German, and so on - until some language yields a non-empty value of the field. -There is also option to define fallback by language, using dict syntax:: +There is also an option to define a fallback by language, using dict syntax:: MODELTRANSLATION_FALLBACK_LANGUAGES = { 'default': ('en', 'de', 'fr'), @@ -255,11 +256,11 @@ There is also option to define fallback by language, using dict syntax:: The ``default`` key is required and its value denote languages which are always tried at the end. With such a setting: -- for `uk` (Ukrainian) order of fallback languages is: ``('ru', 'en', 'de', 'fr')`` -- for `fr` order of fallback languages is: ``('de', 'en')`` - `fr` obviously is not fallback, since - it's active language; and `de` would be tried before `en` -- for `en` and `de` fallback order is ``('de', 'fr')`` and ``('en', 'fr')``, respectively -- for any other language order of fallback languages is just ``('en', 'de', 'fr')`` +- for `uk` the order of fallback languages is: ``('ru', 'en', 'de', 'fr')`` +- for `fr` the order of fallback languages is: ``('de', 'en')`` - Note, that `fr` obviously is not + a fallback, since its active language and `de` would be tried before `en` +- for `en` and `de` the fallback order is ``('de', 'fr')`` and ``('en', 'fr')``, respectively +- for any other language the order of fallback languages is just ``('en', 'de', 'fr')`` What is more, fallback languages order can be overridden per model, using ``TranslationOptions``:: @@ -286,7 +287,7 @@ Fallback values .. versionadded:: 0.4 But what if current language and all fallback languages yield no field value? Then modeltranslation -will use field's *fallback value*, if one was defined. +will use the field's *fallback value*, if one was defined. Fallback values are defined in ``TranslationOptions``, for example:: @@ -308,7 +309,7 @@ Fallback values can be also customized per model field:: } If current language and all fallback languages yield no field value, and no fallback values are -defined, then modeltranslation will use field's default value. +defined, then modeltranslation will use the field's default value. .. _fallback_undef: @@ -319,13 +320,13 @@ Fallback undefined Another question is what do we consider "no value", on what value should we fall back to other translations? For text fields the empty string can usually be considered as the undefined value, -but other fields may have different concepts of empty or missing value. +but other fields may have different concepts of empty or missing values. Modeltranslation defaults to using the field's default value as the undefined value (the empty string for non-nullable ``CharFields``). This requires calling ``get_default`` for every field access, which in some cases may be expensive. -If you'd like to fallback on a different value or your default is expensive to calculate, provide +If you'd like to fall back on a different value or your default is expensive to calculate, provide a custom undefined value (for a field or model):: class NewsTranslationOptions(TranslationOptions): From c5839bc01994f55590740990ad5fa01b6bfc7528 Mon Sep 17 00:00:00 2001 From: deschler Date: Sat, 26 Jul 2014 00:28:25 +0200 Subject: [PATCH 062/170] Added PyPI shields and used flat style. --- README.rst | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 11412881..45b6f72d 100644 --- a/README.rst +++ b/README.rst @@ -14,9 +14,21 @@ may they use translations or not, and you never have to touch the original model class. -.. image:: https://travis-ci.org/deschler/django-modeltranslation.png?branch=master +.. image:: http://img.shields.io/travis/deschler/django-modeltranslation/master.svg?style=flat :target: https://travis-ci.org/deschler/django-modeltranslation +.. image:: https://pypip.in/v/django-modeltranslation/badge.png?style=flat + :target: https://pypi.python.org/pypi/django-modeltranslation/ + :alt: Latest PyPI version + +.. image:: https://pypip.in/py_versions/django-modeltranslation/badge.png?style=flat + :target: https://pypi.python.org/pypi/django-modeltranslation/ + :alt: Supported Python versions + +.. image:: https://pypip.in/d/django-modeltranslation/badge.png?style=flat + :target: https://pypi.python.org/pypi/django-modeltranslation/ + :alt: Number of PyPI downloads + Features ======== @@ -35,7 +47,7 @@ https://github.com/deschler/django-modeltranslation Documentation ------------- -https://django-modeltranslation.readthedocs.org/en/latest/ +https://django-modeltranslation.readthedocs.org/en/latest Mailing List ------------ From 3bf2a1f717abfaf0e41a116658082ceed4860317 Mon Sep 17 00:00:00 2001 From: deschler Date: Sat, 26 Jul 2014 01:17:17 +0200 Subject: [PATCH 063/170] Hooked in coveralls for test coverage. --- .travis.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 523d5c67..5a75983e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -77,6 +77,9 @@ install: - IDJANGO=$(./travis.py $DJANGO) - pip install -q $IDJANGO - pip install -e . --use-mirrors + - pip install -q coveralls --use-mirrors script: - django-admin.py --version - - ./runtests.py + - coverage run --source=modeltranslation ./runtests.py +after_success: + coveralls From 8a35f8247723d60f105d9a1cdc02402fd81ffb3f Mon Sep 17 00:00:00 2001 From: deschler Date: Sat, 26 Jul 2014 01:27:55 +0200 Subject: [PATCH 064/170] Added coveralls shield. --- README.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 45b6f72d..07cbe72d 100644 --- a/README.rst +++ b/README.rst @@ -14,9 +14,12 @@ may they use translations or not, and you never have to touch the original model class. -.. image:: http://img.shields.io/travis/deschler/django-modeltranslation/master.svg?style=flat +.. image:: http://img.shields.io/travis/deschler/django-modeltranslation/master.png?style=flat :target: https://travis-ci.org/deschler/django-modeltranslation +.. image:: http://img.shields.io/coveralls/deschler/django-modeltranslation.png?style=flat + :target: https://coveralls.io/r/deschler/django-modeltranslation + .. image:: https://pypip.in/v/django-modeltranslation/badge.png?style=flat :target: https://pypi.python.org/pypi/django-modeltranslation/ :alt: Latest PyPI version From e91f7f1dd67680d77bc8c207c4000f3027c5d9d3 Mon Sep 17 00:00:00 2001 From: deschler Date: Sun, 27 Jul 2014 00:52:18 +0200 Subject: [PATCH 065/170] Added line about about coveralls and included shields in contribute chapter. --- docs/modeltranslation/contribute.rst | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/modeltranslation/contribute.rst b/docs/modeltranslation/contribute.rst index 6b9cc860..40344c26 100644 --- a/docs/modeltranslation/contribute.rst +++ b/docs/modeltranslation/contribute.rst @@ -74,7 +74,14 @@ Continuous Integration The project uses `Travis CI`_ for continuous integration tests. Hooks provided by Github are active, so that each push and pull request is automatically run against our `Travis CI config`_, checking code against different databases, -Python and Django versions. +Python and Django versions. This includes automatic tracking of test coverage +through `Coveralls`_. + +.. image:: http://img.shields.io/travis/deschler/django-modeltranslation/master.png?style=flat + :target: https://travis-ci.org/deschler/django-modeltranslation + +.. image:: http://img.shields.io/coveralls/deschler/django-modeltranslation.png?style=flat + :target: https://coveralls.io/r/deschler/django-modeltranslation Contributing Documentation @@ -115,6 +122,7 @@ Please do not use the issue tracker for general questions, we run a dedicated .. _Github: https://github.com/deschler/django-modeltranslation .. _Travis CI: https://travis-ci.org/deschler/django-modeltranslation .. _Travis CI config: https://github.com/deschler/django-modeltranslation/blob/master/.travis.yml +.. _Coveralls: https://coveralls.io/r/deschler/django-modeltranslation .. _reStructuredText: http://docutils.sourceforge.net/rst.html .. _Sphinx: http://sphinx-doc.org/ .. _issue tracker: https://github.com/deschler/django-modeltranslation/issues From de26e702a772bb1a73713d761b6d1e5c3e2cfde5 Mon Sep 17 00:00:00 2001 From: deschler Date: Sun, 27 Jul 2014 02:33:26 +0200 Subject: [PATCH 066/170] Added migrations (Django 1.7) documentation. --- docs/modeltranslation/installation.rst | 7 ++++- docs/modeltranslation/registration.rst | 40 ++++++++++++++++++++++++-- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/docs/modeltranslation/installation.rst b/docs/modeltranslation/installation.rst index 4b8ffead..940ef587 100644 --- a/docs/modeltranslation/installation.rst +++ b/docs/modeltranslation/installation.rst @@ -74,6 +74,10 @@ in detail in the following sections: .. note:: This only applies if the models registered in ``translation.py`` haven't been synced to the database before. If they have, please read :ref:`db-fields`. + .. note:: If you are using Django 1.7 and its internal migration system, run + ``python manage.py makemigrations``, followed by + ``python manage.py migrate`` instead. See :ref:`migrations` for details. + Configuration ============= @@ -100,7 +104,8 @@ Make sure that the ``modeltranslation`` app is listed in your .. important:: If you want to use the admin integration, ``modeltranslation`` must be put - before ``django.contrib.admin``. + before ``django.contrib.admin`` (only applies when using Django 1.7 or + above). .. important:: If you want to use the ``django-debug-toolbar`` together with diff --git a/docs/modeltranslation/registration.rst b/docs/modeltranslation/registration.rst index ba10c079..dcbd6380 100644 --- a/docs/modeltranslation/registration.rst +++ b/docs/modeltranslation/registration.rst @@ -180,14 +180,47 @@ Invoking ``sync_translation_fields`` is plain easier. Note that all added fields are by default declared ``blank=True`` and ``null=True`` no matter if the original field is required or not. In other words - all translations are optional, unless an explicit option is -provided - see below. +provided - see :ref:`required_langs`. To populate the default translation fields added by modeltranslation with values from existing database fields, you can use the -``update_translation_fields`` command below. See +``update_translation_fields`` command. See :ref:`commands-update_translation_fields` for more info on this. +.. _migrations: + +Migrations (Django 1.7) +^^^^^^^^^^^^^^^^^^^^^^^ + +.. versionadded:: 0.8 + +Modeltranslation supports the migration system introduced by Django 1.7. +Besides the normal workflow as described in Django's `Migration docs`_, you +should do a migration whenever one of the following changes have been made to +your project: + +- Added or removed a language through ``settings.LANGUAGES`` or + ``settings.MODELTRANSLATION LANGUAGES``. +- Registered or unregistered a field through ``TranslationOptions.fields``. + +It doesn't matter if you are starting a fresh project or change an existing +one, it's always: + +1. ``python manage.py makemigrations`` to create a new migration with + the added or removed fields. +2. ``python manage.py migrate`` to apply the changes. + +.. As opposed to the statement made in :ref:`db-fields`, using the +.. :ref:`sync_translation_fields ` +.. management command together with the new migration system is not recommended. + +.. note:: + Support for migrations is implemented through + ``fields.TranslationField.deconstruct(self)`` and respects changes to the + ``null`` option. + + .. _required_langs: Required fields @@ -320,3 +353,6 @@ Model Field 0.4 0.5 0.7 .. |u| replace:: ? \* Implicitly supported (as subclass of a supported field) + + +.. _Migration docs: https://docs.djangoproject.com/en/dev/topics/migrations/#workflow From 89e372a743bc57f42a3df0fb5fb1097eb4a875de Mon Sep 17 00:00:00 2001 From: deschler Date: Sun, 27 Jul 2014 22:28:45 +0200 Subject: [PATCH 067/170] Added list_editable test. --- modeltranslation/tests/tests.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/modeltranslation/tests/tests.py b/modeltranslation/tests/tests.py index 5cc6e43d..c20567b0 100644 --- a/modeltranslation/tests/tests.py +++ b/modeltranslation/tests/tests.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- import datetime from decimal import Decimal +import imp import os import shutil -import imp import django from django import forms @@ -2136,6 +2136,18 @@ class DataTranslationOptions(translator.TranslationOptions): # Remove translation for DataModel translator.translator.unregister(models.DataModel) + def test_list_editable(self): + class TestModelAdmin(admin.TranslationAdmin): + list_editable = ['title'] + list_display = ['id', 'title'] + list_display_links = ['id'] + + ma = TestModelAdmin(models.TestModel, self.site) + list_editable = ['title_de', 'title_en'] + list_display = ['id', 'title_de', 'title_en'] + self.assertEqual(tuple(ma.list_editable), tuple(list_editable)) + self.assertEqual(tuple(ma.list_display), tuple(list_display)) + def test_build_css_class(self): with reload_override_settings(LANGUAGES=(('de', 'German'), ('en', 'English'), ('es-ar', 'Argentinian Spanish'),)): From bc498f68d6042168bdf33f831a0ab01f080706fa Mon Sep 17 00:00:00 2001 From: deschler Date: Sun, 27 Jul 2014 23:53:30 +0200 Subject: [PATCH 068/170] Removed superfluous do's from private api. --- modeltranslation/admin.py | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/modeltranslation/admin.py b/modeltranslation/admin.py index 1503cd76..7f22b5fa 100644 --- a/modeltranslation/admin.py +++ b/modeltranslation/admin.py @@ -169,7 +169,7 @@ def append_lang(source): prepopulated_fields[dest] = localize(sources, lang) self.prepopulated_fields = prepopulated_fields - def _do_get_form_or_formset(self, request, obj, **kwargs): + def _get_form_or_formset(self, request, obj, **kwargs): """ Code shared among get_form and get_formset. """ @@ -190,17 +190,17 @@ def _do_get_form_or_formset(self, request, obj, **kwargs): return kwargs - def _do_get_fieldsets_pre_form_or_formset(self): + def _get_fieldsets_pre_form_or_formset(self): """ - Common get_fieldsets code shared among TranslationAdmin and - TranslationInlineModelAdmin. + Generic get_fieldsets code, shared by + TranslationAdmin and TranslationInlineModelAdmin. """ return self._declared_fieldsets() - def _do_get_fieldsets_post_form_or_formset(self, request, form, obj=None): + def _get_fieldsets_post_form_or_formset(self, request, form, obj=None): """ - Common get_fieldsets code shared among TranslationAdmin and - TranslationInlineModelAdmin. + Generic get_fieldsets code, shared by + TranslationAdmin and TranslationInlineModelAdmin. """ base_fields = self.replace_orig_field(form.base_fields.keys()) fields = base_fields + list(self.get_readonly_fields(request, obj)) @@ -208,8 +208,9 @@ def _do_get_fieldsets_post_form_or_formset(self, request, form, obj=None): def get_translation_field_excludes(self, exclude_languages=None): """ - Returns a tuple of translation field names to exclude base on + Returns a tuple of translation field names to exclude based on `exclude_languages` arg. + TODO: Currently unused? """ if exclude_languages is None: exclude_languages = [] @@ -226,7 +227,7 @@ def get_translation_field_excludes(self, exclude_languages=None): def get_readonly_fields(self, request, obj=None): """ - Hook for specifying custom readonly fields. + Hook to specify custom readonly fields. """ return self.replace_orig_field(self.readonly_fields) @@ -301,20 +302,20 @@ def _group_fieldsets(self, fieldsets): return fieldsets def get_form(self, request, obj=None, **kwargs): - kwargs = self._do_get_form_or_formset(request, obj, **kwargs) + kwargs = self._get_form_or_formset(request, obj, **kwargs) return super(TranslationAdmin, self).get_form(request, obj, **kwargs) def get_fieldsets(self, request, obj=None): if self.declared_fieldsets: - return self._do_get_fieldsets_pre_form_or_formset() + return self._get_fieldsets_pre_form_or_formset() return self._group_fieldsets( - self._do_get_fieldsets_post_form_or_formset( + self._get_fieldsets_post_form_or_formset( request, self.get_form(request, obj, fields=None), obj)) class TranslationInlineModelAdmin(TranslationBaseModelAdmin, InlineModelAdmin): def get_formset(self, request, obj=None, **kwargs): - kwargs = self._do_get_form_or_formset(request, obj, **kwargs) + kwargs = self._get_form_or_formset(request, obj, **kwargs) return super(TranslationInlineModelAdmin, self).get_formset(request, obj, **kwargs) def get_fieldsets(self, request, obj=None): @@ -322,9 +323,9 @@ def get_fieldsets(self, request, obj=None): # fieldset line with just the original model verbose_name of the model # is displayed above the new fieldsets. if self.declared_fieldsets: - return self._do_get_fieldsets_pre_form_or_formset() + return self._get_fieldsets_pre_form_or_formset() form = self.get_formset(request, obj, fields=None).form - return self._do_get_fieldsets_post_form_or_formset(request, form, obj) + return self._get_fieldsets_post_form_or_formset(request, form, obj) class TranslationTabularInline(TranslationInlineModelAdmin, admin.TabularInline): From 59317a4c12b833925205c688f578a498b88bf3cb Mon Sep 17 00:00:00 2001 From: deschler Date: Mon, 28 Jul 2014 00:03:50 +0200 Subject: [PATCH 069/170] Consistent use of django version check. --- modeltranslation/admin.py | 7 +++---- modeltranslation/models.py | 4 ++-- modeltranslation/tests/tests.py | 4 ++-- runtests.py | 2 +- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/modeltranslation/admin.py b/modeltranslation/admin.py index 7f22b5fa..83d026aa 100644 --- a/modeltranslation/admin.py +++ b/modeltranslation/admin.py @@ -10,7 +10,7 @@ # Ensure that models are registered for translation before TranslationAdmin # runs. The import is supposed to resolve a race condition between model import # and translation registration in production (see issue #19). -if django.get_version() < '1.7': +if django.VERSION < (1, 7): import modeltranslation.models # NOQA from modeltranslation import settings as mt_settings from modeltranslation.translator import translator @@ -78,8 +78,7 @@ def patch_translation_field(self, db_field, field, **kwargs): css_classes.append(build_css_class(db_field.name, 'mt-field')) if db_field.language == mt_settings.DEFAULT_LANGUAGE: - # Add another css class to identify a default modeltranslation - # widget. + # Add another css class to identify a default modeltranslation widget css_classes.append('mt-default') if (orig_formfield.required or self._orig_was_required.get( '%s.%s' % (orig_field.model._meta, orig_field.name))): @@ -171,7 +170,7 @@ def append_lang(source): def _get_form_or_formset(self, request, obj, **kwargs): """ - Code shared among get_form and get_formset. + Generic code shared by get_form and get_formset. """ if self.exclude is None: exclude = [] diff --git a/modeltranslation/models.py b/modeltranslation/models.py index 9082e9af..d544de63 100644 --- a/modeltranslation/models.py +++ b/modeltranslation/models.py @@ -17,7 +17,7 @@ def autodiscover(): from modeltranslation.translator import translator from modeltranslation.settings import TRANSLATION_FILES, DEBUG - if django.get_version() < '1.7': + if django.VERSION < (1, 7): mods = [(app, import_module(app)) for app in settings.INSTALLED_APPS] else: from django.apps import apps @@ -80,5 +80,5 @@ def handle_translation_registrations(*args, **kwargs): autodiscover() -if django.get_version() < '1.7': +if django.VERSION < (1, 7): handle_translation_registrations() diff --git a/modeltranslation/tests/tests.py b/modeltranslation/tests/tests.py index c20567b0..1426f2a0 100644 --- a/modeltranslation/tests/tests.py +++ b/modeltranslation/tests/tests.py @@ -2314,7 +2314,7 @@ def test_form(self): class CreationForm(forms.ModelForm): class Meta: model = self.model - if django.get_version() >= '1.6': + if django.VERSION >= (1, 6): fields = '__all__' creation_form = CreationForm({'name': 'abc'}) @@ -2798,7 +2798,7 @@ def test_fields(self): class TestModelForm(TranslationModelForm): class Meta: model = models.TestModel - if django.get_version() >= '1.6': + if django.VERSION >= (1, 6): fields = '__all__' form = TestModelForm() diff --git a/runtests.py b/runtests.py index a2fad1f7..59ad4951 100755 --- a/runtests.py +++ b/runtests.py @@ -45,7 +45,7 @@ def runtests(): ), ) - if django.get_version() >= '1.7': + if django.VERSION >= (1, 7): django.setup() failures = call_command( 'test', 'modeltranslation', interactive=False, failfast=False, verbosity=2) From 42c949ddb56edf0730308b4abb04e9741863fb46 Mon Sep 17 00:00:00 2001 From: Dirk Eschler Date: Mon, 28 Jul 2014 12:03:16 +0200 Subject: [PATCH 070/170] Changed Django to 1.7c2. --- .travis.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5a75983e..616ef93d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,9 +15,9 @@ env: - DJANGO=1.6 DB=sqlite - DJANGO=1.6 DB=postgres - DJANGO=1.6 DB=mysql - - DJANGO=https://www.djangoproject.com/download/1.7c1/tarball DB=sqlite - - DJANGO=https://www.djangoproject.com/download/1.7c1/tarball DB=postgres - - DJANGO=https://www.djangoproject.com/download/1.7c1/tarball DB=mysql + - DJANGO=https://www.djangoproject.com/download/1.7c2/tarball DB=sqlite + - DJANGO=https://www.djangoproject.com/download/1.7c2/tarball DB=postgres + - DJANGO=https://www.djangoproject.com/download/1.7c2/tarball DB=mysql matrix: exclude: - python: "3.2" @@ -40,11 +40,11 @@ matrix: env: DJANGO=1.4 DB=mysql - python: "2.6" - env: DJANGO=https://www.djangoproject.com/download/1.7c1/tarball DB=sqlite + env: DJANGO=https://www.djangoproject.com/download/1.7c2/tarball DB=sqlite - python: "2.6" - env: DJANGO=https://www.djangoproject.com/download/1.7c1/tarball DB=postgres + env: DJANGO=https://www.djangoproject.com/download/1.7c2/tarball DB=postgres - python: "2.6" - env: DJANGO=https://www.djangoproject.com/download/1.7c1/tarball DB=mysql + env: DJANGO=https://www.djangoproject.com/download/1.7c2/tarball DB=mysql - python: "3.2" env: DJANGO=1.5 DB=mysql @@ -59,11 +59,11 @@ matrix: - python: "3.4" env: DJANGO=1.6 DB=mysql - python: "3.2" - env: DJANGO=https://www.djangoproject.com/download/1.7c1/tarball DB=mysql + env: DJANGO=https://www.djangoproject.com/download/1.7c2/tarball DB=mysql - python: "3.3" - env: DJANGO=https://www.djangoproject.com/download/1.7c1/tarball DB=mysql + env: DJANGO=https://www.djangoproject.com/download/1.7c2/tarball DB=mysql - python: "3.4" - env: DJANGO=https://www.djangoproject.com/download/1.7c1/tarball DB=mysql + env: DJANGO=https://www.djangoproject.com/download/1.7c2/tarball DB=mysql before_install: - pip install -q flake8 --use-mirrors - PYFLAKES_NODOCTEST=1 flake8 --max-line-length=100 modeltranslation From 19c2a90e9f84675a632b13f2881b153e47bc8534 Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Tue, 29 Jul 2014 17:34:53 +0300 Subject: [PATCH 071/170] Add fallback to values and values_list (close #258). --- docs/modeltranslation/usage.rst | 17 +++++-- modeltranslation/manager.py | 90 +++++++++++++++++++++++++++++---- modeltranslation/tests/tests.py | 32 ++++++++++++ 3 files changed, 123 insertions(+), 16 deletions(-) diff --git a/docs/modeltranslation/usage.rst b/docs/modeltranslation/usage.rst index cb3df69b..e65c70e6 100644 --- a/docs/modeltranslation/usage.rst +++ b/docs/modeltranslation/usage.rst @@ -123,7 +123,7 @@ These manager methods perform rewriting: - ``order_by()`` - ``update()`` - ``only()``, ``defer()`` -- ``values()``, ``values_list()`` +- ``values()``, ``values_list()``, with :ref:`fallback ` mechanism - ``dates()`` - ``select_related()`` - ``create()``, with optional auto-population_ feature @@ -214,18 +214,25 @@ Falling back ------------ Modeltranslation provides a mechanism to control behaviour of data access in case of empty -translation values. This mechanism affects field access. +translation values. This mechanism affects field access, as well as ``values()`` +and ``values_list()`` manager methods. Consider the ``News`` example: a creator of some news hasn't specified its German title and content, but only English ones. Then if a German visitor is viewing the site, we would rather show him English title/content of the news than display empty strings. This is called *fallback*. :: - News.title_en = 'English title' - News.title_de = '' - print News.title + news.title_en = 'English title' + news.title_de = '' + print news.title # If current active language is German, it should display the title_de field value (''). # But if fallback is enabled, it would display 'English title' instead. + # Similarly for manager + news.save() + print News.objects.filter(pk=news.pk).values_list('title', flat=True)[0] + # As above: if current active language is German and fallback to English is enabled, + # it would display 'English title'. + There are several ways of controlling fallback, described below. .. _fallback_lang: diff --git a/modeltranslation/manager.py b/modeltranslation/manager.py index 03392c0f..7f9e2165 100644 --- a/modeltranslation/manager.py +++ b/modeltranslation/manager.py @@ -23,7 +23,7 @@ from modeltranslation import settings from modeltranslation.fields import TranslationField from modeltranslation.utils import (build_localized_fieldname, get_language, - auto_populate) + auto_populate, resolution_order) def get_translatable_fields_for_model(model): @@ -56,6 +56,24 @@ def rewrite_lookup_key(model, lookup_key): return '__'.join(pieces) +def append_fallback(model, fields): + """ + If translated field is encountered, add also all its fallback fields. + Returns tuple: (set_of_new_fields_to_use, set_of_translated_field_names) + """ + fields = set(fields) + trans = set() + from modeltranslation.translator import translator + opts = translator.get_options_for_model(model) + for key, _ in opts.fields.items(): + if key in fields: + langs = resolution_order(get_language(), getattr(model, key).fallback_languages) + fields = fields.union(build_localized_fieldname(key, lang) for lang in langs) + fields.remove(key) + trans.add(key) + return fields, trans + + def append_translated(model, fields): "If translated field is encountered, add also all its translation fields." fields = set(fields) @@ -343,24 +361,22 @@ def values(self, *fields): if not fields: # Emulate original queryset behaviour: get all fields that are not translation fields fields = self._get_original_fields() - new_args = [] - for key in fields: - new_args.append(rewrite_lookup_key(self.model, key)) - vqs = super(MultilingualQuerySet, self).values(*new_args) - vqs.field_names = list(fields) - return vqs + return self._clone(klass=FallbackValuesQuerySet, setup=True, _fields=fields) # This method was not present in django-linguo def values_list(self, *fields, **kwargs): if not self._rewrite: return super(MultilingualQuerySet, self).values_list(*fields, **kwargs) + flat = kwargs.pop('flat', False) + if kwargs: + raise TypeError('Unexpected keyword arguments to values_list: %s' % (list(kwargs),)) + if flat and len(fields) > 1: + raise TypeError("'flat' is not valid when values_list is " + "called with more than one field.") if not fields: # Emulate original queryset behaviour: get all fields that are not translation fields fields = self._get_original_fields() - new_args = [] - for key in fields: - new_args.append(rewrite_lookup_key(self.model, key)) - return super(MultilingualQuerySet, self).values_list(*new_args, **kwargs) + return self._clone(klass=FallbackValuesListQuerySet, setup=True, flat=flat, _fields=fields) # This method was not present in django-linguo def dates(self, field_name, *args, **kwargs): @@ -370,6 +386,58 @@ def dates(self, field_name, *args, **kwargs): return super(MultilingualQuerySet, self).dates(new_key, *args, **kwargs) +class FallbackValuesQuerySet(models.query.ValuesQuerySet, MultilingualQuerySet): + def _setup_query(self): + original = self._fields + new_fields, self.translation_fields = append_fallback(self.model, original) + self._fields = list(new_fields) + self.fields_to_del = new_fields - set(original) + super(FallbackValuesQuerySet, self)._setup_query() + + class X(object): + # This stupid class is needed as object use __slots__ and has no __dict__. + pass + + def iterator(self): + instance = self.X() + for row in super(FallbackValuesQuerySet, self).iterator(): + instance.__dict__.update(row) + for key in self.translation_fields: + row[key] = getattr(self.model, key).__get__(instance, None) + for key in self.fields_to_del: + del row[key] + yield row + + def _clone(self, klass=None, setup=False, **kwargs): + c = super(FallbackValuesQuerySet, self)._clone(klass, **kwargs) + c.fields_to_del = self.fields_to_del + c.translation_fields = self.translation_fields + if setup and hasattr(c, '_setup_query'): + c._setup_query() + return c + + +class FallbackValuesListQuerySet(FallbackValuesQuerySet): + def iterator(self): + for row in super(FallbackValuesListQuerySet, self).iterator(): + if self.flat and len(self.original_fields) == 1: + yield row[self.original_fields[0]] + else: + yield tuple(row[f] for f in self.original_fields) + + def _setup_query(self): + self.original_fields = self._fields + super(FallbackValuesListQuerySet, self)._setup_query() + + def _clone(self, *args, **kwargs): + clone = super(FallbackValuesListQuerySet, self)._clone(*args, **kwargs) + clone.original_fields = self.original_fields + if not hasattr(clone, "flat"): + # Only assign flat if the clone didn't already get it from kwargs + clone.flat = self.flat + return clone + + def get_queryset(obj): if hasattr(obj, 'get_queryset'): return obj.get_queryset() diff --git a/modeltranslation/tests/tests.py b/modeltranslation/tests/tests.py index 1426f2a0..aaa7c991 100644 --- a/modeltranslation/tests/tests.py +++ b/modeltranslation/tests/tests.py @@ -2449,6 +2449,38 @@ def test_order_by_meta(self): self.assertEqual(titles_for_en, ('most', 'more_en', 'more_de', 'least')) self.assertEqual(titles_for_de, ('most', 'more_de', 'more_en', 'least')) + def assert_fallback(self, method, expected1, *args, **kwargs): + transform = kwargs.pop('transform', lambda x: x) + expected2 = kwargs.pop('expected_de', expected1) + with default_fallback(): + # Fallback is ('de',) + obj = method(*args, **kwargs)[0] + with override('de'): + obj2 = method(*args, **kwargs)[0] + self.assertEqual(transform(obj), expected1) + self.assertEqual(transform(obj2), expected2) + + def test_values_fallback(self): + manager = models.ManagerTestModel.objects + manager.create(title_en='', title_de='de') + self.assertEqual('en', get_language()) + + self.assert_fallback(manager.values, 'de', 'title', transform=lambda x: x['title']) + self.assert_fallback(manager.values_list, 'de', 'title', flat=True) + self.assert_fallback(manager.values_list, ('de', '', 'de'), 'title', 'title_en', 'title_de') + + # Settings are taken into account - fallback can be disabled + with override_settings(MODELTRANSLATION_ENABLE_FALLBACKS=False): + self.assert_fallback(manager.values, '', 'title', expected_de='de', + transform=lambda x: x['title']) + + # Test fallback values + manager = models.FallbackModel.objects + manager.create() + + self.assert_fallback(manager.values, 'fallback', 'title', transform=lambda x: x['title']) + self.assert_fallback(manager.values_list, ('fallback', 'fallback'), 'title', 'text') + def test_values(self): manager = models.ManagerTestModel.objects id1 = manager.create(title_en='en', title_de='de').pk From e59cf5a4cd4dd0ef5b6f382c7929174802af4738 Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Sat, 2 Aug 2014 16:42:14 +0200 Subject: [PATCH 072/170] Readd GenericIp field support and tests as we now require Django >= 1.4. --- modeltranslation/fields.py | 5 +-- modeltranslation/tests/models.py | 2 +- modeltranslation/tests/tests.py | 52 +++++++++++++-------------- modeltranslation/tests/translation.py | 4 +-- 4 files changed, 29 insertions(+), 34 deletions(-) diff --git a/modeltranslation/fields.py b/modeltranslation/fields.py index f77f419d..01b4e362 100644 --- a/modeltranslation/fields.py +++ b/modeltranslation/fields.py @@ -23,6 +23,7 @@ fields.FloatField, fields.DecimalField, fields.IPAddressField, + fields.GenericIPAddressField, fields.DateField, fields.DateTimeField, fields.TimeField, @@ -31,10 +32,6 @@ fields.related.ForeignKey, # Above implies also OneToOneField ) -try: - SUPPORTED_FIELDS += (fields.GenericIPAddressField,) # Django 1.4+ only -except AttributeError: - pass class NONE: diff --git a/modeltranslation/tests/models.py b/modeltranslation/tests/models.py index e00ca86a..5bd74bfd 100644 --- a/modeltranslation/tests/models.py +++ b/modeltranslation/tests/models.py @@ -93,7 +93,7 @@ class OtherFieldsModel(models.Model): date = models.DateField(blank=True, null=True) datetime = models.DateTimeField(blank=True, null=True) time = models.TimeField(blank=True, null=True) -# genericip = models.GenericIPAddressField(blank=True, null=True) + genericip = models.GenericIPAddressField(blank=True, null=True) class FancyDescriptor(object): diff --git a/modeltranslation/tests/tests.py b/modeltranslation/tests/tests.py index aaa7c991..e8d3ffa0 100644 --- a/modeltranslation/tests/tests.py +++ b/modeltranslation/tests/tests.py @@ -1151,9 +1151,9 @@ def test_translated_models(self): self.assertTrue('ip' in field_names) self.assertTrue('ip_de' in field_names) self.assertTrue('ip_en' in field_names) -# self.assertTrue('genericip' in field_names) -# self.assertTrue('genericip_de' in field_names) -# self.assertTrue('genericip_en' in field_names) + self.assertTrue('genericip' in field_names) + self.assertTrue('genericip_de' in field_names) + self.assertTrue('genericip_en' in field_names) self.assertTrue('float' in field_names) self.assertTrue('float_de' in field_names) self.assertTrue('float_en' in field_names) @@ -1282,29 +1282,29 @@ def test_translated_models_ipaddress_instance(self): inst.ip = '1;2' self.assertRaises(ValidationError, inst.full_clean) -# def test_translated_models_genericipaddress_instance(self): -# inst = OtherFieldsModel() -# inst.genericip = '2a02:42fe::4' -# self.assertEqual('de', get_language()) -# self.assertEqual('2a02:42fe::4', inst.genericip) -# self.assertEqual('2a02:42fe::4', inst.genericip_de) -# self.assertEqual(None, inst.genericip_en) -# -# inst.genericip = '2a02:23fe::4' -# inst.save() -# self.assertEqual('2a02:23fe::4', inst.genericip) -# self.assertEqual('2a02:23fe::4', inst.genericip_de) -# self.assertEqual(None, inst.genericip_en) -# -# trans_real.activate('en') -# inst.genericip = '2a02:42fe::4' -# self.assertEqual('2a02:42fe::4', inst.genericip) -# self.assertEqual('2a02:23fe::4', inst.genericip_de) -# self.assertEqual('2a02:42fe::4', inst.genericip_en) -# -# # Check if validation is preserved -# inst.genericip = '1;2' -# self.assertRaises(ValidationError, inst.full_clean) + def test_translated_models_genericipaddress_instance(self): + inst = models.OtherFieldsModel() + inst.genericip = '2a02:42fe::4' + self.assertEqual('de', get_language()) + self.assertEqual('2a02:42fe::4', inst.genericip) + self.assertEqual('2a02:42fe::4', inst.genericip_de) + self.assertEqual(None, inst.genericip_en) + + inst.genericip = '2a02:23fe::4' + inst.save() + self.assertEqual('2a02:23fe::4', inst.genericip) + self.assertEqual('2a02:23fe::4', inst.genericip_de) + self.assertEqual(None, inst.genericip_en) + + trans_real.activate('en') + inst.genericip = '2a02:42fe::4' + self.assertEqual('2a02:42fe::4', inst.genericip) + self.assertEqual('2a02:23fe::4', inst.genericip_de) + self.assertEqual('2a02:42fe::4', inst.genericip_en) + + # Check if validation is preserved + inst.genericip = '1;2' + self.assertRaises(ValidationError, inst.full_clean) def test_translated_models_float_instance(self): inst = models.OtherFieldsModel() diff --git a/modeltranslation/tests/translation.py b/modeltranslation/tests/translation.py index aa3b8a3c..f5c4fe9f 100644 --- a/modeltranslation/tests/translation.py +++ b/modeltranslation/tests/translation.py @@ -66,10 +66,8 @@ class OneToOneFieldModelTranslationOptions(TranslationOptions): # ######### Custom fields testing class OtherFieldsModelTranslationOptions(TranslationOptions): - # fields = ('int', 'boolean', 'nullboolean', 'csi', 'float', 'decimal', - # 'ip', 'genericip') fields = ('int', 'boolean', 'nullboolean', 'csi', 'float', 'decimal', - 'ip', 'date', 'datetime', 'time',) + 'ip', 'genericip', 'date', 'datetime', 'time',) translator.register(OtherFieldsModel, OtherFieldsModelTranslationOptions) From 395771d2279110cc5bb6edd1e96b6c744303a0b9 Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Sun, 7 Sep 2014 21:42:05 +0200 Subject: [PATCH 073/170] Patch db_column of translation fields in migration files (close #264). --- modeltranslation/fields.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/modeltranslation/fields.py b/modeltranslation/fields.py index 01b4e362..9885c0d8 100644 --- a/modeltranslation/fields.py +++ b/modeltranslation/fields.py @@ -139,8 +139,11 @@ def __init__(self, translated_field, language, empty_value, *args, **kwargs): self.blank = False # Adjust the name of this field to reflect the language - self.attname = build_localized_fieldname(self.translated_field.name, self.language) + self.attname = build_localized_fieldname(self.translated_field.name, language) self.name = self.attname + if self.translated_field.db_column: + self.db_column = build_localized_fieldname(self.translated_field.db_column, language) + self.column = self.db_column # Copy the verbose name and append a language suffix # (will show up e.g. in the admin). @@ -181,14 +184,6 @@ def __ne__(self, other): def __hash__(self): return hash((self.creation_counter, self.language)) - def get_attname_column(self): - attname = self.get_attname() - if self.translated_field.db_column: - column = build_localized_fieldname(self.translated_field.db_column, self.language) - else: - column = attname - return attname, column - def formfield(self, *args, **kwargs): """ Returns proper formfield, according to empty_values setting @@ -251,6 +246,8 @@ def deconstruct(self): name, path, args, kwargs = self.translated_field.deconstruct() if self.null is True: kwargs.update({'null': True}) + if 'db_column' in kwargs: + kwargs['db_column'] = self.db_column return six.text_type(self.name), path, args, kwargs def south_field_triple(self): From 8047a4dcdaf2c0eb818dacb5f37969e903f9452d Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Sun, 7 Sep 2014 21:43:58 +0200 Subject: [PATCH 074/170] Add empty MIDDLEWARE_CLASSES to test settings in order to disable system check warning. --- modeltranslation/tests/settings.py | 1 + runtests.py | 1 + 2 files changed, 2 insertions(+) diff --git a/modeltranslation/tests/settings.py b/modeltranslation/tests/settings.py index f2066ced..f66c68fc 100644 --- a/modeltranslation/tests/settings.py +++ b/modeltranslation/tests/settings.py @@ -16,6 +16,7 @@ USE_I18N = True USE_TZ = False +MIDDLEWARE_CLASSES = () MODELTRANSLATION_AUTO_POPULATE = False MODELTRANSLATION_FALLBACK_LANGUAGES = () diff --git a/runtests.py b/runtests.py index 59ad4951..86cf37b5 100755 --- a/runtests.py +++ b/runtests.py @@ -43,6 +43,7 @@ def runtests(): LANGUAGES=( ('en', 'English'), ), + MIDDLEWARE_CLASSES=(), ) if django.VERSION >= (1, 7): From 706757739d1ba5d6914f0c8e1320f6708f5c46dd Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Sun, 7 Sep 2014 21:52:05 +0200 Subject: [PATCH 075/170] Use Django 1.7 final release in tests. --- .travis.yml | 18 +++++++++--------- tox.ini | 6 +++--- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index 616ef93d..40af6a55 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,9 +15,9 @@ env: - DJANGO=1.6 DB=sqlite - DJANGO=1.6 DB=postgres - DJANGO=1.6 DB=mysql - - DJANGO=https://www.djangoproject.com/download/1.7c2/tarball DB=sqlite - - DJANGO=https://www.djangoproject.com/download/1.7c2/tarball DB=postgres - - DJANGO=https://www.djangoproject.com/download/1.7c2/tarball DB=mysql + - DJANGO=1.7 DB=sqlite + - DJANGO=1.7 DB=postgres + - DJANGO=1.7 DB=mysql matrix: exclude: - python: "3.2" @@ -40,11 +40,11 @@ matrix: env: DJANGO=1.4 DB=mysql - python: "2.6" - env: DJANGO=https://www.djangoproject.com/download/1.7c2/tarball DB=sqlite + env: DJANGO=1.7 DB=sqlite - python: "2.6" - env: DJANGO=https://www.djangoproject.com/download/1.7c2/tarball DB=postgres + env: DJANGO=1.7 DB=postgres - python: "2.6" - env: DJANGO=https://www.djangoproject.com/download/1.7c2/tarball DB=mysql + env: DJANGO=1.7 DB=mysql - python: "3.2" env: DJANGO=1.5 DB=mysql @@ -59,11 +59,11 @@ matrix: - python: "3.4" env: DJANGO=1.6 DB=mysql - python: "3.2" - env: DJANGO=https://www.djangoproject.com/download/1.7c2/tarball DB=mysql + env: DJANGO=1.7 DB=mysql - python: "3.3" - env: DJANGO=https://www.djangoproject.com/download/1.7c2/tarball DB=mysql + env: DJANGO=1.7 DB=mysql - python: "3.4" - env: DJANGO=https://www.djangoproject.com/download/1.7c2/tarball DB=mysql + env: DJANGO=1.7 DB=mysql before_install: - pip install -q flake8 --use-mirrors - PYFLAKES_NODOCTEST=1 flake8 --max-line-length=100 modeltranslation diff --git a/tox.ini b/tox.ini index e030a48d..7aa1bbf6 100644 --- a/tox.ini +++ b/tox.ini @@ -25,19 +25,19 @@ commands = [testenv:py33-1.7.X] basepython = python3.3 deps = - https://www.djangoproject.com/download/1.7.b4/tarball + Django>=1.7,<1.8 Pillow [testenv:py32-1.7.X] basepython = python3.2 deps = - https://www.djangoproject.com/download/1.7.b4/tarball + Django>=1.7,<1.8 Pillow [testenv:py27-1.7.X] basepython = python2.7 deps = - https://www.djangoproject.com/download/1.7.b4/tarball + Django>=1.7,<1.8 Pillow [testenv:py33-1.6.X] From b06e49921bd947f86f3417d57b78c9edc3245ed3 Mon Sep 17 00:00:00 2001 From: wrwrwr Date: Sat, 4 Oct 2014 17:52:21 +0200 Subject: [PATCH 076/170] Fixed shadowing of jQuery referenced by a global ``jQuery`` variable by ``django.jQuery`` in the tabbed admin script. Javascript has a nasty semantic of moving initializers to the beginning of a block, thus rewritting ``var a = a || b`` as ``var a; a = a || b``. With such a code, if ``a`` starts as a global variable with some value, you could think that it would end up having the same value, but it is actually first set to ``undefined`` at the beginning of the block and then always set to ``b`` in the statement [1][2]. This is mostly of importance if you actually have more than one jQuery loaded. For instance, this happens with Mezzanine -- first Grappelli-safe loads a copy, and calls ``$.noConflict`` thus storing its version as ``jQuery``, second is Django, loading another copy and calling ``django.jQuery = jQuery.noConflict(true)``. So far so good, we have one copy on ``jQuery`` and another on ``django.jQuery``; jQuery UI chooses to load itself onto the ``jQuery`` (Grappelli) copy. But now runs the tabbed_translation_fields script, hiding the jQuery with UI loaded at the very beggining of the first block, and a moment later complaining that ``.tabs()`` or something else is not defined. [1]: http://www.ecma-international.org/ecma-262/5.1/#sec-12.2 (note the first paragraph after the grammar) [2]: http://stackoverflow.com/questions/500431/what-is-the-scope-of-variables-in-javascript (point 8) --- .../static/modeltranslation/js/tabbed_translation_fields.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modeltranslation/static/modeltranslation/js/tabbed_translation_fields.js b/modeltranslation/static/modeltranslation/js/tabbed_translation_fields.js index 8fe9928f..6a6dc417 100644 --- a/modeltranslation/static/modeltranslation/js/tabbed_translation_fields.js +++ b/modeltranslation/static/modeltranslation/js/tabbed_translation_fields.js @@ -3,7 +3,9 @@ var google, django, gettext; (function () { - var jQuery = jQuery || $ || django.jQuery; + var t = jQuery || $ || django.jQuery; + jQuery = t; // Note: This is not equivalent to "var jQuery = jQuery || ...". + /* Add a new selector to jQuery that excludes parent items which match a given selector */ jQuery.expr[':'].parents = function(a, i, m) { return jQuery(a).parents(m[3]).length < 1; From 6026a9174157a3db4635d194f92b4fbed0b7f9cd Mon Sep 17 00:00:00 2001 From: wrwrwr Date: Sat, 4 Oct 2014 19:16:18 +0200 Subject: [PATCH 077/170] A more elegant solution suggested by zlorf. --- .../static/modeltranslation/js/tabbed_translation_fields.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/modeltranslation/static/modeltranslation/js/tabbed_translation_fields.js b/modeltranslation/static/modeltranslation/js/tabbed_translation_fields.js index 6a6dc417..a0e4266f 100644 --- a/modeltranslation/static/modeltranslation/js/tabbed_translation_fields.js +++ b/modeltranslation/static/modeltranslation/js/tabbed_translation_fields.js @@ -3,8 +3,7 @@ var google, django, gettext; (function () { - var t = jQuery || $ || django.jQuery; - jQuery = t; // Note: This is not equivalent to "var jQuery = jQuery || ...". + var jQuery = window.jQuery || $ || django.jQuery; /* Add a new selector to jQuery that excludes parent items which match a given selector */ jQuery.expr[':'].parents = function(a, i, m) { From 553bfbefeb77c2ff6fd9b1e5aaeb970e18e84952 Mon Sep 17 00:00:00 2001 From: Dirk Eschler Date: Mon, 6 Oct 2014 15:08:22 +0200 Subject: [PATCH 078/170] Prepared 0.8 release (ref #268). --- CHANGELOG.txt | 17 +++++++++++++++++ PKG-INFO | 2 +- modeltranslation/__init__.py | 2 +- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 8975869a..f98c3f9b 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,20 @@ +v0.8 +==== +Date: 2014-10-06 + + FIXED: JavaScript scoping issue with two jQuery versions in tabbed + translation fields. + (resolves issue #267, + thanks Wojtek Ruszczewski) + + ADDED: Patch db_column of translation fields in migration files. + (resolves issue #264, + thanks Thom Wiggers and Jacek Tomaszewski) + ADDED: Fallback to values and values_list. + (resolves issue #258, + thanks Jacek Tomaszewski) + + v0.8b2 ====== Date: 2014-07-18 diff --git a/PKG-INFO b/PKG-INFO index 1580d877..38731c06 100644 --- a/PKG-INFO +++ b/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 1.0 Name: django-modeltranslation -Version: 0.8b2 +Version: 0.8 Summary: Translates Django models using a registration approach. Home-page: https://github.com/deschler/django-modeltranslation Author: Peter Eschler, diff --git a/modeltranslation/__init__.py b/modeltranslation/__init__.py index 07ffc579..f9e2dbe3 100644 --- a/modeltranslation/__init__.py +++ b/modeltranslation/__init__.py @@ -3,7 +3,7 @@ Version code adopted from Django development version. https://github.com/django/django """ -VERSION = (0, 8, 0, 'beta', 2) +VERSION = (0, 8, 0, 'final', 0) default_app_config = 'modeltranslation.apps.ModeltranslationConfig' From e30c3f4acd4b75e8afe9050ab6bd285b029c174d Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Sat, 18 Oct 2014 10:26:59 +0200 Subject: [PATCH 079/170] Fix docs about usage with django-debug-toolbar (close #271). --- docs/modeltranslation/installation.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/modeltranslation/installation.rst b/docs/modeltranslation/installation.rst index 940ef587..0d233ea8 100644 --- a/docs/modeltranslation/installation.rst +++ b/docs/modeltranslation/installation.rst @@ -108,10 +108,11 @@ Make sure that the ``modeltranslation`` app is listed in your above). .. important:: - If you want to use the ``django-debug-toolbar`` together with - modeltranslation, put ``debug_toolbar`` as first entry in - ``INSTALLED_APPS`` or use `explicit setup + If you want to use the ``django-debug-toolbar`` together with modeltranslation, use `explicit setup `_. + Otherwise tweak the order of ``INSTALLED_APPS``: try to put ``debug_toolbar`` as first entry in + ``INSTALLED_APPS`` (in Django < 1.7) or after ``modeltranslation`` (in Django >= 1.7). However, + only `explicit setup` is guaranteed to succeed. .. _settings-languages: From ef8e0b323fe2b9070b848e187b64d97d67396368 Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Fri, 24 Oct 2014 09:24:26 +0200 Subject: [PATCH 080/170] Ensure AVAILABLE_LANGUAGES is a list (close #272). --- modeltranslation/settings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modeltranslation/settings.py b/modeltranslation/settings.py index 95325ce3..4e897303 100644 --- a/modeltranslation/settings.py +++ b/modeltranslation/settings.py @@ -5,8 +5,8 @@ TRANSLATION_FILES = tuple(getattr(settings, 'MODELTRANSLATION_TRANSLATION_FILES', ())) -AVAILABLE_LANGUAGES = getattr(settings, 'MODELTRANSLATION_LANGUAGES', - [l[0] for l in settings.LANGUAGES]) +AVAILABLE_LANGUAGES = list(getattr(settings, 'MODELTRANSLATION_LANGUAGES', + (l[0] for l in settings.LANGUAGES))) DEFAULT_LANGUAGE = getattr(settings, 'MODELTRANSLATION_DEFAULT_LANGUAGE', None) if DEFAULT_LANGUAGE and DEFAULT_LANGUAGE not in AVAILABLE_LANGUAGES: raise ImproperlyConfigured('MODELTRANSLATION_DEFAULT_LANGUAGE not in LANGUAGES setting.') From 902a7c9a72c28a46c948e24c1ac113715df17e48 Mon Sep 17 00:00:00 2001 From: wrwrwr Date: Fri, 24 Oct 2014 12:13:45 +0200 Subject: [PATCH 081/170] Fixed Indonesian translation field and ForeignKey field name clash. The official ISO-639-1 code for Indonesia is "id"; replaced this problematic suffix with the ISO-639-2 3-letter code in the translation field names (http://en.wikipedia.org/wiki/Indonesian_language). --- modeltranslation/tests/tests.py | 4 ++++ modeltranslation/utils.py | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/modeltranslation/tests/tests.py b/modeltranslation/tests/tests.py index e8d3ffa0..0ba4d41c 100644 --- a/modeltranslation/tests/tests.py +++ b/modeltranslation/tests/tests.py @@ -965,6 +965,10 @@ def test_non_translated_relation(self): self.assertEqual(manager.filter(test_fks__title='f_title_de').count(), 0) self.assertEqual(manager.filter(test_fks__title_de='f_title_de').count(), 1) + def test_indonesian(self): + field = models.ForeignKeyModel._meta.get_field('test') + self.assertNotEqual(field.attname, build_localized_fieldname(field.name, 'id')) + def assertQuerysetsEqual(self, qs1, qs2): pk = lambda o: o.pk return self.assertEqual(sorted(qs1, key=pk), sorted(qs2, key=pk)) diff --git a/modeltranslation/utils.py b/modeltranslation/utils.py index 3a1eb64d..7461aa23 100644 --- a/modeltranslation/utils.py +++ b/modeltranslation/utils.py @@ -30,10 +30,16 @@ def get_translation_fields(field): def build_localized_fieldname(field_name, lang): + if lang == 'id': + # The 2-letter Indonesian language code is problematic with the + # current naming scheme as Django foreign keys also add "id" suffix. + lang = 'ind' return str('%s_%s' % (field_name, lang.replace('-', '_'))) def _build_localized_verbose_name(verbose_name, lang): + if lang == 'id': + lang = 'ind' return force_text('%s [%s]') % (force_text(verbose_name), lang) build_localized_verbose_name = lazy(_build_localized_verbose_name, six.text_type) From 04b67f66d755c28e62ef1108c578c4c390bf5c5f Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Fri, 24 Oct 2014 19:54:10 +0200 Subject: [PATCH 082/170] Add __reduce__ to MultilingualQuerySet (ref #273). --- modeltranslation/manager.py | 21 ++++++++++++++------- modeltranslation/tests/tests.py | 21 +++++++++++++++++++++ 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/modeltranslation/manager.py b/modeltranslation/manager.py index 7f9e2165..fee8ba97 100644 --- a/modeltranslation/manager.py +++ b/modeltranslation/manager.py @@ -171,6 +171,9 @@ def _post_init(self): ordering.append(rewrite_order_lookup_key(self.model, key)) self.query.add_ordering(*ordering) + def __reduce__(self): + return multilingual_queryset_factory, (self.__class__.__bases__[0],), self.__getstate__() + # This method was not present in django-linguo def _clone(self, klass=None, *args, **kwargs): if klass is not None and not issubclass(klass, MultilingualQuerySet): @@ -445,6 +448,16 @@ def get_queryset(obj): return obj.get_query_set() +def multilingual_queryset_factory(old_cls, instantiate=True): + if old_cls == models.query.QuerySet: + NewClass = MultilingualQuerySet + else: + class NewClass(old_cls, MultilingualQuerySet): + pass + NewClass.__name__ = 'Multilingual%s' % old_cls.__name__ + return NewClass() if instantiate else NewClass + + class MultilingualQuerysetManager(models.Manager): """ This class gets hooked in MRO just before plain Manager, so that every call to @@ -455,13 +468,7 @@ def get_queryset(self): return self._patch_queryset(qs) def _patch_queryset(self, qs): - if qs.__class__ == models.query.QuerySet: - qs.__class__ = MultilingualQuerySet - else: - class NewClass(qs.__class__, MultilingualQuerySet): - pass - NewClass.__name__ = 'Multilingual%s' % qs.__class__.__name__ - qs.__class__ = NewClass + qs.__class__ = multilingual_queryset_factory(qs.__class__, instantiate=False) qs._post_init() qs._rewrite_applied_operations() return qs diff --git a/modeltranslation/tests/tests.py b/modeltranslation/tests/tests.py index e8d3ffa0..863e8417 100644 --- a/modeltranslation/tests/tests.py +++ b/modeltranslation/tests/tests.py @@ -2564,6 +2564,27 @@ def test_custom_manager_custom_method_name(self): qs = models.CustomManagerTestModel.objects.custom_qs() self.assertIsInstance(qs, MultilingualQuerySet) + def test_multilingual_queryset_pickling(self): + import pickle + from modeltranslation.manager import MultilingualQuerySet + + # typical + models.CustomManagerTestModel.objects.create(title='a') + qs = models.CustomManagerTestModel.objects.all() + serialized = pickle.dumps(qs) + deserialized = pickle.loads(serialized) + self.assertIsInstance(deserialized, MultilingualQuerySet) + self.assertListEqual(list(qs), list(deserialized)) + + # Generated class + models.CustomManager2TestModel.objects.create() + qs = models.CustomManager2TestModel.objects.all() + serialized = pickle.dumps(qs) + deserialized = pickle.loads(serialized) + self.assertIsInstance(deserialized, MultilingualQuerySet) + self.assertIsInstance(deserialized, models.CustomQuerySet) + self.assertListEqual(list(qs), list(deserialized)) + def test_non_objects_manager(self): """Test if managers other than ``objects`` are patched too""" from modeltranslation.manager import MultilingualManager From 369293b14d8cd1f3d1d77fc0232eb351fb3188ca Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Fri, 24 Oct 2014 20:23:10 +0200 Subject: [PATCH 083/170] Get language getting from the field attribute rather than name. The method seems to be not used anymore, but fix it nevertheless. --- modeltranslation/admin.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/modeltranslation/admin.py b/modeltranslation/admin.py index 83d026aa..cac26671 100644 --- a/modeltranslation/admin.py +++ b/modeltranslation/admin.py @@ -219,8 +219,7 @@ def get_translation_field_excludes(self, exclude_languages=None): exclude = [] for orig_fieldname, translation_fields in self.trans_opts.fields.items(): for tfield in translation_fields: - language = tfield.name.split('_')[-1] - if language in excl_languages and tfield not in exclude: + if tfield.language in excl_languages and tfield not in exclude: exclude.append(tfield) return tuple(exclude) From ef45ac7544f95e8a1b400282f75a02dd48e61fcb Mon Sep 17 00:00:00 2001 From: wrwrwr Date: Sat, 25 Oct 2014 16:45:57 +0200 Subject: [PATCH 084/170] Added flake8 config to tox.ini. --- tox.ini | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tox.ini b/tox.ini index 7aa1bbf6..c0435965 100644 --- a/tox.ini +++ b/tox.ini @@ -1,3 +1,7 @@ +[flake8] +max-line-length = 100 +exclude = .tox,docs/modeltranslation/conf.py + [tox] distribute = False envlist = From e806c03277b8e9a56c410a8c339562e205cc1a51 Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Sat, 25 Oct 2014 18:25:57 +0200 Subject: [PATCH 085/170] Let's check if flake8 in travis can read parameters from tox.ini. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 40af6a55..438983c3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -66,7 +66,7 @@ matrix: env: DJANGO=1.7 DB=mysql before_install: - pip install -q flake8 --use-mirrors - - PYFLAKES_NODOCTEST=1 flake8 --max-line-length=100 modeltranslation + - PYFLAKES_NODOCTEST=1 flake8 modeltranslation before_script: - mysql -e 'create database modeltranslation;' - psql -c 'create database modeltranslation;' -U postgres From bd0ea50c507044e751eba52de2518c5f0eea119d Mon Sep 17 00:00:00 2001 From: Dirk Eschler Date: Wed, 29 Oct 2014 17:01:33 +0100 Subject: [PATCH 086/170] Set TabbedTranslationAdmin class depending on Django version. Added a new class for Django 1.5 which depends on older jquery/jquery-ui versions. Updated the jquery/jquery-ui versions for the existing admin classes (ref #270). --- modeltranslation/admin.py | 38 ++++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/modeltranslation/admin.py b/modeltranslation/admin.py index cac26671..b8467ce8 100644 --- a/modeltranslation/admin.py +++ b/modeltranslation/admin.py @@ -342,7 +342,12 @@ class TranslationGenericStackedInline(TranslationInlineModelAdmin, generic.Gener pass -class TabbedDjangoJqueryTranslationAdmin(TranslationAdmin): +class TabbedDjango15JqueryTranslationAdmin(TranslationAdmin): + """ + Convenience class which includes the necessary static files for tabbed + translation fields. Reuses Django's internal jquery version. Django 1.5 + included jquery 1.4.2 which is known to work well with jquery-ui 1.8.2. + """ class Media: js = ( 'modeltranslation/js/force_jquery.js', @@ -352,16 +357,41 @@ class Media: css = { 'all': ('modeltranslation/css/tabbed_translation_fields.css',), } -TabbedTranslationAdmin = TabbedDjangoJqueryTranslationAdmin + + +class TabbedDjangoJqueryTranslationAdmin(TranslationAdmin): + """ + Convenience class which includes the necessary media files for tabbed + translation fields. Reuses Django's internal jquery version. + """ + class Media: + js = ( + 'modeltranslation/js/force_jquery.js', + '//ajax.googleapis.com/ajax/libs/jqueryui/1.11.2/jquery-ui.min.js', + 'modeltranslation/js/tabbed_translation_fields.js', + ) + css = { + 'all': ('modeltranslation/css/tabbed_translation_fields.css',), + } class TabbedExternalJqueryTranslationAdmin(TranslationAdmin): + """ + Convenience class which includes the necessary media files for tabbed + translation fields. Loads recent jquery version from a cdn. + """ class Media: js = ( - '//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js', - '//ajax.googleapis.com/ajax/libs/jqueryui/1.10.2/jquery-ui.min.js', + '//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js', + '//ajax.googleapis.com/ajax/libs/jqueryui/1.11.2/jquery-ui.min.js', 'modeltranslation/js/tabbed_translation_fields.js', ) css = { 'screen': ('modeltranslation/css/tabbed_translation_fields.css',), } + + +if django.VERSION < (1, 6): + TabbedTranslationAdmin = TabbedDjango15JqueryTranslationAdmin +else: + TabbedTranslationAdmin = TabbedDjangoJqueryTranslationAdmin From 352902556e42b417e42f459ca4bf759fc08e6615 Mon Sep 17 00:00:00 2001 From: Alex Marandon Date: Fri, 14 Nov 2014 15:14:55 +0100 Subject: [PATCH 087/170] Update deprecated imports with Django >= 1.7 --- modeltranslation/admin.py | 10 +++++++--- modeltranslation/models.py | 3 ++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/modeltranslation/admin.py b/modeltranslation/admin.py index b8467ce8..d86e0992 100644 --- a/modeltranslation/admin.py +++ b/modeltranslation/admin.py @@ -4,14 +4,18 @@ import django from django.contrib import admin from django.contrib.admin.options import BaseModelAdmin, flatten_fieldsets, InlineModelAdmin -from django.contrib.contenttypes import generic from django import forms # Ensure that models are registered for translation before TranslationAdmin # runs. The import is supposed to resolve a race condition between model import # and translation registration in production (see issue #19). if django.VERSION < (1, 7): + from django.contrib.contenttypes.generic import GenericTabularInline + from django.contrib.contenttypes.generic import GenericStackedInline import modeltranslation.models # NOQA +else: + from django.contrib.contenttypes.admin import GenericTabularInline + from django.contrib.contenttypes.admin import GenericStackedInline from modeltranslation import settings as mt_settings from modeltranslation.translator import translator from modeltranslation.utils import ( @@ -334,11 +338,11 @@ class TranslationStackedInline(TranslationInlineModelAdmin, admin.StackedInline) pass -class TranslationGenericTabularInline(TranslationInlineModelAdmin, generic.GenericTabularInline): +class TranslationGenericTabularInline(TranslationInlineModelAdmin, GenericTabularInline): pass -class TranslationGenericStackedInline(TranslationInlineModelAdmin, generic.GenericStackedInline): +class TranslationGenericStackedInline(TranslationInlineModelAdmin, GenericStackedInline): pass diff --git a/modeltranslation/models.py b/modeltranslation/models.py index d544de63..a54c835b 100644 --- a/modeltranslation/models.py +++ b/modeltranslation/models.py @@ -12,14 +12,15 @@ def autodiscover(): import sys import copy from django.conf import settings - from django.utils.importlib import import_module from django.utils.module_loading import module_has_submodule from modeltranslation.translator import translator from modeltranslation.settings import TRANSLATION_FILES, DEBUG if django.VERSION < (1, 7): + from django.utils.importlib import import_module mods = [(app, import_module(app)) for app in settings.INSTALLED_APPS] else: + from importlib import import_module from django.apps import apps mods = [(app_config.name, app_config.module) for app_config in apps.get_app_configs()] From 4d1f5af12964ff66a7a7e564ba19596b51c911a9 Mon Sep 17 00:00:00 2001 From: Fabio Caccamo Date: Thu, 12 Mar 2015 17:58:56 +0100 Subject: [PATCH 088/170] add missed jquery browser plugin --- modeltranslation/admin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/modeltranslation/admin.py b/modeltranslation/admin.py index d86e0992..2afdda6e 100644 --- a/modeltranslation/admin.py +++ b/modeltranslation/admin.py @@ -388,6 +388,7 @@ class Media: js = ( '//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js', '//ajax.googleapis.com/ajax/libs/jqueryui/1.11.2/jquery-ui.min.js', + '//cdn.jsdelivr.net/jquery.mb.browser/0.1/jquery.mb.browser.min.js', 'modeltranslation/js/tabbed_translation_fields.js', ) css = { From 1fc52f9918db0d9ce34bd039a54650e179e35fee Mon Sep 17 00:00:00 2001 From: Fabio Caccamo Date: Thu, 12 Mar 2015 18:01:52 +0100 Subject: [PATCH 089/170] add missed jquery browser plugin --- modeltranslation/admin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modeltranslation/admin.py b/modeltranslation/admin.py index 2afdda6e..ad790e55 100644 --- a/modeltranslation/admin.py +++ b/modeltranslation/admin.py @@ -356,6 +356,7 @@ class Media: js = ( 'modeltranslation/js/force_jquery.js', '//ajax.googleapis.com/ajax/libs/jqueryui/1.8.2/jquery-ui.min.js', + '//cdn.jsdelivr.net/jquery.mb.browser/0.1/jquery.mb.browser.min.js', 'modeltranslation/js/tabbed_translation_fields.js', ) css = { @@ -372,6 +373,7 @@ class Media: js = ( 'modeltranslation/js/force_jquery.js', '//ajax.googleapis.com/ajax/libs/jqueryui/1.11.2/jquery-ui.min.js', + '//cdn.jsdelivr.net/jquery.mb.browser/0.1/jquery.mb.browser.min.js', 'modeltranslation/js/tabbed_translation_fields.js', ) css = { From 36e331c73e9ff26011dc21200c6427cfe9328899 Mon Sep 17 00:00:00 2001 From: Vladimir Sinitsin Date: Wed, 1 Apr 2015 15:30:20 +0600 Subject: [PATCH 090/170] Damn bug when using a queryset with select related Please fix it and upload a fixed version on PyPI --- modeltranslation/manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modeltranslation/manager.py b/modeltranslation/manager.py index fee8ba97..7755a7d2 100644 --- a/modeltranslation/manager.py +++ b/modeltranslation/manager.py @@ -258,7 +258,7 @@ def _rewrite_order(self): def _rewrite_select_related(self): if isinstance(self.query.select_related, dict): - new = [] + new = {} for field_name, value in self.query.select_related.items(): new[rewrite_order_lookup_key(self.model, field_name)] = value self.query.select_related = new From 01bc89f5b74dffac27a1a9e8242b45868c56a389 Mon Sep 17 00:00:00 2001 From: deschler Date: Thu, 2 Apr 2015 12:46:23 +0200 Subject: [PATCH 091/170] Prepared 0.8.1 release (ref #298). --- AUTHORS.rst | 3 +++ CHANGELOG.txt | 12 ++++++++++++ PKG-INFO | 2 +- modeltranslation/__init__.py | 2 +- 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index ef41b26b..7c45b852 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -31,6 +31,9 @@ Contributors * Mathieu Leplatre * Thom Wiggers * Warnar Boekkooi +* Alex Marandon +* Fabio Caccamo +* Vladimir Sinitsin * And many more ... (if you miss your name here, please let us know!) .. _django-linguo: https://github.com/zmathew/django-linguo diff --git a/CHANGELOG.txt b/CHANGELOG.txt index f98c3f9b..09a45184 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,15 @@ +v0.8.1 +====== +Date: 2015-04-02 + + FIXED: Using a queryset with select related. + (resolves issue #298, thanks Vladimir Sinitsin) + FIXED: Added missing jquery browser plugin. + (resolves issue #270, thanks Fabio Caccamo) + FIXED: Deprecated imports with Django >= 1.7 + (resolves issue #283, thanks Alex Marandon) + + v0.8 ==== Date: 2014-10-06 diff --git a/PKG-INFO b/PKG-INFO index 38731c06..13aecd09 100644 --- a/PKG-INFO +++ b/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 1.0 Name: django-modeltranslation -Version: 0.8 +Version: 0.8.1 Summary: Translates Django models using a registration approach. Home-page: https://github.com/deschler/django-modeltranslation Author: Peter Eschler, diff --git a/modeltranslation/__init__.py b/modeltranslation/__init__.py index f9e2dbe3..2a4339d7 100644 --- a/modeltranslation/__init__.py +++ b/modeltranslation/__init__.py @@ -3,7 +3,7 @@ Version code adopted from Django development version. https://github.com/django/django """ -VERSION = (0, 8, 0, 'final', 0) +VERSION = (0, 8, 1, 'final', 0) default_app_config = 'modeltranslation.apps.ModeltranslationConfig' From d8e6f32cbd3843eff2ccc2d4ecad012a63b368b6 Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Thu, 2 Apr 2015 22:54:15 +0200 Subject: [PATCH 092/170] Django 1.8 compatibility changes (ref #299). Thanks to Luca Corti for initial commit. --- .../management/commands/loaddata.py | 33 ++++++++++++------- modeltranslation/manager.py | 29 +++++++++++++--- modeltranslation/tests/tests.py | 26 ++++++++++----- modeltranslation/utils.py | 2 ++ runtests.py | 1 + 5 files changed, 67 insertions(+), 24 deletions(-) diff --git a/modeltranslation/management/commands/loaddata.py b/modeltranslation/management/commands/loaddata.py index 5912bd0d..0fb039de 100644 --- a/modeltranslation/management/commands/loaddata.py +++ b/modeltranslation/management/commands/loaddata.py @@ -1,5 +1,3 @@ -from optparse import make_option, OptionValueError - from django import VERSION from django.core.management.commands.loaddata import Command as LoadDataCommand @@ -13,23 +11,36 @@ ALLOWED_FOR_PRINT = ', '.join(str(i) for i in (0, ) + ALLOWED[1:]) # For pretty-printing -def check_mode(option, opt_str, value, parser): +def check_mode(option, opt_str, value, parser, namespace=None): if value == '0' or value.lower() == 'false': value = False if value not in ALLOWED: - raise OptionValueError("%s option can be only one of: %s" % (opt_str, ALLOWED_FOR_PRINT)) - setattr(parser.values, option.dest, value) + raise ValueError("%s option can be only one of: %s" % (opt_str, ALLOWED_FOR_PRINT)) + setattr(namespace or parser.values, option.dest, value) class Command(LoadDataCommand): leave_locale_alone = mt_settings.LOADDATA_RETAIN_LOCALE # Django 1.6 - option_list = LoadDataCommand.option_list + ( - make_option('--populate', action='callback', callback=check_mode, dest='populate', - type='string', - metavar='MODE', help='Using this option will cause fixtures to be loaded under ' - 'auto-population MODE. Allowed values are: %s' % ALLOWED_FOR_PRINT), - ) + help = ('Using this option will cause fixtures to be loaded under auto-population MODE.' + + 'Allowed values are: %s' % ALLOWED_FOR_PRINT) + if VERSION < (1, 8): + from optparse import make_option + option_list = LoadDataCommand.option_list + ( + make_option('--populate', action='callback', callback=check_mode, type='string', + dest='populate', metavar='MODE', help=help), + ) + else: + import argparse + + class CheckAction(argparse.Action): + def __call__(self, parser, namespace, value, option_string=None): + check_mode(self, option_string, value, parser, namespace) + + def add_arguments(self, parser): + super(Command, self).add_arguments(parser) + parser.add_argument('--populate', action=self.CheckAction, type=str, dest='populate', + metavar='MODE', help=self.help) def __init__(self): super(Command, self).__init__() diff --git a/modeltranslation/manager.py b/modeltranslation/manager.py index 7755a7d2..f91788bb 100644 --- a/modeltranslation/manager.py +++ b/modeltranslation/manager.py @@ -9,7 +9,13 @@ from django.db import models from django.db.models import FieldDoesNotExist -from django.db.models.fields.related import RelatedField, RelatedObject +try: + from django.db.models.fields.related import RelatedObject + from django.db.models.fields.related import RelatedField + NEW_META_API = False +except ImportError: + NEW_META_API = True + from django.db.models.sql.where import Constraint from django.utils.six import moves from django.utils.tree import Node @@ -117,8 +123,16 @@ def rewrite_order_lookup_key(model, lookup_key): def get_fields_to_translatable_models(model): - if model not in _F2TM_CACHE: - results = [] + if model in _F2TM_CACHE: + return _F2TM_CACHE[model] + + results = [] + if NEW_META_API: + for f in model._meta.get_fields(): + if f.is_relation: + if get_translatable_fields_for_model(f.related_model) is not None: + results.append((f.name, f.related_model)) + else: for field_name in model._meta.get_all_field_names(): field_object, modelclass, direct, m2m = model._meta.get_field_by_name(field_name) # Direct relationship @@ -129,7 +143,7 @@ def get_fields_to_translatable_models(model): if isinstance(field_object, RelatedObject): if get_translatable_fields_for_model(field_object.model) is not None: results.append((field_name, field_object.model)) - _F2TM_CACHE[model] = dict(results) + _F2TM_CACHE[model] = dict(results) return _F2TM_CACHE[model] _C2F_CACHE = {} @@ -221,7 +235,7 @@ def select_related(self, *fields, **kwargs): # This method was not present in django-linguo def _rewrite_col(self, col): - """Django 1.7 column name rewriting""" + """Django >= 1.7 column name rewriting""" if isinstance(col, Col): new_name = rewrite_lookup_key(self.model, col.target.name) if col.target.name != new_name: @@ -282,6 +296,11 @@ def _rewrite_f(self, q): return q if isinstance(q, Node): q.children = list(map(self._rewrite_f, q.children)) + # Django >= 1.8 + if hasattr(q, 'lhs'): + q.lhs = self._rewrite_f(q.lhs) + if hasattr(q, 'rhs'): + q.rhs = self._rewrite_f(q.rhs) return q def _filter_or_exclude(self, negate, *args, **kwargs): diff --git a/modeltranslation/tests/tests.py b/modeltranslation/tests/tests.py index de733af9..cb675314 100644 --- a/modeltranslation/tests/tests.py +++ b/modeltranslation/tests/tests.py @@ -9,7 +9,6 @@ from django import forms from django.conf import settings as django_settings from django.contrib.admin.sites import AdminSite -from django.contrib.auth.models import User from django.core.exceptions import ValidationError, ImproperlyConfigured from django.core.files.base import ContentFile from django.core.files.storage import default_storage @@ -63,6 +62,14 @@ def default_fallback(): MODELTRANSLATION_FALLBACK_LANGUAGES=(mt_settings.DEFAULT_LANGUAGE,)) +class dummy_context_mgr(): + def __enter__(self): + return None + + def __exit__(self, _type, value, traceback): + return False + + @override_settings(**TEST_SETTINGS) class ModeltranslationTransactionTestBase(TransactionTestCase): urls = 'modeltranslation.tests.urls' @@ -77,10 +84,12 @@ def setUpClass(cls): default testrunner's db creation modeltranslation.tests was not in INSTALLED_APPS """ super(ModeltranslationTransactionTestBase, cls).setUpClass() - if not ModeltranslationTestBase.synced: + if not ModeltranslationTransactionTestBase.synced: # In order to perform only one syncdb - ModeltranslationTestBase.synced = True - with override_settings(**TEST_SETTINGS): + ModeltranslationTransactionTestBase.synced = True + mgr = (override_settings(**TEST_SETTINGS) if django.VERSION < (1, 8) + else dummy_context_mgr()) + with mgr: # 1. Reload translation in case USE_I18N was False from django.utils import translation as dj_trans imp.reload(dj_trans) @@ -108,7 +117,8 @@ def setUpClass(cls): # 5. Syncdb (``migrate=False`` in case of south) from django.db import connections, DEFAULT_DB_ALIAS - call_command('syncdb', verbosity=0, migrate=False, interactive=False, + cmd = 'syncdb' if django.VERSION < (1, 8) else 'migrate' + call_command(cmd, verbosity=0, migrate=False, interactive=False, database=connections[DEFAULT_DB_ALIAS].alias, load_initial_data=False) # A rather dirty trick to import models into module namespace, but not before @@ -125,7 +135,7 @@ def tearDown(self): trans_real.activate(self._old_language) -class ModeltranslationTestBase(ModeltranslationTransactionTestBase, TestCase): +class ModeltranslationTestBase(TestCase, ModeltranslationTransactionTestBase): pass @@ -239,7 +249,7 @@ def test_registration(self): # Try to get options for a model that is not registered self.assertRaises(translator.NotRegistered, - translator.translator.get_options_for_model, User) + translator.translator.get_options_for_model, models.ThirdPartyModel) # Ensure that a base can't be registered after a subclass. self.assertRaises(translator.DescendantRegistered, @@ -2762,7 +2772,7 @@ def assertDeferred(self, use_defer, *fields): self.assertEqual('title_de', inst1.title) self.assertEqual('title_de', inst2.title) - def test_deferred(self): + def _deactivated_test_deferred(self): """ Check if ``only`` and ``defer`` are working. """ diff --git a/modeltranslation/utils.py b/modeltranslation/utils.py index 7461aa23..aff60dbd 100644 --- a/modeltranslation/utils.py +++ b/modeltranslation/utils.py @@ -15,6 +15,8 @@ def get_language(): settings.LANGUAGES (Django does not seem to guarantee this for us). """ lang = _get_language() + if lang is None: # Django >= 1.8 + return settings.DEFAULT_LANGUAGE if lang not in settings.AVAILABLE_LANGUAGES and '-' in lang: lang = lang.split('-')[0] if lang in settings.AVAILABLE_LANGUAGES: diff --git a/runtests.py b/runtests.py index 86cf37b5..72c503b9 100755 --- a/runtests.py +++ b/runtests.py @@ -37,6 +37,7 @@ def runtests(): settings.configure( DATABASES=DATABASES, INSTALLED_APPS=( + 'django.contrib.contenttypes', 'modeltranslation', ), ROOT_URLCONF=None, # tests override urlconf, but it still needs to be defined From 018716eb5fc52728c9db6403165a9836b1ba0d60 Mon Sep 17 00:00:00 2001 From: Luca Corti Date: Thu, 2 Apr 2015 23:39:49 +0200 Subject: [PATCH 093/170] Add tests for django 1.8 to travis.yml --- .travis.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.travis.yml b/.travis.yml index 438983c3..1d627a06 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,6 +18,9 @@ env: - DJANGO=1.7 DB=sqlite - DJANGO=1.7 DB=postgres - DJANGO=1.7 DB=mysql + - DJANGO=1.8 DB=sqlite + - DJANGO=1.8 DB=postgres + - DJANGO=1.8 DB=mysql matrix: exclude: - python: "3.2" @@ -45,6 +48,12 @@ matrix: env: DJANGO=1.7 DB=postgres - python: "2.6" env: DJANGO=1.7 DB=mysql + - python: "2.6" + env: DJANGO=1.8 DB=sqlite + - python: "2.6" + env: DJANGO=1.8 DB=postgres + - python: "2.6" + env: DJANGO=1.8 DB=mysql - python: "3.2" env: DJANGO=1.5 DB=mysql @@ -64,6 +73,12 @@ matrix: env: DJANGO=1.7 DB=mysql - python: "3.4" env: DJANGO=1.7 DB=mysql + - python: "3.2" + env: DJANGO=1.8 DB=mysql + - python: "3.3" + env: DJANGO=1.8 DB=mysql + - python: "3.4" + env: DJANGO=1.8 DB=mysql before_install: - pip install -q flake8 --use-mirrors - PYFLAKES_NODOCTEST=1 flake8 modeltranslation From 5d74f42c60d8206bd5df8c33070992f1f12cfc75 Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Mon, 13 Apr 2015 00:04:41 +0200 Subject: [PATCH 094/170] Add 1.8 test to tox. --- tox.ini | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tox.ini b/tox.ini index c0435965..be9fa228 100644 --- a/tox.ini +++ b/tox.ini @@ -5,6 +5,9 @@ exclude = .tox,docs/modeltranslation/conf.py [tox] distribute = False envlist = + py33-1.8.X, + py32-1.8.X, + py27-1.8.X, py33-1.7.X, py32-1.7.X, py27-1.7.X, @@ -26,6 +29,24 @@ commands = {envpython} runtests.py +[testenv:py33-1.8.X] +basepython = python3.3 +deps = + Django>=1.8,<1.9 + Pillow + +[testenv:py32-1.8.X] +basepython = python3.2 +deps = + Django>=1.8,<1.9 + Pillow + +[testenv:py27-1.8.X] +basepython = python2.7 +deps = + Django>=1.8,<1.9 + Pillow + [testenv:py33-1.7.X] basepython = python3.3 deps = From 896dc140c0407ffda61daa9c83a362ec3456d884 Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Mon, 13 Apr 2015 08:57:01 +0200 Subject: [PATCH 095/170] Fix testrunner for postgres (ref #299). --- runtests.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/runtests.py b/runtests.py index 72c503b9..08e133a4 100755 --- a/runtests.py +++ b/runtests.py @@ -28,10 +28,9 @@ def runtests(): 'ENGINE': 'django.db.backends.postgresql_psycopg2', 'USER': 'postgres', 'NAME': 'modeltranslation', - 'OPTIONS': { - 'autocommit': True, - } }) + if django.VERSION < (1, 6): + DATABASES['default']['OPTIONS'] = {'autocommit': True} # Configure test environment settings.configure( From f6d02c1e5aa9583a87220a0b904c29f000a14b02 Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Mon, 13 Apr 2015 22:31:09 +0200 Subject: [PATCH 096/170] Fix deferring - enable skipped test (#299). --- modeltranslation/tests/tests.py | 2 +- modeltranslation/translator.py | 26 ++++++++++++++++++++++++-- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/modeltranslation/tests/tests.py b/modeltranslation/tests/tests.py index cb675314..a513ffc6 100644 --- a/modeltranslation/tests/tests.py +++ b/modeltranslation/tests/tests.py @@ -2772,7 +2772,7 @@ def assertDeferred(self, use_defer, *fields): self.assertEqual('title_de', inst1.title) self.assertEqual('title_de', inst2.title) - def _deactivated_test_deferred(self): + def test_deferred(self): """ Check if ``only`` and ``defer`` are working. """ diff --git a/modeltranslation/translator.py b/modeltranslation/translator.py index c50a5c45..18bc7430 100644 --- a/modeltranslation/translator.py +++ b/modeltranslation/translator.py @@ -242,6 +242,22 @@ def new_clean_fields(self, exclude=None): model.clean_fields = new_clean_fields +def patch_get_deferred_fields(model): + """ + Django >= 1.8: patch detecting deferred fields. Crucial for only/defer to work. + """ + if not hasattr(model, 'get_deferred_fields'): + return + old_get_deferred_fields = model.get_deferred_fields + + def new_get_deferred_fields(self): + sup = old_get_deferred_fields(self) + if hasattr(self, '_fields_were_deferred'): + sup.update(self._fields_were_deferred) + return sup + model.get_deferred_fields = new_get_deferred_fields + + def patch_metaclass(model): """ Monkey patches original model metaclass to exclude translated fields on deferred subclasses. @@ -260,8 +276,13 @@ class translation_deferred_mcs(old_mcs): def __new__(cls, name, bases, attrs): if attrs.get('_deferred', False): opts = translator.get_options_for_model(model) + were_deferred = set() for field_name in opts.fields.keys(): - attrs.pop(field_name, None) + if attrs.pop(field_name, None): + # Field was deferred. Store this for future reference. + were_deferred.add(field_name) + if len(were_deferred): + attrs['_fields_were_deferred'] = were_deferred return super(translation_deferred_mcs, cls).__new__(cls, name, bases, attrs) # Assign to __metaclass__ wouldn't work, since metaclass search algorithm check for __class__. # http://docs.python.org/2/reference/datamodel.html#__metaclass__ @@ -410,8 +431,9 @@ def register(self, model_or_iterable, opts_class=None, **options): # Patch clean_fields to verify form field clearing patch_clean_fields(model) - # Patch __metaclass__ to allow deferring to work + # Patch __metaclass__ and other methods to allow deferring to work patch_metaclass(model) + patch_get_deferred_fields(model) # Substitute original field with descriptor model_fallback_languages = getattr(opts, 'fallback_languages', None) From 2a63788e0dcfc3f7baaa5dbf06b2054a7c8e7a4c Mon Sep 17 00:00:00 2001 From: Dirk Eschler Date: Thu, 16 Apr 2015 10:46:15 +0200 Subject: [PATCH 097/170] Added requirements for 0.9 release (ref #299). --- docs/modeltranslation/installation.rst | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/modeltranslation/installation.rst b/docs/modeltranslation/installation.rst index 0d233ea8..21ddae5c 100644 --- a/docs/modeltranslation/installation.rst +++ b/docs/modeltranslation/installation.rst @@ -9,7 +9,13 @@ Requirements +------------------+------------+-----------+ | Modeltranslation | Python | Django | +==================+============+===========+ -| >=0.8 | 3.2 - 3.4 | 1.5 - 1.7 | +| >=0.9 | 3.2 - 3.4 | 1.5 - 1.8 | +| +------------+-----------+ +| | 2.7 | 1.8 | +| +------------+-----------+ +| | 2.6 - 2.7 | 1.4 - 1.6 | ++------------------+------------+-----------+ +| ==0.8 | 3.2 - 3.4 | 1.5 - 1.7 | | +------------+-----------+ | | 2.7 | 1.7 | | +------------+-----------+ From 9c7ac4e02d73ddde93714f66b2f2a8943e5dee4a Mon Sep 17 00:00:00 2001 From: Dirk Eschler Date: Thu, 16 Apr 2015 10:51:15 +0200 Subject: [PATCH 098/170] Prepared 0.9 release (ref #299). --- AUTHORS.rst | 1 + CHANGELOG.txt | 8 ++++++++ PKG-INFO | 2 +- modeltranslation/__init__.py | 2 +- 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 7c45b852..b7e3ce11 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -34,6 +34,7 @@ Contributors * Alex Marandon * Fabio Caccamo * Vladimir Sinitsin +* Luca Corti * And many more ... (if you miss your name here, please let us know!) .. _django-linguo: https://github.com/zmathew/django-linguo diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 09a45184..d084be12 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,11 @@ +v0.9 +==== +Date: 2015-04-16 + + ADDED: Support for Django 1.8 and the new meta API. + (resolves issue #299, thanks Luca Corti and Jacek Tomaszewski) + + v0.8.1 ====== Date: 2015-04-02 diff --git a/PKG-INFO b/PKG-INFO index 13aecd09..4fa83bbb 100644 --- a/PKG-INFO +++ b/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 1.0 Name: django-modeltranslation -Version: 0.8.1 +Version: 0.9 Summary: Translates Django models using a registration approach. Home-page: https://github.com/deschler/django-modeltranslation Author: Peter Eschler, diff --git a/modeltranslation/__init__.py b/modeltranslation/__init__.py index 2a4339d7..76ebecca 100644 --- a/modeltranslation/__init__.py +++ b/modeltranslation/__init__.py @@ -3,7 +3,7 @@ Version code adopted from Django development version. https://github.com/django/django """ -VERSION = (0, 8, 1, 'final', 0) +VERSION = (0, 9, 0, 'final', 0) default_app_config = 'modeltranslation.apps.ModeltranslationConfig' From c34ae160d18115e9308e57aa8c7abc0aecb700db Mon Sep 17 00:00:00 2001 From: ellmetha Date: Mon, 4 May 2015 13:52:06 +0200 Subject: [PATCH 099/170] Fixed translatable fields discovery with the new _meta API and generic relations --- modeltranslation/manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modeltranslation/manager.py b/modeltranslation/manager.py index f91788bb..6127e315 100644 --- a/modeltranslation/manager.py +++ b/modeltranslation/manager.py @@ -129,7 +129,7 @@ def get_fields_to_translatable_models(model): results = [] if NEW_META_API: for f in model._meta.get_fields(): - if f.is_relation: + if f.is_relation and f.related_model: if get_translatable_fields_for_model(f.related_model) is not None: results.append((f.name, f.related_model)) else: From 54883a13cf70f827c286a996afd4a5fab8c6f305 Mon Sep 17 00:00:00 2001 From: ellmetha Date: Mon, 4 May 2015 13:57:23 +0200 Subject: [PATCH 100/170] Comment added to the previous fix --- modeltranslation/manager.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modeltranslation/manager.py b/modeltranslation/manager.py index 6127e315..f1c3fb0b 100644 --- a/modeltranslation/manager.py +++ b/modeltranslation/manager.py @@ -130,6 +130,10 @@ def get_fields_to_translatable_models(model): if NEW_META_API: for f in model._meta.get_fields(): if f.is_relation and f.related_model: + # The new get_field() will find GenericForeignKey relations. + # In that case the 'related_model' attribute is set to None + # so it is necessary to check for this value before trying to + # get translatable fields. if get_translatable_fields_for_model(f.related_model) is not None: results.append((f.name, f.related_model)) else: From 1f7ce84bf2c3b69e6bfe0144b820be5891441b23 Mon Sep 17 00:00:00 2001 From: Dirk Eschler Date: Wed, 6 May 2015 12:35:57 +0200 Subject: [PATCH 101/170] Fixed deprecated meta api usage for Django 1.8, module_name was renamed to model_name (ref #310). --- .../management/commands/sync_translation_fields.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/modeltranslation/management/commands/sync_translation_fields.py b/modeltranslation/management/commands/sync_translation_fields.py index a6e38f8a..4243ec32 100644 --- a/modeltranslation/management/commands/sync_translation_fields.py +++ b/modeltranslation/management/commands/sync_translation_fields.py @@ -10,6 +10,7 @@ Credits: Heavily inspired by django-transmeta's sync_transmeta_db command. """ from optparse import make_option +import django from django.core.management.base import NoArgsCommand from django.core.management.color import no_style from django.db import connection, transaction @@ -67,7 +68,11 @@ def handle_noargs(self, **options): models = translator.get_registered_models(abstract=False) for model in models: db_table = model._meta.db_table - model_full_name = '%s.%s' % (model._meta.app_label, model._meta.module_name) + if django.VERSION < (1, 8): + model_name = model._meta.module_name + else: + model_name = model._meta.model_name + model_full_name = '%s.%s' % (model._meta.app_label, model_name) opts = translator.get_options_for_model(model) for field_name, fields in opts.local_fields.items(): # Take `db_column` attribute into account From 15c7d9d6f6f12a62ee01bfc069370d0c356e0220 Mon Sep 17 00:00:00 2001 From: Dirk Eschler Date: Wed, 6 May 2015 14:18:32 +0200 Subject: [PATCH 102/170] Omitted transaction.commit_unless_managed() call for newer Django versions. This is handled by the transaction system in Django 1.6 and above (close #310). --- .../management/commands/sync_translation_fields.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modeltranslation/management/commands/sync_translation_fields.py b/modeltranslation/management/commands/sync_translation_fields.py index 4243ec32..ac89948b 100644 --- a/modeltranslation/management/commands/sync_translation_fields.py +++ b/modeltranslation/management/commands/sync_translation_fields.py @@ -93,7 +93,8 @@ def handle_noargs(self, **options): else: print('SQL not executed') - transaction.commit_unless_managed() + if django.VERSION < (1, 6): + transaction.commit_unless_managed() if not found_missing_fields: print('No new translatable fields detected') From f23e2f7ee224b7f7b286c56a7b9982c2d1449f44 Mon Sep 17 00:00:00 2001 From: Mathias Date: Wed, 13 May 2015 14:06:44 +0200 Subject: [PATCH 103/170] Better support for django 1.8 (Fixes #304) --- modeltranslation/translator.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/modeltranslation/translator.py b/modeltranslation/translator.py index 18bc7430..c6edfb64 100644 --- a/modeltranslation/translator.py +++ b/modeltranslation/translator.py @@ -150,7 +150,12 @@ def add_translation_fields(model, opts): # Rebuild information about parents fields. If there are opts.local_fields, field cache would be # invalidated (by model._meta.add_field() function). Otherwise, we need to do it manually. if len(opts.local_fields) == 0: - model._meta._fill_fields_cache() + try: + model._meta._fill_fields_cache() + except AttributeError: + # Django 1.8 removed _fill_fields_cache + model._meta._expire_cache() + model._meta.get_fields() def has_custom_queryset(manager): From c4f80c77e9daac492ead06053e7a67d67249175a Mon Sep 17 00:00:00 2001 From: deschler Date: Thu, 14 May 2015 13:40:23 +0200 Subject: [PATCH 104/170] Prepared 0.9.1 release (ref #304). --- AUTHORS.rst | 4 ++++ CHANGELOG.txt | 15 +++++++++++++++ PKG-INFO | 2 +- modeltranslation/__init__.py | 2 +- 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index b7e3ce11..e7f4c55c 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -35,6 +35,10 @@ Contributors * Fabio Caccamo * Vladimir Sinitsin * Luca Corti +* Morgan Aubert +* Mathias Ettinger +* Daniel Loeb +* Stephen McDonald * And many more ... (if you miss your name here, please let us know!) .. _django-linguo: https://github.com/zmathew/django-linguo diff --git a/CHANGELOG.txt b/CHANGELOG.txt index d084be12..e387b2a6 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,18 @@ +v0.9.1 +====== +Date: 2015-05-14 + + FIXED: Handled deprecation of _meta._fill_fields_cache() for Django 1.8 + in add_translation_fields. + (resolves issue #304, thanks Mathias Ettinger and Daniel Loeb) + FIXED: Handled deprecation of transaction.commit_unless_managed for + Django 1.8 in sync_translation_fields management command. + (resolves issue #310) + FIXED: Fixed translatable fields discovery with the new _meta API and + generic relations for Django 1.8. + (resolves issue #309, thanks Morgan Aubert) + + v0.9 ==== Date: 2015-04-16 diff --git a/PKG-INFO b/PKG-INFO index 4fa83bbb..dcec7e6e 100644 --- a/PKG-INFO +++ b/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 1.0 Name: django-modeltranslation -Version: 0.9 +Version: 0.9.1 Summary: Translates Django models using a registration approach. Home-page: https://github.com/deschler/django-modeltranslation Author: Peter Eschler, diff --git a/modeltranslation/__init__.py b/modeltranslation/__init__.py index 76ebecca..d18b51ed 100644 --- a/modeltranslation/__init__.py +++ b/modeltranslation/__init__.py @@ -3,7 +3,7 @@ Version code adopted from Django development version. https://github.com/django/django """ -VERSION = (0, 9, 0, 'final', 0) +VERSION = (0, 9, 1, 'final', 0) default_app_config = 'modeltranslation.apps.ModeltranslationConfig' From 9480dba1c5bbec3dd54e489ab1fbf483043f2eb9 Mon Sep 17 00:00:00 2001 From: oliphunt Date: Thu, 21 May 2015 12:54:38 +0100 Subject: [PATCH 105/170] Add mt-bidi class to input fields for bi-directional languages. --- modeltranslation/admin.py | 7 +++++-- modeltranslation/utils.py | 7 +++++++ 2 files changed, 12 insertions(+), 2 deletions(-) mode change 100644 => 100755 modeltranslation/admin.py mode change 100644 => 100755 modeltranslation/utils.py diff --git a/modeltranslation/admin.py b/modeltranslation/admin.py old mode 100644 new mode 100755 index ad790e55..183c628a --- a/modeltranslation/admin.py +++ b/modeltranslation/admin.py @@ -19,7 +19,8 @@ from modeltranslation import settings as mt_settings from modeltranslation.translator import translator from modeltranslation.utils import ( - get_translation_fields, build_css_class, build_localized_fieldname, get_language, unique) + get_translation_fields, build_css_class, build_localized_fieldname, get_language, + get_language_bidi, unique) from modeltranslation.widgets import ClearableWidgetWrapper @@ -80,7 +81,9 @@ def patch_translation_field(self, db_field, field, **kwargs): css_classes.append('mt') # Add localized fieldname css class css_classes.append(build_css_class(db_field.name, 'mt-field')) - + # Add mt-bidi css class if language is bidirectional + if(get_language_bidi(db_field.language)): + css_classes.append('mt-bidi') if db_field.language == mt_settings.DEFAULT_LANGUAGE: # Add another css class to identify a default modeltranslation widget css_classes.append('mt-default') diff --git a/modeltranslation/utils.py b/modeltranslation/utils.py old mode 100644 new mode 100755 index aff60dbd..6f828eff --- a/modeltranslation/utils.py +++ b/modeltranslation/utils.py @@ -4,6 +4,7 @@ from django.utils import six from django.utils.encoding import force_text from django.utils.translation import get_language as _get_language +from django.utils.translation import get_language_info from django.utils.functional import lazy from modeltranslation import settings @@ -23,6 +24,12 @@ def get_language(): return lang return settings.DEFAULT_LANGUAGE +def get_language_bidi(lang): + """ + Check if a language is bi-directional. + """ + lang_info = get_language_info(lang) + return lang_info['bidi'] def get_translation_fields(field): """ From 96746672610bed0317201474e7beefd156284673 Mon Sep 17 00:00:00 2001 From: zenoamaro Date: Fri, 22 May 2015 23:24:57 +0200 Subject: [PATCH 106/170] Added decorator equivalent for `translator.register`. --- modeltranslation/decorators.py | 25 +++++++++++++++++++++++++ modeltranslation/translator.py | 3 ++- 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 modeltranslation/decorators.py diff --git a/modeltranslation/decorators.py b/modeltranslation/decorators.py new file mode 100644 index 00000000..7282e267 --- /dev/null +++ b/modeltranslation/decorators.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- + + +def register(model_or_iterable, **options): + """ + Registers the given model(s) with the given translation options. + + The model(s) should be Model classes, not instances. + + Fields declared for translation on a base class are inherited by + subclasses. If the model or one of its subclasses is already + registered for translation, this will raise an exception. + + @register(Author) + class AuthorTranslation(TranslationOptions): + pass + """ + from modeltranslation.translator import translator, TranslationOptions + + def wrapper(opts_class): + if not issubclass(opts_class, TranslationOptions): + raise ValueError('Wrapped class must subclass TranslationOptions.') + translator.register(model_or_iterable, opts_class, **options) + + return wrapper diff --git a/modeltranslation/translator.py b/modeltranslation/translator.py index c6edfb64..d7bd91f3 100644 --- a/modeltranslation/translator.py +++ b/modeltranslation/translator.py @@ -12,6 +12,7 @@ from modeltranslation.manager import (MultilingualManager, MultilingualQuerysetManager, rewrite_lookup_key) from modeltranslation.utils import build_localized_fieldname, parse_field +from modeltranslation.decorators import register class AlreadyRegistered(Exception): @@ -548,4 +549,4 @@ def get_options_for_model(self, model): # This global object represents the singleton translator object -translator = Translator() +translator = Translator() \ No newline at end of file From 80d804b03e886de1966cd55696bd68a86caffe4c Mon Sep 17 00:00:00 2001 From: zenoamaro Date: Fri, 22 May 2015 23:49:11 +0200 Subject: [PATCH 107/170] Testing registration with decorator. --- modeltranslation/tests/models.py | 6 ++++++ modeltranslation/tests/tests.py | 2 +- modeltranslation/tests/translation.py | 11 +++++++++-- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/modeltranslation/tests/models.py b/modeltranslation/tests/models.py index 5bd74bfd..dcbd4b20 100644 --- a/modeltranslation/tests/models.py +++ b/modeltranslation/tests/models.py @@ -307,3 +307,9 @@ class RequiredModel(models.Model): req = models.CharField(max_length=10) req_reg = models.CharField(max_length=10) req_en_reg = models.CharField(max_length=10) + + +# ######### Decorated registration testing + +class DecoratedModel(models.Model): + title = models.CharField(ugettext_lazy('title'), max_length=255) diff --git a/modeltranslation/tests/tests.py b/modeltranslation/tests/tests.py index a513ffc6..43c7bab6 100644 --- a/modeltranslation/tests/tests.py +++ b/modeltranslation/tests/tests.py @@ -42,7 +42,7 @@ request = None # How many models are registered for tests. -TEST_MODELS = 28 +TEST_MODELS = 29 class reload_override_settings(override_settings): diff --git a/modeltranslation/tests/translation.py b/modeltranslation/tests/translation.py index f5c4fe9f..0753cc06 100644 --- a/modeltranslation/tests/translation.py +++ b/modeltranslation/tests/translation.py @@ -1,14 +1,14 @@ # -*- coding: utf-8 -*- from django.utils.translation import ugettext_lazy -from modeltranslation.translator import translator, TranslationOptions +from modeltranslation.translator import translator, register, TranslationOptions from modeltranslation.tests.models import ( TestModel, FallbackModel, FallbackModel2, FileFieldsModel, ForeignKeyModel, OtherFieldsModel, DescriptorModel, AbstractModelA, AbstractModelB, Slugged, MetaData, Displayable, Page, RichText, RichTextPage, MultitableModelA, MultitableModelB, MultitableModelC, ManagerTestModel, CustomManagerTestModel, CustomManager2TestModel, GroupFieldsetsModel, NameModel, ThirdPartyRegisteredModel, ProxyTestModel, UniqueNullableModel, OneToOneFieldModel, - RequiredModel) + RequiredModel, DecoratedModel) class TestTranslationOptions(TranslationOptions): @@ -196,3 +196,10 @@ class RequiredTranslationOptions(TranslationOptions): 'default': ('req_reg',), # for all other languages } translator.register(RequiredModel, RequiredTranslationOptions) + + +# ######### Decorated registration testing + +@register(DecoratedModel) +class DecoratedTranslationOptions(TranslationOptions): + fields = ('title',) From 6b5d9355cdc06fca33bf256f731f3d96d001d52a Mon Sep 17 00:00:00 2001 From: zenoamaro Date: Fri, 22 May 2015 23:58:30 +0200 Subject: [PATCH 108/170] Added docs on registration using the decorator. --- docs/modeltranslation/registration.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/modeltranslation/registration.rst b/docs/modeltranslation/registration.rst index dcbd6380..cc5831f8 100644 --- a/docs/modeltranslation/registration.rst +++ b/docs/modeltranslation/registration.rst @@ -50,6 +50,18 @@ only imported. The ``NewsTranslationOptions`` derives from ``TranslationOptions`` and provides the ``fields`` attribute. Finally the model and its translation options are registered at the ``translator`` object. +If you prefer, ``register`` is also available as a decorator, much like the +one Django introduced for its admin in version 1.7. Usage is similar to the +standard ``register``, just provide arguments as you normally would, except +the options class which will be the decorated one:: + + from modeltranslation.translator import register, TranslationOptions + from news.models import News + + @register(News) + class NewsTranslationOptions(TranslationOptions): + fields = ('title', 'text',) + At this point you are mostly done and the model classes registered for translation will have been added some auto-magical fields. The next section explains how things are working under the hood. From 78eaccb927797107e5373cb79d55e9166f233085 Mon Sep 17 00:00:00 2001 From: zenoamaro Date: Sat, 23 May 2015 01:19:16 +0200 Subject: [PATCH 109/170] Satisfying flake8. --- modeltranslation/translator.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/modeltranslation/translator.py b/modeltranslation/translator.py index d7bd91f3..2f480ce1 100644 --- a/modeltranslation/translator.py +++ b/modeltranslation/translator.py @@ -12,7 +12,6 @@ from modeltranslation.manager import (MultilingualManager, MultilingualQuerysetManager, rewrite_lookup_key) from modeltranslation.utils import build_localized_fieldname, parse_field -from modeltranslation.decorators import register class AlreadyRegistered(Exception): @@ -549,4 +548,8 @@ def get_options_for_model(self, model): # This global object represents the singleton translator object -translator = Translator() \ No newline at end of file +translator = Translator() + + +# Re-export the decorator for convenience +from modeltranslation.decorators import register # NOQA re-export From 28eb14e4fa8154b15d5aec6290d0411cc8fed756 Mon Sep 17 00:00:00 2001 From: oliphunt Date: Tue, 26 May 2015 12:18:45 +0100 Subject: [PATCH 110/170] CS fixes. --- modeltranslation/admin.py | 2 +- modeltranslation/utils.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/modeltranslation/admin.py b/modeltranslation/admin.py index 183c628a..4d79dfcb 100755 --- a/modeltranslation/admin.py +++ b/modeltranslation/admin.py @@ -19,7 +19,7 @@ from modeltranslation import settings as mt_settings from modeltranslation.translator import translator from modeltranslation.utils import ( - get_translation_fields, build_css_class, build_localized_fieldname, get_language, + get_translation_fields, build_css_class, build_localized_fieldname, get_language, get_language_bidi, unique) from modeltranslation.widgets import ClearableWidgetWrapper diff --git a/modeltranslation/utils.py b/modeltranslation/utils.py index 6f828eff..c6712285 100755 --- a/modeltranslation/utils.py +++ b/modeltranslation/utils.py @@ -24,6 +24,7 @@ def get_language(): return lang return settings.DEFAULT_LANGUAGE + def get_language_bidi(lang): """ Check if a language is bi-directional. @@ -31,6 +32,7 @@ def get_language_bidi(lang): lang_info = get_language_info(lang) return lang_info['bidi'] + def get_translation_fields(field): """ Returns a list of localized fieldnames for a given field. From 620efeb660c284e5c45328e6f1bcedcfae776e17 Mon Sep 17 00:00:00 2001 From: Dirk Eschler Date: Sun, 28 Jun 2015 23:18:47 +0200 Subject: [PATCH 111/170] Fixed broken shields and used flat-square style. --- README.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 07cbe72d..53b79dfc 100644 --- a/README.rst +++ b/README.rst @@ -14,21 +14,21 @@ may they use translations or not, and you never have to touch the original model class. -.. image:: http://img.shields.io/travis/deschler/django-modeltranslation/master.png?style=flat +.. image:: http://img.shields.io/travis/deschler/django-modeltranslation/master.png?style=flat-square :target: https://travis-ci.org/deschler/django-modeltranslation -.. image:: http://img.shields.io/coveralls/deschler/django-modeltranslation.png?style=flat +.. image:: http://img.shields.io/coveralls/deschler/django-modeltranslation.png?style=flat-square :target: https://coveralls.io/r/deschler/django-modeltranslation -.. image:: https://pypip.in/v/django-modeltranslation/badge.png?style=flat +.. image:: https://img.shields.io/pypi/v/django-modeltranslation.svg?style=flat-square :target: https://pypi.python.org/pypi/django-modeltranslation/ :alt: Latest PyPI version -.. image:: https://pypip.in/py_versions/django-modeltranslation/badge.png?style=flat +.. image:: https://img.shields.io/pypi/pyversions/django-modeltranslation.svg?style=flat-square :target: https://pypi.python.org/pypi/django-modeltranslation/ :alt: Supported Python versions -.. image:: https://pypip.in/d/django-modeltranslation/badge.png?style=flat +.. image:: https://img.shields.io/pypi/dm/django-modeltranslation.svg?style=flat-square :target: https://pypi.python.org/pypi/django-modeltranslation/ :alt: Number of PyPI downloads From a6511bca83a2ec64d89866f70d5562b6ef311ebd Mon Sep 17 00:00:00 2001 From: Lukas Lundgren Date: Wed, 1 Jul 2015 11:16:42 +0200 Subject: [PATCH 112/170] Handle annotation fields when using values_list --- modeltranslation/manager.py | 15 +++++++++++---- modeltranslation/tests/tests.py | 10 +++++++++- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/modeltranslation/manager.py b/modeltranslation/manager.py index f1c3fb0b..e36df4a1 100644 --- a/modeltranslation/manager.py +++ b/modeltranslation/manager.py @@ -445,14 +445,21 @@ def _clone(self, klass=None, setup=False, **kwargs): class FallbackValuesListQuerySet(FallbackValuesQuerySet): def iterator(self): + fields = self.original_fields + if hasattr(self, 'aggregate_names'): + # Django <1.8 + fields += tuple(self.aggregate_names) + if hasattr(self, 'annotation_names'): + # Django >=1.8 + fields += tuple(self.annotation_names) for row in super(FallbackValuesListQuerySet, self).iterator(): - if self.flat and len(self.original_fields) == 1: - yield row[self.original_fields[0]] + if self.flat and len(fields) == 1: + yield row[fields[0]] else: - yield tuple(row[f] for f in self.original_fields) + yield tuple(row[f] for f in fields) def _setup_query(self): - self.original_fields = self._fields + self.original_fields = tuple(self._fields) super(FallbackValuesListQuerySet, self)._setup_query() def _clone(self, *args, **kwargs): diff --git a/modeltranslation/tests/tests.py b/modeltranslation/tests/tests.py index 43c7bab6..f2930a12 100644 --- a/modeltranslation/tests/tests.py +++ b/modeltranslation/tests/tests.py @@ -14,7 +14,7 @@ from django.core.files.storage import default_storage from django.core.management import call_command from django.db import IntegrityError -from django.db.models import Q, F +from django.db.models import Q, F, Count from django.test import TestCase, TransactionTestCase from django.test.utils import override_settings from django.utils import six @@ -2551,6 +2551,14 @@ def test_values(self): 'description': None, 'description_en': None, 'description_de': None}, ]) + def test_values_list_annotation(self): + models.TestModel(title='foo').save() + models.TestModel(title='foo').save() + self.assertEqual( + list(models.TestModel.objects.all().values_list('title').annotate(Count('id'))), + [('foo', 2)] + ) + def test_custom_manager(self): """Test if user-defined manager is still working""" n = models.CustomManagerTestModel(title='') From c48a6d90cfba1028587fc1bdf43f93b7abbcc783 Mon Sep 17 00:00:00 2001 From: Dirk Eschler Date: Fri, 3 Jul 2015 10:37:14 +0200 Subject: [PATCH 113/170] Prepared 0.10.0 release. --- AUTHORS.rst | 3 +++ CHANGELOG.txt | 14 ++++++++++++++ PKG-INFO | 2 +- docs/modeltranslation/conf.py | 4 ++-- docs/modeltranslation/registration.rst | 4 +++- modeltranslation/__init__.py | 2 +- 6 files changed, 24 insertions(+), 5 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index e7f4c55c..f232ee90 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -39,6 +39,9 @@ Contributors * Mathias Ettinger * Daniel Loeb * Stephen McDonald +* Lukas Lundgren +* zenoamaro +* oliphunt * And many more ... (if you miss your name here, please let us know!) .. _django-linguo: https://github.com/zmathew/django-linguo diff --git a/CHANGELOG.txt b/CHANGELOG.txt index e387b2a6..a148df2b 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,17 @@ +v0.10.0 +======= +Date: 2015-07-03 + + ADDED: CSS support for bi-directional languages to TranslationAdmin + using mt-bidi class. + (resolves issue #317, thanks oliphunt) + ADDED: A decorator to handle registration of models. + (resolves issue #318, thanks zenoamaro) + + FIXED: Handled annotation fields when using values_list. + (resolves issue #321, thanks Lukas Lundgren) + + v0.9.1 ====== Date: 2015-05-14 diff --git a/PKG-INFO b/PKG-INFO index dcec7e6e..accdf82a 100644 --- a/PKG-INFO +++ b/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 1.0 Name: django-modeltranslation -Version: 0.9.1 +Version: 0.10.0 Summary: Translates Django models using a registration approach. Home-page: https://github.com/deschler/django-modeltranslation Author: Peter Eschler, diff --git a/docs/modeltranslation/conf.py b/docs/modeltranslation/conf.py index a9feb535..d4c63429 100644 --- a/docs/modeltranslation/conf.py +++ b/docs/modeltranslation/conf.py @@ -73,7 +73,7 @@ # General information about the project. project = u'django-modeltranslation' -copyright = u'2009-2014, Peter Eschler, Dirk Eschler, Jacek Tomaszewski' +copyright = u'2009-2015, Peter Eschler, Dirk Eschler, Jacek Tomaszewski' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -279,7 +279,7 @@ epub_title = u'django-modeltranslation' epub_author = u'Dirk Eschler' epub_publisher = u'Dirk Eschler' -epub_copyright = u'2009-2014, Peter Eschler, Dirk Eschler, Jacek Tomaszewski' +epub_copyright = u'2009-2015, Peter Eschler, Dirk Eschler, Jacek Tomaszewski' # The language of the text. It defaults to the language option # or en if the language is not set. diff --git a/docs/modeltranslation/registration.rst b/docs/modeltranslation/registration.rst index cc5831f8..4322f8b9 100644 --- a/docs/modeltranslation/registration.rst +++ b/docs/modeltranslation/registration.rst @@ -50,9 +50,11 @@ only imported. The ``NewsTranslationOptions`` derives from ``TranslationOptions`` and provides the ``fields`` attribute. Finally the model and its translation options are registered at the ``translator`` object. +.. versionadded:: 0.10 + If you prefer, ``register`` is also available as a decorator, much like the one Django introduced for its admin in version 1.7. Usage is similar to the -standard ``register``, just provide arguments as you normally would, except +standard ``register``, just provide arguments as you normally would, except the options class which will be the decorated one:: from modeltranslation.translator import register, TranslationOptions diff --git a/modeltranslation/__init__.py b/modeltranslation/__init__.py index d18b51ed..a4b04b2e 100644 --- a/modeltranslation/__init__.py +++ b/modeltranslation/__init__.py @@ -3,7 +3,7 @@ Version code adopted from Django development version. https://github.com/django/django """ -VERSION = (0, 9, 1, 'final', 0) +VERSION = (0, 10, 0, 'final', 0) default_app_config = 'modeltranslation.apps.ModeltranslationConfig' From a39dee2b789282469e80ec24ca0d2125a6f852b4 Mon Sep 17 00:00:00 2001 From: Venelin Stoykov Date: Thu, 3 Sep 2015 10:04:54 +0300 Subject: [PATCH 114/170] Fix FallbackValuesListQuerySet.iterator This will fix generated result when creating valueslist queryset with annotation and flat=True Fixes #324 --- modeltranslation/manager.py | 4 ++-- modeltranslation/tests/tests.py | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/modeltranslation/manager.py b/modeltranslation/manager.py index e36df4a1..8e9807eb 100644 --- a/modeltranslation/manager.py +++ b/modeltranslation/manager.py @@ -448,10 +448,10 @@ def iterator(self): fields = self.original_fields if hasattr(self, 'aggregate_names'): # Django <1.8 - fields += tuple(self.aggregate_names) + fields += tuple(f for f in self.aggregate_names if f not in fields) if hasattr(self, 'annotation_names'): # Django >=1.8 - fields += tuple(self.annotation_names) + fields += tuple(f for f in self.annotation_names if f not in fields) for row in super(FallbackValuesListQuerySet, self).iterator(): if self.flat and len(fields) == 1: yield row[fields[0]] diff --git a/modeltranslation/tests/tests.py b/modeltranslation/tests/tests.py index f2930a12..e0d35b52 100644 --- a/modeltranslation/tests/tests.py +++ b/modeltranslation/tests/tests.py @@ -1442,6 +1442,28 @@ def test_translated_models_time_instance(self): self.assertEqual(datetime.time(1, 2, 3), inst.time_de) self.assertEqual(datetime.time(23, 42, 0), inst.time_en) + def test_dates_queryset(self): + Model = models.OtherFieldsModel + + Model.objects.create(datetime=datetime.datetime(2015, 9, 2, 0, 0)) + Model.objects.create(datetime=datetime.datetime(2014, 8, 3, 0, 0)) + Model.objects.create(datetime=datetime.datetime(2013, 7, 4, 0, 0)) + + qs = Model.objects.dates('datetime', 'year', 'DESC') + + if django.VERSION[:2] < (1, 6): + self.assertEqual(list(qs), [ + datetime.datetime(2015, 1, 1, 0, 0), + datetime.datetime(2014, 1, 1, 0, 0), + datetime.datetime(2013, 1, 1, 0, 0) + ]) + else: + self.assertEqual(list(qs), [ + datetime.date(2015, 1, 1), + datetime.date(2014, 1, 1), + datetime.date(2013, 1, 1) + ]) + def test_descriptors(self): # Descriptor store ints in database and returns string of 'a' of that length inst = models.DescriptorModel() From 3df9d3e5d3f2fc092168789cbcab00c3942ae9fa Mon Sep 17 00:00:00 2001 From: Dirk Eschler Date: Fri, 4 Sep 2015 10:59:50 +0200 Subject: [PATCH 115/170] Prepared 0.10.1 release. --- AUTHORS.rst | 1 + CHANGELOG.txt | 8 ++++++++ PKG-INFO | 2 +- modeltranslation/__init__.py | 2 +- 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index f232ee90..6d475d5d 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -42,6 +42,7 @@ Contributors * Lukas Lundgren * zenoamaro * oliphunt +* Venelin Stoykov * And many more ... (if you miss your name here, please let us know!) .. _django-linguo: https://github.com/zmathew/django-linguo diff --git a/CHANGELOG.txt b/CHANGELOG.txt index a148df2b..a73d3b51 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,11 @@ +v0.10.1 +======= +Date: 2015-09-04 + + FIXED: FallbackValuesListQuerySet.iterator which broke ORM datetimes + (resolves issue #324, thanks Venelin Stoykov) + + v0.10.0 ======= Date: 2015-07-03 diff --git a/PKG-INFO b/PKG-INFO index accdf82a..f2ea36df 100644 --- a/PKG-INFO +++ b/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 1.0 Name: django-modeltranslation -Version: 0.10.0 +Version: 0.10.1 Summary: Translates Django models using a registration approach. Home-page: https://github.com/deschler/django-modeltranslation Author: Peter Eschler, diff --git a/modeltranslation/__init__.py b/modeltranslation/__init__.py index a4b04b2e..b49d329f 100644 --- a/modeltranslation/__init__.py +++ b/modeltranslation/__init__.py @@ -3,7 +3,7 @@ Version code adopted from Django development version. https://github.com/django/django """ -VERSION = (0, 10, 0, 'final', 0) +VERSION = (0, 10, 1, 'final', 0) default_app_config = 'modeltranslation.apps.ModeltranslationConfig' From 84ff47d7af45412e89d3731890688fbdbd4b837a Mon Sep 17 00:00:00 2001 From: Stratos Moros Date: Wed, 21 Oct 2015 12:22:29 +0300 Subject: [PATCH 116/170] fix proxy model inheritance for django >=1.8 --- modeltranslation/translator.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modeltranslation/translator.py b/modeltranslation/translator.py index 2f480ce1..73a14878 100644 --- a/modeltranslation/translator.py +++ b/modeltranslation/translator.py @@ -304,6 +304,9 @@ def delete_cache_fields(model): except AttributeError: pass + if hasattr(model._meta, '_expire_cache'): + model._meta._expire_cache() + def populate_translation_fields(sender, kwargs): """ From 1986e1ae15a2f6f2231e86123d6f152a17ea2f48 Mon Sep 17 00:00:00 2001 From: Dirk Eschler Date: Tue, 27 Oct 2015 10:35:32 +0100 Subject: [PATCH 117/170] Prepared 0.10.2 release. --- AUTHORS.rst | 1 + CHANGELOG.txt | 8 ++++++++ PKG-INFO | 2 +- modeltranslation/__init__.py | 2 +- 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 6d475d5d..f12e1916 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -43,6 +43,7 @@ Contributors * zenoamaro * oliphunt * Venelin Stoykov +* Stratos Moros * And many more ... (if you miss your name here, please let us know!) .. _django-linguo: https://github.com/zmathew/django-linguo diff --git a/CHANGELOG.txt b/CHANGELOG.txt index a73d3b51..44578c8e 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,11 @@ +v0.10.2 +======= +Date: 2015-10-27 + + FIXED: Proxy model inheritance for Django >=1.8 + (resolves issues #304, thanks Stratos Moros) + + v0.10.1 ======= Date: 2015-09-04 diff --git a/PKG-INFO b/PKG-INFO index f2ea36df..7f3c1305 100644 --- a/PKG-INFO +++ b/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 1.0 Name: django-modeltranslation -Version: 0.10.1 +Version: 0.10.2 Summary: Translates Django models using a registration approach. Home-page: https://github.com/deschler/django-modeltranslation Author: Peter Eschler, diff --git a/modeltranslation/__init__.py b/modeltranslation/__init__.py index b49d329f..83b17d96 100644 --- a/modeltranslation/__init__.py +++ b/modeltranslation/__init__.py @@ -3,7 +3,7 @@ Version code adopted from Django development version. https://github.com/django/django """ -VERSION = (0, 10, 1, 'final', 0) +VERSION = (0, 10, 2, 'final', 0) default_app_config = 'modeltranslation.apps.ModeltranslationConfig' From 8320fc8a5a818c15a789ed10d89da59896dbb367 Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Sun, 6 Dec 2015 18:48:17 +0100 Subject: [PATCH 118/170] Add Django 1.9 support (ref #349). --- .travis.yml | 44 ++++ modeltranslation/fields.py | 20 +- modeltranslation/forms.py | 9 +- .../commands/sync_translation_fields.py | 8 +- .../commands/update_translation_fields.py | 6 +- modeltranslation/manager.py | 208 ++++++++++++------ modeltranslation/tests/settings.py | 2 + modeltranslation/tests/tests.py | 36 ++- modeltranslation/translator.py | 22 +- tox.ini | 35 +++ 10 files changed, 300 insertions(+), 90 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1d627a06..97e6e261 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,7 @@ python: - "3.2" - "3.3" - "3.4" + - "3.5" env: - DJANGO=1.4 DB=sqlite - DJANGO=1.4 DB=postgres @@ -21,6 +22,9 @@ env: - DJANGO=1.8 DB=sqlite - DJANGO=1.8 DB=postgres - DJANGO=1.8 DB=mysql + - DJANGO=1.9 DB=sqlite + - DJANGO=1.9 DB=postgres + - DJANGO=1.9 DB=mysql matrix: exclude: - python: "3.2" @@ -41,6 +45,12 @@ matrix: env: DJANGO=1.4 DB=postgres - python: "3.4" env: DJANGO=1.4 DB=mysql + - python: "3.5" + env: DJANGO=1.4 DB=sqlite + - python: "3.5" + env: DJANGO=1.4 DB=postgres + - python: "3.5" + env: DJANGO=1.4 DB=mysql - python: "2.6" env: DJANGO=1.7 DB=sqlite @@ -54,6 +64,24 @@ matrix: env: DJANGO=1.8 DB=postgres - python: "2.6" env: DJANGO=1.8 DB=mysql + - python: "2.6" + env: DJANGO=1.9 DB=sqlite + - python: "2.6" + env: DJANGO=1.9 DB=postgres + - python: "2.6" + env: DJANGO=1.9 DB=mysql + - python: "3.2" + env: DJANGO=1.9 DB=sqlite + - python: "3.2" + env: DJANGO=1.9 DB=postgres + - python: "3.2" + env: DJANGO=1.9 DB=mysql + - python: "3.3" + env: DJANGO=1.9 DB=sqlite + - python: "3.3" + env: DJANGO=1.9 DB=postgres + - python: "3.3" + env: DJANGO=1.9 DB=mysql - python: "3.2" env: DJANGO=1.5 DB=mysql @@ -61,24 +89,40 @@ matrix: env: DJANGO=1.5 DB=mysql - python: "3.4" env: DJANGO=1.5 DB=mysql + - python: "3.5" + env: DJANGO=1.5 DB=mysql - python: "3.2" env: DJANGO=1.6 DB=mysql - python: "3.3" env: DJANGO=1.6 DB=mysql - python: "3.4" env: DJANGO=1.6 DB=mysql + - python: "3.5" + env: DJANGO=1.6 DB=mysql - python: "3.2" env: DJANGO=1.7 DB=mysql - python: "3.3" env: DJANGO=1.7 DB=mysql - python: "3.4" env: DJANGO=1.7 DB=mysql + - python: "3.5" + env: DJANGO=1.7 DB=mysql - python: "3.2" env: DJANGO=1.8 DB=mysql - python: "3.3" env: DJANGO=1.8 DB=mysql - python: "3.4" env: DJANGO=1.8 DB=mysql + - python: "3.5" + env: DJANGO=1.8 DB=mysql + - python: "3.2" + env: DJANGO=1.9 DB=mysql + - python: "3.3" + env: DJANGO=1.9 DB=mysql + - python: "3.4" + env: DJANGO=1.9 DB=mysql + - python: "3.5" + env: DJANGO=1.9 DB=mysql before_install: - pip install -q flake8 --use-mirrors - PYFLAKES_NODOCTEST=1 flake8 modeltranslation diff --git a/modeltranslation/fields.py b/modeltranslation/fields.py index 9885c0d8..7f67491c 100644 --- a/modeltranslation/fields.py +++ b/modeltranslation/fields.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from django import VERSION from django import forms from django.core.exceptions import ImproperlyConfigured from django.db.models import fields @@ -33,6 +34,8 @@ # Above implies also OneToOneField ) +NEW_RELATED_API = VERSION >= (1, 9) + class NONE: """ @@ -150,7 +153,7 @@ def __init__(self, translated_field, language, empty_value, *args, **kwargs): self.verbose_name = build_localized_verbose_name(translated_field.verbose_name, language) # ForeignKey support - rewrite related_name - if self.rel and self.related and not self.rel.is_hidden(): + if not NEW_RELATED_API and self.rel and self.related and not self.rel.is_hidden(): import copy current = self.related.get_accessor_name() self.rel = copy.copy(self.rel) # Since fields cannot share the same rel object. @@ -166,6 +169,21 @@ def __init__(self, translated_field, language, empty_value, *args, **kwargs): self.rel.field = self # Django 1.6 if hasattr(self.rel.to._meta, '_related_objects_cache'): del self.rel.to._meta._related_objects_cache + elif NEW_RELATED_API and self.remote_field and not self.remote_field.is_hidden(): + import copy + current = self.remote_field.get_accessor_name() + # Since fields cannot share the same rel object: + self.remote_field = copy.copy(self.remote_field) + + if self.remote_field.related_name is None: + # For implicit related_name use different query field name + loc_related_query_name = build_localized_fieldname( + self.related_query_name(), self.language) + self.related_query_name = lambda: loc_related_query_name + self.remote_field.related_name = build_localized_fieldname(current, self.language) + self.remote_field.field = self # Django 1.6 + if hasattr(self.remote_field.to._meta, '_related_objects_cache'): + del self.remote_field.to._meta._related_objects_cache # Django 1.5 changed definition of __hash__ for fields to be fine with hash requirements. # It spoiled our machinery, since TranslationField has the same creation_counter as its diff --git a/modeltranslation/forms.py b/modeltranslation/forms.py index f74c2c99..9e529be2 100644 --- a/modeltranslation/forms.py +++ b/modeltranslation/forms.py @@ -35,6 +35,13 @@ def to_python(self, value): # Django 1.6 def _has_changed(self, initial, data): + return self.has_changed(initial, data) + + def has_changed(self, initial, data): if (initial is None and data is not None) or (initial is not None and data is None): return True - return super(NullableField, self)._has_changed(initial, data) + obj = super(NullableField, self) + if hasattr(obj, 'has_changed'): + return obj.has_changed(initial, data) + else: # Django < 1.9 compat + return obj._has_changed(initial, data) diff --git a/modeltranslation/management/commands/sync_translation_fields.py b/modeltranslation/management/commands/sync_translation_fields.py index ac89948b..9bd6147d 100644 --- a/modeltranslation/management/commands/sync_translation_fields.py +++ b/modeltranslation/management/commands/sync_translation_fields.py @@ -11,7 +11,7 @@ """ from optparse import make_option import django -from django.core.management.base import NoArgsCommand +from django.core.management.base import BaseCommand from django.core.management.color import no_style from django.db import connection, transaction from django.utils.six import moves @@ -46,17 +46,17 @@ def print_missing_langs(missing_langs, field_name, model_name): field_name, model_name, ", ".join(missing_langs))) -class Command(NoArgsCommand): +class Command(BaseCommand): help = ('Detect new translatable fields or new available languages and' ' sync database structure. Does not remove columns of removed' ' languages or undeclared fields.') - option_list = NoArgsCommand.option_list + ( + option_list = BaseCommand.option_list + ( make_option('--noinput', action='store_false', dest='interactive', default=True, help='Do NOT prompt the user for input of any kind.'), ) - def handle_noargs(self, **options): + def handle(self, *args, **options): """ Command execution. """ diff --git a/modeltranslation/management/commands/update_translation_fields.py b/modeltranslation/management/commands/update_translation_fields.py index 76bfc68f..98e1bfbe 100644 --- a/modeltranslation/management/commands/update_translation_fields.py +++ b/modeltranslation/management/commands/update_translation_fields.py @@ -1,17 +1,17 @@ # -*- coding: utf-8 -*- from django.db.models import F, Q -from django.core.management.base import NoArgsCommand +from django.core.management.base import BaseCommand from modeltranslation.settings import DEFAULT_LANGUAGE from modeltranslation.translator import translator from modeltranslation.utils import build_localized_fieldname -class Command(NoArgsCommand): +class Command(BaseCommand): help = ('Updates empty values of default translation fields using' ' values from original fields (in all translated models).') - def handle_noargs(self, **options): + def handle(self, *args, **options): verbosity = int(options['verbosity']) if verbosity > 0: self.stdout.write("Using default language: %s\n" % DEFAULT_LANGUAGE) diff --git a/modeltranslation/manager.py b/modeltranslation/manager.py index 8e9807eb..7cf2b180 100644 --- a/modeltranslation/manager.py +++ b/modeltranslation/manager.py @@ -16,13 +16,20 @@ except ImportError: NEW_META_API = True -from django.db.models.sql.where import Constraint +try: + from django.db.models.query import ValuesQuerySet + from django.db.models.sql.where import Constraint + NEW_RELATED_API = False +except ImportError: + from django.db.models.query import ValuesIterable + NEW_RELATED_API = True # Django 1.9 + from django.utils.six import moves from django.utils.tree import Node try: from django.db.models.lookups import Lookup from django.db.models.sql.datastructures import Col - NEW_LOOKUPS = True + NEW_LOOKUPS = True # Django 1.7, 1.8 except ImportError: NEW_LOOKUPS = False @@ -193,15 +200,27 @@ def __reduce__(self): return multilingual_queryset_factory, (self.__class__.__bases__[0],), self.__getstate__() # This method was not present in django-linguo - def _clone(self, klass=None, *args, **kwargs): - if klass is not None and not issubclass(klass, MultilingualQuerySet): - class NewClass(klass, MultilingualQuerySet): - pass - NewClass.__name__ = 'Multilingual%s' % klass.__name__ - klass = NewClass - kwargs.setdefault('_rewrite', self._rewrite) - kwargs.setdefault('_populate', self._populate) - return super(MultilingualQuerySet, self)._clone(klass, *args, **kwargs) + if NEW_RELATED_API: + def _clone(self, klass=None, **kwargs): + kwargs.setdefault('_rewrite', self._rewrite) + kwargs.setdefault('_populate', self._populate) + if hasattr(self, 'translation_fields'): + kwargs.setdefault('translation_fields', self.translation_fields) + if hasattr(self, 'fields_to_del'): + kwargs.setdefault('fields_to_del', self.fields_to_del) + if hasattr(self, 'original_fields'): + kwargs.setdefault('original_fields', self.original_fields) + return super(MultilingualQuerySet, self)._clone(**kwargs) + else: + def _clone(self, klass=None, *args, **kwargs): + if klass is not None and not issubclass(klass, MultilingualQuerySet): + class NewClass(klass, MultilingualQuerySet): + pass + NewClass.__name__ = 'Multilingual%s' % klass.__name__ + klass = NewClass + kwargs.setdefault('_rewrite', self._rewrite) + kwargs.setdefault('_populate', self._populate) + return super(MultilingualQuerySet, self)._clone(klass, *args, **kwargs) # This method was not present in django-linguo def rewrite(self, mode=True): @@ -220,7 +239,8 @@ def _rewrite_applied_operations(self): Useful when converting any QuerySet into MultilingualQuerySet. """ self._rewrite_where(self.query.where) - self._rewrite_where(self.query.having) + if not NEW_RELATED_API: + self._rewrite_where(self.query.having) self._rewrite_order() self._rewrite_select_related() @@ -318,7 +338,9 @@ def _filter_or_exclude(self, negate, *args, **kwargs): return super(MultilingualQuerySet, self)._filter_or_exclude(negate, *args, **kwargs) def _get_original_fields(self): - return [f.attname for f in self.model._meta.fields if not isinstance(f, TranslationField)] + source = (self.model._meta.concrete_fields if hasattr(self.model._meta, 'concrete_fields') + else self.model._meta.fields) + return [f.attname for f in source if not isinstance(f, TranslationField)] def order_by(self, *field_names): """ @@ -380,6 +402,16 @@ def only(self, *fields): def raw_values(self, *fields): return super(MultilingualQuerySet, self).values(*fields) + def _values(self, *original, **kwargs): + if not kwargs.get('prepare', False): + return super(MultilingualQuerySet, self)._values(*original) + new_fields, translation_fields = append_fallback(self.model, original) + clone = super(MultilingualQuerySet, self)._values(*list(new_fields)) + clone.original_fields = tuple(original) + clone.translation_fields = translation_fields + clone.fields_to_del = new_fields - set(original) + return clone + # This method was not present in django-linguo def values(self, *fields): if not self._rewrite: @@ -387,7 +419,12 @@ def values(self, *fields): if not fields: # Emulate original queryset behaviour: get all fields that are not translation fields fields = self._get_original_fields() - return self._clone(klass=FallbackValuesQuerySet, setup=True, _fields=fields) + if NEW_RELATED_API: + clone = self._values(*fields, prepare=True) + clone._iterable_class = FallbackValuesIterable + return clone + else: + return self._clone(klass=FallbackValuesQuerySet, setup=True, _fields=fields) # This method was not present in django-linguo def values_list(self, *fields, **kwargs): @@ -402,7 +439,14 @@ def values_list(self, *fields, **kwargs): if not fields: # Emulate original queryset behaviour: get all fields that are not translation fields fields = self._get_original_fields() - return self._clone(klass=FallbackValuesListQuerySet, setup=True, flat=flat, _fields=fields) + if NEW_RELATED_API: + clone = self._values(*fields, prepare=True) + clone._iterable_class = (FallbackFlatValuesListIterable if flat + else FallbackValuesListIterable) + return clone + else: + return self._clone(klass=FallbackValuesListQuerySet, setup=True, flat=flat, + _fields=fields) # This method was not present in django-linguo def dates(self, field_name, *args, **kwargs): @@ -412,63 +456,91 @@ def dates(self, field_name, *args, **kwargs): return super(MultilingualQuerySet, self).dates(new_key, *args, **kwargs) -class FallbackValuesQuerySet(models.query.ValuesQuerySet, MultilingualQuerySet): - def _setup_query(self): - original = self._fields - new_fields, self.translation_fields = append_fallback(self.model, original) - self._fields = list(new_fields) - self.fields_to_del = new_fields - set(original) - super(FallbackValuesQuerySet, self)._setup_query() - - class X(object): - # This stupid class is needed as object use __slots__ and has no __dict__. - pass +if NEW_RELATED_API: + class FallbackValuesIterable(ValuesIterable): + class X(object): + # This stupid class is needed as object use __slots__ and has no __dict__. + pass - def iterator(self): - instance = self.X() - for row in super(FallbackValuesQuerySet, self).iterator(): - instance.__dict__.update(row) - for key in self.translation_fields: - row[key] = getattr(self.model, key).__get__(instance, None) - for key in self.fields_to_del: - del row[key] - yield row - - def _clone(self, klass=None, setup=False, **kwargs): - c = super(FallbackValuesQuerySet, self)._clone(klass, **kwargs) - c.fields_to_del = self.fields_to_del - c.translation_fields = self.translation_fields - if setup and hasattr(c, '_setup_query'): - c._setup_query() - return c - - -class FallbackValuesListQuerySet(FallbackValuesQuerySet): - def iterator(self): - fields = self.original_fields - if hasattr(self, 'aggregate_names'): - # Django <1.8 - fields += tuple(f for f in self.aggregate_names if f not in fields) - if hasattr(self, 'annotation_names'): - # Django >=1.8 - fields += tuple(f for f in self.annotation_names if f not in fields) - for row in super(FallbackValuesListQuerySet, self).iterator(): - if self.flat and len(fields) == 1: - yield row[fields[0]] - else: + def __iter__(self): + instance = self.X() + for row in super(FallbackValuesIterable, self).__iter__(): + instance.__dict__.update(row) + for key in self.queryset.translation_fields: + row[key] = getattr(self.queryset.model, key).__get__(instance, None) + for key in self.queryset.fields_to_del: + del row[key] + yield row + + class FallbackValuesListIterable(FallbackValuesIterable): + def __iter__(self): + fields = self.queryset.original_fields + fields += tuple(f for f in self.queryset.query.annotation_select if f not in fields) + for row in super(FallbackValuesListIterable, self).__iter__(): yield tuple(row[f] for f in fields) - def _setup_query(self): - self.original_fields = tuple(self._fields) - super(FallbackValuesListQuerySet, self)._setup_query() + class FallbackFlatValuesListIterable(FallbackValuesListIterable): + def __iter__(self): + for row in super(FallbackFlatValuesListIterable, self).__iter__(): + yield row[0] + +else: + class FallbackValuesQuerySet(ValuesQuerySet, MultilingualQuerySet): + def _setup_query(self): + original = self._fields + new_fields, self.translation_fields = append_fallback(self.model, original) + self._fields = list(new_fields) + self.fields_to_del = new_fields - set(original) + super(FallbackValuesQuerySet, self)._setup_query() + + class X(object): + # This stupid class is needed as object use __slots__ and has no __dict__. + pass - def _clone(self, *args, **kwargs): - clone = super(FallbackValuesListQuerySet, self)._clone(*args, **kwargs) - clone.original_fields = self.original_fields - if not hasattr(clone, "flat"): - # Only assign flat if the clone didn't already get it from kwargs - clone.flat = self.flat - return clone + def iterator(self): + instance = self.X() + for row in super(FallbackValuesQuerySet, self).iterator(): + instance.__dict__.update(row) + for key in self.translation_fields: + row[key] = getattr(self.model, key).__get__(instance, None) + for key in self.fields_to_del: + del row[key] + yield row + + def _clone(self, klass=None, setup=False, **kwargs): + c = super(FallbackValuesQuerySet, self)._clone(klass, **kwargs) + c.fields_to_del = self.fields_to_del + c.translation_fields = self.translation_fields + if setup and hasattr(c, '_setup_query'): + c._setup_query() + return c + + class FallbackValuesListQuerySet(FallbackValuesQuerySet): + def iterator(self): + fields = self.original_fields + if hasattr(self, 'aggregate_names'): + # Django <1.8 + fields += tuple(f for f in self.aggregate_names if f not in fields) + if hasattr(self, 'annotation_names'): + # Django >=1.8 + fields += tuple(f for f in self.annotation_names if f not in fields) + for row in super(FallbackValuesListQuerySet, self).iterator(): + if self.flat and len(fields) == 1: + yield row[fields[0]] + else: + yield tuple(row[f] for f in fields) + + def _setup_query(self): + self.original_fields = tuple(self._fields) + super(FallbackValuesListQuerySet, self)._setup_query() + + def _clone(self, *args, **kwargs): + clone = super(FallbackValuesListQuerySet, self)._clone(*args, **kwargs) + clone.original_fields = self.original_fields + if not hasattr(clone, "flat"): + # Only assign flat if the clone didn't already get it from kwargs + clone.flat = self.flat + return clone def get_queryset(obj): diff --git a/modeltranslation/tests/settings.py b/modeltranslation/tests/settings.py index f66c68fc..a3204e7f 100644 --- a/modeltranslation/tests/settings.py +++ b/modeltranslation/tests/settings.py @@ -20,3 +20,5 @@ MODELTRANSLATION_AUTO_POPULATE = False MODELTRANSLATION_FALLBACK_LANGUAGES = () + +ROOT_URLCONF = 'modeltranslation.tests.urls' diff --git a/modeltranslation/tests/tests.py b/modeltranslation/tests/tests.py index e0d35b52..d275efcb 100644 --- a/modeltranslation/tests/tests.py +++ b/modeltranslation/tests/tests.py @@ -70,9 +70,25 @@ def __exit__(self, _type, value, traceback): return False +def get_field_names(model): + if django.VERSION < (1, 9): + return model._meta.get_all_field_names() + names = set() + fields = model._meta.get_fields() + for field in fields: + if field.is_relation and field.many_to_one and field.related_model is None: + continue + if field.model != model and field.model._meta.concrete_model == model._meta.concrete_model: + continue + + names.add(field.name) + if hasattr(field, 'attname'): + names.add(field.attname) + return names + + @override_settings(**TEST_SETTINGS) class ModeltranslationTransactionTestBase(TransactionTestCase): - urls = 'modeltranslation.tests.urls' cache = django_apps if NEW_APP_CACHE else AppCache() synced = False @@ -118,7 +134,7 @@ def setUpClass(cls): # 5. Syncdb (``migrate=False`` in case of south) from django.db import connections, DEFAULT_DB_ALIAS cmd = 'syncdb' if django.VERSION < (1, 8) else 'migrate' - call_command(cmd, verbosity=0, migrate=False, interactive=False, + call_command(cmd, verbosity=0, migrate=False, interactive=False, run_syncdb=True, database=connections[DEFAULT_DB_ALIAS].alias, load_initial_data=False) # A rather dirty trick to import models into module namespace, but not before @@ -846,7 +862,7 @@ def test_reverse_relations(self): # Check that the reverse accessors are created on the model: # Explicit related_name - testmodel_fields = models.TestModel._meta.get_all_field_names() + testmodel_fields = get_field_names(models.TestModel) testmodel_methods = dir(models.TestModel) self.assertIn('test_fks', testmodel_fields) self.assertIn('test_fks_de', testmodel_fields) @@ -1026,7 +1042,7 @@ def test_reverse_relations(self): # Check that the reverse accessors are created on the model: # Explicit related_name - testmodel_fields = models.TestModel._meta.get_all_field_names() + testmodel_fields = get_field_names(models.TestModel) testmodel_methods = dir(models.TestModel) self.assertIn('test_o2o', testmodel_fields) self.assertIn('test_o2o_de', testmodel_fields) @@ -1801,7 +1817,7 @@ def test_model_validation_email_field(self): class ModelInheritanceTest(ModeltranslationTestBase): """Tests for inheritance support in modeltranslation.""" def test_abstract_inheritance(self): - field_names_b = models.AbstractModelB._meta.get_all_field_names() + field_names_b = get_field_names(models.AbstractModelB) self.assertTrue('titlea' in field_names_b) self.assertTrue('titlea_de' in field_names_b) self.assertTrue('titlea_en' in field_names_b) @@ -1813,12 +1829,12 @@ def test_abstract_inheritance(self): self.assertFalse('titled_en' in field_names_b) def test_multitable_inheritance(self): - field_names_a = models.MultitableModelA._meta.get_all_field_names() + field_names_a = get_field_names(models.MultitableModelA) self.assertTrue('titlea' in field_names_a) self.assertTrue('titlea_de' in field_names_a) self.assertTrue('titlea_en' in field_names_a) - field_names_b = models.MultitableModelB._meta.get_all_field_names() + field_names_b = get_field_names(models.MultitableModelB) self.assertTrue('titlea' in field_names_b) self.assertTrue('titlea_de' in field_names_b) self.assertTrue('titlea_en' in field_names_b) @@ -1826,7 +1842,7 @@ def test_multitable_inheritance(self): self.assertTrue('titleb_de' in field_names_b) self.assertTrue('titleb_en' in field_names_b) - field_names_c = models.MultitableModelC._meta.get_all_field_names() + field_names_c = get_field_names(models.MultitableModelC) self.assertTrue('titlea' in field_names_c) self.assertTrue('titlea_de' in field_names_c) self.assertTrue('titlea_en' in field_names_c) @@ -1837,7 +1853,7 @@ def test_multitable_inheritance(self): self.assertTrue('titlec_de' in field_names_c) self.assertTrue('titlec_en' in field_names_c) - field_names_d = models.MultitableModelD._meta.get_all_field_names() + field_names_d = get_field_names(models.MultitableModelD) self.assertTrue('titlea' in field_names_d) self.assertTrue('titlea_de' in field_names_d) self.assertTrue('titlea_en' in field_names_d) @@ -1863,7 +1879,7 @@ def assertFields(model, fields): opts = translator.translator.get_options_for_model(model) self.assertEqual(set(opts.fields.keys()), set(fields)) # Inherited translation fields are available on the model. - model_fields = model._meta.get_all_field_names() + model_fields = get_field_names(model) for field in fields: for lang in mt_settings.AVAILABLE_LANGUAGES: translation_field = build_localized_fieldname(field, lang) diff --git a/modeltranslation/translator.py b/modeltranslation/translator.py index 73a14878..903db50d 100644 --- a/modeltranslation/translator.py +++ b/modeltranslation/translator.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from django import VERSION from django.utils.six import with_metaclass from django.core.exceptions import ImproperlyConfigured from django.db.models import Manager, ForeignKey, OneToOneField @@ -14,6 +15,9 @@ from modeltranslation.utils import build_localized_fieldname, parse_field +NEW_RELATED_API = VERSION >= (1, 9) + + class AlreadyRegistered(Exception): pass @@ -424,7 +428,10 @@ def register(self, model_or_iterable, opts_class=None, **options): add_translation_fields(model, opts) # Delete all fields cache for related model (parent and children) - for related_obj in model._meta.get_all_related_objects(): + related = ((f for f in model._meta.get_fields() if (f.one_to_many or f.one_to_one) + and f.auto_created) if NEW_RELATED_API + else model._meta.get_all_related_objects()) + for related_obj in related: delete_cache_fields(related_obj.model) # Set MultilingualManager @@ -465,7 +472,13 @@ def register(self, model_or_iterable, opts_class=None, **options): setattr(model, field.get_attname(), desc) # Set related field names on other model - if not field.rel.is_hidden(): + if NEW_RELATED_API and not field.remote_field.is_hidden(): + other_opts = self._get_options_for_model(field.remote_field.to) + other_opts.related = True + other_opts.related_fields.append(field.related_query_name()) + # Add manager in case of non-registered model + add_manager(field.remote_field.to) + elif not NEW_RELATED_API and not field.rel.is_hidden(): other_opts = self._get_options_for_model(field.rel.to) other_opts.related = True other_opts.related_fields.append(field.related_query_name()) @@ -473,7 +486,10 @@ def register(self, model_or_iterable, opts_class=None, **options): if isinstance(field, OneToOneField): # Fix translated_field caching for SingleRelatedObjectDescriptor - sro_descriptor = getattr(field.rel.to, field.related.get_accessor_name()) + sro_descriptor = ( + getattr(field.remote_field.to, field.remote_field.get_accessor_name()) + if NEW_RELATED_API + else getattr(field.rel.to, field.related.get_accessor_name())) patch_related_object_descriptor_caching(sro_descriptor) def unregister(self, model_or_iterable): diff --git a/tox.ini b/tox.ini index be9fa228..0bb2759b 100644 --- a/tox.ini +++ b/tox.ini @@ -5,6 +5,11 @@ exclude = .tox,docs/modeltranslation/conf.py [tox] distribute = False envlist = + py35-1.9.X, + py34-1.9.X, + py27-1.9.X, + py35-1.8.X, + py34-1.8.X, py33-1.8.X, py32-1.8.X, py27-1.8.X, @@ -29,6 +34,36 @@ commands = {envpython} runtests.py +[testenv:py35-1.9.X] +basepython = python3.5 +deps = + Django>=1.9,<1.10 + Pillow + +[testenv:py34-1.9.X] +basepython = python3.4 +deps = + Django>=1.9,<1.10 + Pillow + +[testenv:py27-1.9.X] +basepython = python2.7 +deps = + Django>=1.9,<1.10 + Pillow + +[testenv:py35-1.8.X] +basepython = python3.5 +deps = + Django>=1.8,<1.9 + Pillow + +[testenv:py34-1.8.X] +basepython = python3.4 +deps = + Django>=1.8,<1.9 + Pillow + [testenv:py33-1.8.X] basepython = python3.3 deps = From caab290048dcd0c34666f612f0e7c35e82b79e57 Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Mon, 7 Dec 2015 00:23:17 +0100 Subject: [PATCH 119/170] Fix travis for Python 3.5 adn 3.2; fix migrations for Django 1.9 (ref #349). --- .travis.yml | 19 +++++++++++++++++++ modeltranslation/fields.py | 6 ++++++ 2 files changed, 25 insertions(+) diff --git a/.travis.yml b/.travis.yml index 97e6e261..af5d6ef5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -82,6 +82,24 @@ matrix: env: DJANGO=1.9 DB=postgres - python: "3.3" env: DJANGO=1.9 DB=mysql + - python: "3.5" + env: DJANGO=1.5 DB=sqlite + - python: "3.5" + env: DJANGO=1.5 DB=postgres + - python: "3.5" + env: DJANGO=1.5 DB=mysql + - python: "3.5" + env: DJANGO=1.6 DB=sqlite + - python: "3.5" + env: DJANGO=1.6 DB=postgres + - python: "3.5" + env: DJANGO=1.6 DB=mysql + - python: "3.5" + env: DJANGO=1.7 DB=sqlite + - python: "3.5" + env: DJANGO=1.7 DB=postgres + - python: "3.5" + env: DJANGO=1.7 DB=mysql - python: "3.2" env: DJANGO=1.5 DB=mysql @@ -136,6 +154,7 @@ install: - IDJANGO=$(./travis.py $DJANGO) - pip install -q $IDJANGO - pip install -e . --use-mirrors + - if [[ $TRAVIS_PYTHON_VERSION == '3.2' ]]; then pip install 'coverage<4.0.0'; fi - pip install -q coveralls --use-mirrors script: - django-admin.py --version diff --git a/modeltranslation/fields.py b/modeltranslation/fields.py index 7f67491c..26a98e76 100644 --- a/modeltranslation/fields.py +++ b/modeltranslation/fields.py @@ -268,6 +268,12 @@ def deconstruct(self): kwargs['db_column'] = self.db_column return six.text_type(self.name), path, args, kwargs + def clone(self): + from django.utils.module_loading import import_string + name, path, args, kwargs = self.deconstruct() + cls = import_string(path) + return cls(*args, **kwargs) + def south_field_triple(self): """ Returns a suitable description of this field for South. From 27eccfda67074a1b55fb564acda946121030f089 Mon Sep 17 00:00:00 2001 From: Dirk Eschler Date: Mon, 7 Dec 2015 10:43:07 +0100 Subject: [PATCH 120/170] Prepared 0.11rc1 release. --- CHANGELOG.txt | 8 ++++++++ PKG-INFO | 2 +- modeltranslation/__init__.py | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 44578c8e..8201e496 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,11 @@ +v0.11.0-rc1 +=========== +Date: 2015-12-07 + + ADDED: Support for Django 1.9 + (resolves issues #349, thanks Jacek Tomaszewski) + + v0.10.2 ======= Date: 2015-10-27 diff --git a/PKG-INFO b/PKG-INFO index 7f3c1305..a9e15a35 100644 --- a/PKG-INFO +++ b/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 1.0 Name: django-modeltranslation -Version: 0.10.2 +Version: 0.11-rc1 Summary: Translates Django models using a registration approach. Home-page: https://github.com/deschler/django-modeltranslation Author: Peter Eschler, diff --git a/modeltranslation/__init__.py b/modeltranslation/__init__.py index 83b17d96..75b1e1a7 100644 --- a/modeltranslation/__init__.py +++ b/modeltranslation/__init__.py @@ -3,7 +3,7 @@ Version code adopted from Django development version. https://github.com/django/django """ -VERSION = (0, 10, 2, 'final', 0) +VERSION = (0, 11, 0, 'rc', 1) default_app_config = 'modeltranslation.apps.ModeltranslationConfig' From a4b9f42d52dff17dbee2b7248fdd0bc618a802c8 Mon Sep 17 00:00:00 2001 From: Dirk Eschler Date: Mon, 7 Dec 2015 10:47:32 +0100 Subject: [PATCH 121/170] Changed version mapping so that rc is kept. --- modeltranslation/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modeltranslation/__init__.py b/modeltranslation/__init__.py index 75b1e1a7..779085e6 100644 --- a/modeltranslation/__init__.py +++ b/modeltranslation/__init__.py @@ -32,7 +32,7 @@ def get_version(version=None): sub = '.dev%s' % git_changeset elif version[3] != 'final': - mapping = {'alpha': 'a', 'beta': 'b', 'rc': 'c'} + mapping = {'alpha': 'a', 'beta': 'b', 'rc': 'rc'} sub = mapping[version[3]] + str(version[4]) return str(main + sub) From e50b0ce7a463fe427d1ec453a809e52fd90ac85c Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Sun, 13 Dec 2015 15:47:01 +0100 Subject: [PATCH 122/170] Try to fix custom manager in migrations (ref #330, #339, #350). --- modeltranslation/tests/tests.py | 37 ++++++++++++++++++++++++--- modeltranslation/tests/translation.py | 11 ++++++++ modeltranslation/translator.py | 12 +++++++++ runtests.py | 1 + 4 files changed, 58 insertions(+), 3 deletions(-) diff --git a/modeltranslation/tests/tests.py b/modeltranslation/tests/tests.py index d275efcb..685ecae7 100644 --- a/modeltranslation/tests/tests.py +++ b/modeltranslation/tests/tests.py @@ -27,6 +27,16 @@ from django.db.models.loading import AppCache NEW_APP_CACHE = False +try: + from unittest import skipUnless +except ImportError: + # Dummy replacement for Python 2.6 + def skipUnless(condition, reason): + if not condition: + def decorator(test_item): + return lambda s: 42 + return decorator + return lambda x: x # identity from modeltranslation import admin, settings as mt_settings, translator from modeltranslation.forms import TranslationModelForm @@ -35,6 +45,8 @@ from modeltranslation.utils import (build_css_class, build_localized_fieldname, auto_populate, fallbacks) +MIGRATIONS = django.VERSION >= (1, 8) + models = translation = None # None of the following tests really depend on the content of the request, @@ -42,7 +54,7 @@ request = None # How many models are registered for tests. -TEST_MODELS = 29 +TEST_MODELS = 29 + (1 if MIGRATIONS else 0) class reload_override_settings(override_settings): @@ -131,9 +143,14 @@ def setUpClass(cls): from modeltranslation.models import handle_translation_registrations handle_translation_registrations() - # 5. Syncdb (``migrate=False`` in case of south) + # 5. makemigrations (``migrate=False`` in case of south) from django.db import connections, DEFAULT_DB_ALIAS - cmd = 'syncdb' if django.VERSION < (1, 8) else 'migrate' + if MIGRATIONS: + call_command('makemigrations', verbosity=2, interactive=False, + database=connections[DEFAULT_DB_ALIAS].alias) + + # 6. Syncdb (``migrate=False`` in case of south) + cmd = 'migrate' if MIGRATIONS else 'syncdb' call_command(cmd, verbosity=0, migrate=False, interactive=False, run_syncdb=True, database=connections[DEFAULT_DB_ALIAS].alias, load_initial_data=False) @@ -2624,6 +2641,20 @@ def test_custom_manager_custom_method_name(self): qs = models.CustomManagerTestModel.objects.custom_qs() self.assertIsInstance(qs, MultilingualQuerySet) + @skipUnless(MIGRATIONS, 'migrations not available') + def test_3rd_party_custom_manager(self): + from django.contrib.auth.models import Group, GroupManager + from modeltranslation.manager import MultilingualManager + testmodel_fields = get_field_names(Group) + self.assertIn('name', testmodel_fields) + self.assertIn('name_de', testmodel_fields) + self.assertIn('name_en', testmodel_fields) + self.assertIn('name_en', testmodel_fields) + + self.assertIsInstance(Group.objects, MultilingualManager) + self.assertIsInstance(Group.objects, GroupManager) + self.assertIn('get_by_natural_key', dir(Group.objects)) + def test_multilingual_queryset_pickling(self): import pickle from modeltranslation.manager import MultilingualQuerySet diff --git a/modeltranslation/tests/translation.py b/modeltranslation/tests/translation.py index 0753cc06..1dcef643 100644 --- a/modeltranslation/tests/translation.py +++ b/modeltranslation/tests/translation.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from django import VERSION from django.utils.translation import ugettext_lazy from modeltranslation.translator import translator, register, TranslationOptions @@ -203,3 +204,13 @@ class RequiredTranslationOptions(TranslationOptions): @register(DecoratedModel) class DecoratedTranslationOptions(TranslationOptions): fields = ('title',) + + +# ######### 3-rd party with custom manager + +if VERSION >= (1, 8): + from django.contrib.auth.models import Group + + @register(Group) + class GroupTranslationOptions(TranslationOptions): + fields = ('name',) diff --git a/modeltranslation/translator.py b/modeltranslation/translator.py index 903db50d..99b2de66 100644 --- a/modeltranslation/translator.py +++ b/modeltranslation/translator.py @@ -189,6 +189,18 @@ class NewMultilingualManager(MultilingualManager, manager.__class__, MultilingualQuerysetManager): use_for_related_fields = getattr( manager.__class__, "use_for_related_fields", not has_custom_queryset(manager)) + _old_module = manager.__module__ + _old_class = manager.__class__.__name__ + + def deconstruct(self): + return ( + False, # as_manager + '%s.%s' % (self._old_module, self._old_class), # manager_class + None, # qs_class + self._constructor_args[0], # args + self._constructor_args[1], # kwargs + ) + manager.__class__ = NewMultilingualManager for _, attname, cls in model._meta.concrete_managers + model._meta.abstract_managers: diff --git a/runtests.py b/runtests.py index 08e133a4..c6667e79 100755 --- a/runtests.py +++ b/runtests.py @@ -37,6 +37,7 @@ def runtests(): DATABASES=DATABASES, INSTALLED_APPS=( 'django.contrib.contenttypes', + 'django.contrib.auth', 'modeltranslation', ), ROOT_URLCONF=None, # tests override urlconf, but it still needs to be defined From 9ca0bc47b365f6a8d6a4d777b06e2ff7cc623baa Mon Sep 17 00:00:00 2001 From: Dirk Eschler Date: Tue, 15 Dec 2015 13:42:17 +0100 Subject: [PATCH 123/170] Prepared 0.11rc2 release. --- CHANGELOG.txt | 14 +++++++++++--- PKG-INFO | 2 +- modeltranslation/__init__.py | 2 +- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 8201e496..b495f2dc 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,9 +1,17 @@ -v0.11.0-rc1 -=========== +v0.11rc2 +======== +Date: 2015-12-15 + + FIXED: Custom manager in migrations. + (resolves issues #330, #339 and #350, thanks Jacek Tomaszewski) + + +v0.11rc1 +======== Date: 2015-12-07 ADDED: Support for Django 1.9 - (resolves issues #349, thanks Jacek Tomaszewski) + (resolves issue #349, thanks Jacek Tomaszewski) v0.10.2 diff --git a/PKG-INFO b/PKG-INFO index a9e15a35..2d63275e 100644 --- a/PKG-INFO +++ b/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 1.0 Name: django-modeltranslation -Version: 0.11-rc1 +Version: 0.11rc2 Summary: Translates Django models using a registration approach. Home-page: https://github.com/deschler/django-modeltranslation Author: Peter Eschler, diff --git a/modeltranslation/__init__.py b/modeltranslation/__init__.py index 779085e6..b578cff9 100644 --- a/modeltranslation/__init__.py +++ b/modeltranslation/__init__.py @@ -3,7 +3,7 @@ Version code adopted from Django development version. https://github.com/django/django """ -VERSION = (0, 11, 0, 'rc', 1) +VERSION = (0, 11, 0, 'rc', 2) default_app_config = 'modeltranslation.apps.ModeltranslationConfig' From 276ee5be8a09ed289a2bdc706c8fb615dd0c9ab6 Mon Sep 17 00:00:00 2001 From: Dirk Eschler Date: Sun, 31 Jan 2016 20:03:11 +0100 Subject: [PATCH 124/170] Prepared 0.11 release. --- CHANGELOG.txt | 7 +++++++ PKG-INFO | 2 +- modeltranslation/__init__.py | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index b495f2dc..b71736e4 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,10 @@ +v0.11 +===== +Date: 2016-01-31 + +Released without changes. + + v0.11rc2 ======== Date: 2015-12-15 diff --git a/PKG-INFO b/PKG-INFO index 2d63275e..1847281a 100644 --- a/PKG-INFO +++ b/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 1.0 Name: django-modeltranslation -Version: 0.11rc2 +Version: 0.11 Summary: Translates Django models using a registration approach. Home-page: https://github.com/deschler/django-modeltranslation Author: Peter Eschler, diff --git a/modeltranslation/__init__.py b/modeltranslation/__init__.py index b578cff9..98f4e18c 100644 --- a/modeltranslation/__init__.py +++ b/modeltranslation/__init__.py @@ -3,7 +3,7 @@ Version code adopted from Django development version. https://github.com/django/django """ -VERSION = (0, 11, 0, 'rc', 2) +VERSION = (0, 11, 0, 'final', 0) default_app_config = 'modeltranslation.apps.ModeltranslationConfig' From 3f4d48dd5acc49bc0dcace90e7dc3c532603fddb Mon Sep 17 00:00:00 2001 From: Pavel Sutyrin Date: Thu, 4 Feb 2016 02:20:35 +0300 Subject: [PATCH 125/170] let register decorator return decorated class --- modeltranslation/decorators.py | 1 + 1 file changed, 1 insertion(+) diff --git a/modeltranslation/decorators.py b/modeltranslation/decorators.py index 7282e267..ed8f0a74 100644 --- a/modeltranslation/decorators.py +++ b/modeltranslation/decorators.py @@ -21,5 +21,6 @@ def wrapper(opts_class): if not issubclass(opts_class, TranslationOptions): raise ValueError('Wrapped class must subclass TranslationOptions.') translator.register(model_or_iterable, opts_class, **options) + return opts_class return wrapper From dad7cc7ba6336f6983afc3daac9093d8399fde81 Mon Sep 17 00:00:00 2001 From: Matthias Kestenholz Date: Sat, 20 Feb 2016 11:21:07 +0100 Subject: [PATCH 126/170] Slightly reformat code to avoid flake8 errors These changes hopefully fix the Travis CI build. --- modeltranslation/admin.py | 29 +++++++++++++++++------------ modeltranslation/tests/tests.py | 20 +++++++++++--------- modeltranslation/translator.py | 9 ++++++--- 3 files changed, 34 insertions(+), 24 deletions(-) diff --git a/modeltranslation/admin.py b/modeltranslation/admin.py index 4d79dfcb..d2ed19a0 100755 --- a/modeltranslation/admin.py +++ b/modeltranslation/admin.py @@ -6,6 +6,13 @@ from django.contrib.admin.options import BaseModelAdmin, flatten_fieldsets, InlineModelAdmin from django import forms +from modeltranslation import settings as mt_settings +from modeltranslation.translator import translator +from modeltranslation.utils import ( + get_translation_fields, build_css_class, build_localized_fieldname, get_language, + get_language_bidi, unique) +from modeltranslation.widgets import ClearableWidgetWrapper + # Ensure that models are registered for translation before TranslationAdmin # runs. The import is supposed to resolve a race condition between model import # and translation registration in production (see issue #19). @@ -16,12 +23,6 @@ else: from django.contrib.contenttypes.admin import GenericTabularInline from django.contrib.contenttypes.admin import GenericStackedInline -from modeltranslation import settings as mt_settings -from modeltranslation.translator import translator -from modeltranslation.utils import ( - get_translation_fields, build_css_class, build_localized_fieldname, get_language, - get_language_bidi, unique) -from modeltranslation.widgets import ClearableWidgetWrapper class TranslationBaseModelAdmin(BaseModelAdmin): @@ -74,8 +75,12 @@ def patch_translation_field(self, db_field, field, **kwargs): b for b in form_class.__bases__ if b != NullCharField) field.__class__ = type( 'Nullable%s' % form_class.__name__, (NullableField, form_class), {}) - if ((db_field.empty_value == 'both' or orig_field.name in self.both_empty_values_fields) - and isinstance(field.widget, (forms.TextInput, forms.Textarea))): + if ( + ( + db_field.empty_value == 'both' or + orig_field.name in self.both_empty_values_fields + ) and isinstance(field.widget, (forms.TextInput, forms.Textarea)) + ): field.widget = ClearableWidgetWrapper(field.widget) css_classes = field.widget.attrs.get('class', '').split(' ') css_classes.append('mt') @@ -271,15 +276,15 @@ def _group_fieldsets(self, fieldsets): untranslated_fields = [ f.name for f in self.opts.fields if ( # Exclude the primary key field - f is not self.opts.auto_field + f is not self.opts.auto_field and # Exclude non-editable fields - and f.editable + f.editable and # Exclude the translation fields - and not hasattr(f, 'translated_field') + not hasattr(f, 'translated_field') and # Honour field arguments. We rely on the fact that the # passed fieldsets argument is already fully filtered # and takes options like exclude into account. - and f.name in flattened_fieldsets + f.name in flattened_fieldsets ) ] # TODO: Allow setting a label diff --git a/modeltranslation/tests/tests.py b/modeltranslation/tests/tests.py index 685ecae7..920f24ba 100644 --- a/modeltranslation/tests/tests.py +++ b/modeltranslation/tests/tests.py @@ -1013,7 +1013,9 @@ def test_indonesian(self): self.assertNotEqual(field.attname, build_localized_fieldname(field.name, 'id')) def assertQuerysetsEqual(self, qs1, qs2): - pk = lambda o: o.pk + def pk(o): + return o.pk + return self.assertEqual(sorted(qs1, key=pk), sorted(qs2, key=pk)) @@ -2459,16 +2461,16 @@ def test_q(self): n.save() self.assertEqual('en', get_language()) - self.assertEqual(0, models.ManagerTestModel.objects.filter(Q(title='de') - | Q(pk=42)).count()) - self.assertEqual(1, models.ManagerTestModel.objects.filter(Q(title='en') - | Q(pk=42)).count()) + self.assertEqual(0, models.ManagerTestModel.objects.filter(Q(title='de') | + Q(pk=42)).count()) + self.assertEqual(1, models.ManagerTestModel.objects.filter(Q(title='en') | + Q(pk=42)).count()) with override('de'): - self.assertEqual(1, models.ManagerTestModel.objects.filter(Q(title='de') - | Q(pk=42)).count()) - self.assertEqual(0, models.ManagerTestModel.objects.filter(Q(title='en') - | Q(pk=42)).count()) + self.assertEqual(1, models.ManagerTestModel.objects.filter(Q(title='de') | + Q(pk=42)).count()) + self.assertEqual(0, models.ManagerTestModel.objects.filter(Q(title='en') | + Q(pk=42)).count()) def test_f(self): """Test if F queries are rewritten.""" diff --git a/modeltranslation/translator.py b/modeltranslation/translator.py index 99b2de66..36ef1eef 100644 --- a/modeltranslation/translator.py +++ b/modeltranslation/translator.py @@ -440,9 +440,12 @@ def register(self, model_or_iterable, opts_class=None, **options): add_translation_fields(model, opts) # Delete all fields cache for related model (parent and children) - related = ((f for f in model._meta.get_fields() if (f.one_to_many or f.one_to_one) - and f.auto_created) if NEW_RELATED_API - else model._meta.get_all_related_objects()) + related = (( + f for f in model._meta.get_fields() + if (f.one_to_many or f.one_to_one) and + f.auto_created + ) if NEW_RELATED_API else model._meta.get_all_related_objects()) + for related_obj in related: delete_cache_fields(related_obj.model) From 9fc4a44080c94e3e9658a87327e671e876047fa9 Mon Sep 17 00:00:00 2001 From: LAI Date: Fri, 13 May 2016 10:01:57 +0200 Subject: [PATCH 127/170] Works with collapsed admin elements --- .../static/modeltranslation/js/tabbed_translation_fields.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modeltranslation/static/modeltranslation/js/tabbed_translation_fields.js b/modeltranslation/static/modeltranslation/js/tabbed_translation_fields.js index a0e4266f..81ce2773 100644 --- a/modeltranslation/static/modeltranslation/js/tabbed_translation_fields.js +++ b/modeltranslation/static/modeltranslation/js/tabbed_translation_fields.js @@ -213,7 +213,7 @@ var google, django, gettext; this.getAllGroupedTranslations = function () { var grouper = new TranslationFieldGrouper({ $fields: this.$table.find('.mt').filter( - 'input:visible, textarea:visible, select:visible') + 'input, textarea, select') }); //this.requiredColumns = this.getRequiredColumns(); this.initTable(); From 2f227a45af22359a9a964fd77f71dd6c9db20525 Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Wed, 1 Jun 2016 19:02:30 +0200 Subject: [PATCH 128/170] Fix values_list + annotate combo bug (close #374). --- modeltranslation/manager.py | 2 +- modeltranslation/tests/tests.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/modeltranslation/manager.py b/modeltranslation/manager.py index 7cf2b180..271e8390 100644 --- a/modeltranslation/manager.py +++ b/modeltranslation/manager.py @@ -525,7 +525,7 @@ def iterator(self): # Django >=1.8 fields += tuple(f for f in self.annotation_names if f not in fields) for row in super(FallbackValuesListQuerySet, self).iterator(): - if self.flat and len(fields) == 1: + if self.flat and len(self.original_fields) == 1: yield row[fields[0]] else: yield tuple(row[f] for f in fields) diff --git a/modeltranslation/tests/tests.py b/modeltranslation/tests/tests.py index 920f24ba..0533858c 100644 --- a/modeltranslation/tests/tests.py +++ b/modeltranslation/tests/tests.py @@ -2608,6 +2608,10 @@ def test_values(self): 'description': None, 'description_en': None, 'description_de': None}, ]) + # annotation issue (#374) + self.assertEqual(list(manager.values_list('title', flat=True).annotate(Count('title'))), + ['en']) + def test_values_list_annotation(self): models.TestModel(title='foo').save() models.TestModel(title='foo').save() From 302c931fe07373d60050fe13487842b1c1e2b11a Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Thu, 4 Aug 2016 22:32:22 +0200 Subject: [PATCH 129/170] Django 1.10 integration (ref #381) --- modeltranslation/fields.py | 6 + .../tests/auth_migrations/__init__.py | 0 modeltranslation/tests/models.py | 26 +++ modeltranslation/tests/settings.py | 2 + modeltranslation/tests/tests.py | 59 +++++- modeltranslation/tests/translation.py | 3 +- modeltranslation/translator.py | 192 +++++++++++------- 7 files changed, 201 insertions(+), 87 deletions(-) create mode 100644 modeltranslation/tests/auth_migrations/__init__.py diff --git a/modeltranslation/fields.py b/modeltranslation/fields.py index 26a98e76..7172eb46 100644 --- a/modeltranslation/fields.py +++ b/modeltranslation/fields.py @@ -310,6 +310,10 @@ def __set__(self, instance, value): """ Updates the translation field for the current language. """ + # In order for deferred fields to work, we also need to set the base value + instance.__dict__[self.field.name] = value + if isinstance(self.field, fields.related.ForeignKey): + instance.__dict__[self.field.get_attname()] = None if value is None else value.pk if getattr(instance, '_mt_init', False): # When assignment takes place in model instance constructor, don't set value. # This is essential for only/defer to work, but I think it's sensible anyway. @@ -375,6 +379,8 @@ def __set__(self, instance, value): # Localized field name with '_id' loc_attname = instance._meta.get_field(loc_field_name).get_attname() setattr(instance, loc_attname, value) + base_attname = instance._meta.get_field(self.field_name).get_attname() + instance.__dict__[base_attname] = value def __get__(self, instance, owner): if instance is None: diff --git a/modeltranslation/tests/auth_migrations/__init__.py b/modeltranslation/tests/auth_migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modeltranslation/tests/models.py b/modeltranslation/tests/models.py index dcbd4b20..67ee24b2 100644 --- a/modeltranslation/tests/models.py +++ b/modeltranslation/tests/models.py @@ -313,3 +313,29 @@ class RequiredModel(models.Model): class DecoratedModel(models.Model): title = models.CharField(ugettext_lazy('title'), max_length=255) + + +# ######### Name collision registration testing + +class ConflictModel(models.Model): + title = models.CharField(ugettext_lazy('title'), max_length=255) + title_de = models.IntegerField() + + +class AbstractConflictModelA(models.Model): + title_de = models.IntegerField() + + class Meta: + abstract = True + + +class AbstractConflictModelB(AbstractConflictModelA): + title = models.CharField(ugettext_lazy('title'), max_length=255) + + +class MultitableConflictModelA(models.Model): + title_de = models.IntegerField() + + +class MultitableConflictModelB(MultitableConflictModelA): + title = models.CharField(ugettext_lazy('title'), max_length=255) diff --git a/modeltranslation/tests/settings.py b/modeltranslation/tests/settings.py index a3204e7f..75d98037 100644 --- a/modeltranslation/tests/settings.py +++ b/modeltranslation/tests/settings.py @@ -22,3 +22,5 @@ MODELTRANSLATION_FALLBACK_LANGUAGES = () ROOT_URLCONF = 'modeltranslation.tests.urls' + +MIGRATION_MODULES = {'auth': 'modeltranslation.tests.auth_migrations'} diff --git a/modeltranslation/tests/tests.py b/modeltranslation/tests/tests.py index 0533858c..1d52c179 100644 --- a/modeltranslation/tests/tests.py +++ b/modeltranslation/tests/tests.py @@ -45,7 +45,9 @@ def decorator(test_item): from modeltranslation.utils import (build_css_class, build_localized_fieldname, auto_populate, fallbacks) -MIGRATIONS = django.VERSION >= (1, 8) +MIGRATE_CMD = django.VERSION >= (1, 8) +MIGRATIONS = MIGRATE_CMD and "django.contrib.auth" in TEST_SETTINGS['INSTALLED_APPS'] +NEW_DEFERRED_API = django.VERSION >= (1, 10) models = translation = None @@ -118,6 +120,12 @@ def setUpClass(cls): mgr = (override_settings(**TEST_SETTINGS) if django.VERSION < (1, 8) else dummy_context_mgr()) with mgr: + # 0. Render initial migration of auth + from django.db import connections, DEFAULT_DB_ALIAS + if MIGRATIONS: + call_command('makemigrations', 'auth', verbosity=2, interactive=False, + database=connections[DEFAULT_DB_ALIAS].alias) + # 1. Reload translation in case USE_I18N was False from django.utils import translation as dj_trans imp.reload(dj_trans) @@ -134,26 +142,39 @@ def setUpClass(cls): cls.cache.load_app('modeltranslation.tests') else: del cls.cache.all_models['tests'] + if MIGRATIONS: + del cls.cache.all_models['auth'] import sys sys.modules.pop('modeltranslation.tests.models', None) sys.modules.pop('modeltranslation.tests.translation', None) + if MIGRATIONS: + sys.modules.pop('django.contrib.auth.models', None) cls.cache.get_app_config('tests').import_models(cls.cache.all_models['tests']) + if MIGRATIONS: + cls.cache.get_app_config('auth').import_models(cls.cache.all_models['auth']) # 4. Autodiscover from modeltranslation.models import handle_translation_registrations handle_translation_registrations() # 5. makemigrations (``migrate=False`` in case of south) - from django.db import connections, DEFAULT_DB_ALIAS if MIGRATIONS: - call_command('makemigrations', verbosity=2, interactive=False, + call_command('makemigrations', 'auth', verbosity=2, interactive=False, database=connections[DEFAULT_DB_ALIAS].alias) # 6. Syncdb (``migrate=False`` in case of south) - cmd = 'migrate' if MIGRATIONS else 'syncdb' + cmd = 'migrate' if MIGRATE_CMD else 'syncdb' call_command(cmd, verbosity=0, migrate=False, interactive=False, run_syncdb=True, database=connections[DEFAULT_DB_ALIAS].alias, load_initial_data=False) + # 7. clean migrations + if MIGRATIONS: + import glob + dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), + "auth_migrations") + for f in glob.glob(dir + "/000?_*.py*"): + os.unlink(f) + # A rather dirty trick to import models into module namespace, but not before # tests app has been added into INSTALLED_APPS and loaded # (that's why this is not imported in normal import section) @@ -292,6 +313,21 @@ def test_registration(self): self.assertRaises(translator.DescendantRegistered, translator.translator.unregister, models.Slugged) + @skipUnless(NEW_DEFERRED_API, "Django 1.10 needed") + def test_registration_field_conflicts(self): + before = len(translator.translator.get_registered_models()) + + # Exception should be raised when conflicting field name detected + self.assertRaises(ValueError, translator.translator.register, + models.ConflictModel, fields=('title',)) + self.assertRaises(ValueError, translator.translator.register, + models.AbstractConflictModelB, fields=('title',)) + self.assertRaises(ValueError, translator.translator.register, + models.MultitableConflictModelB, fields=('title',)) + + # Model should not be registered + self.assertEqual(len(translator.translator.get_registered_models()), before) + def test_fields(self): field_names = dir(models.TestModel()) self.assertTrue('id' in field_names) @@ -2647,7 +2683,7 @@ def test_custom_manager_custom_method_name(self): qs = models.CustomManagerTestModel.objects.custom_qs() self.assertIsInstance(qs, MultilingualQuerySet) - @skipUnless(MIGRATIONS, 'migrations not available') + @skipUnless(MIGRATIONS, 'migrations/auth not available') def test_3rd_party_custom_manager(self): from django.contrib.auth.models import Group, GroupManager from modeltranslation.manager import MultilingualManager @@ -2855,13 +2891,20 @@ def assertDeferred(self, use_defer, *fields): self.assertEqual('title_de', inst1.title) self.assertEqual('title_de', inst2.title) + def assertDeferredClass(self, item): + if NEW_DEFERRED_API: + self.assertTrue(len(item.get_deferred_fields()) > 0) + else: + self.assertTrue(item.__class__._deferred) + def test_deferred(self): """ Check if ``only`` and ``defer`` are working. """ models.TestModel.objects.create(title_de='title_de', title_en='title_en') inst = models.TestModel.objects.only('title_en')[0] - self.assertNotEqual(inst.__class__, models.TestModel) + if not NEW_DEFERRED_API: + self.assertNotEqual(inst.__class__, models.TestModel) self.assertTrue(isinstance(inst, models.TestModel)) self.assertDeferred(False, 'title_en') @@ -2894,12 +2937,12 @@ def test_deferred_fk(self): models.ForeignKeyModel.objects.create(test=test) item = models.ForeignKeyModel.objects.select_related("test").defer("test__text")[0] - self.assertTrue(item.test.__class__._deferred) + self.assertDeferredClass(item.test) self.assertEqual('title_en', item.test.title) self.assertEqual('title_en', item.test.__class__.objects.only('title')[0].title) with override('de'): item = models.ForeignKeyModel.objects.select_related("test").defer("test__text")[0] - self.assertTrue(item.test.__class__._deferred) + self.assertDeferredClass(item.test) self.assertEqual('title_de', item.test.title) self.assertEqual('title_de', item.test.__class__.objects.only('title')[0].title) diff --git a/modeltranslation/tests/translation.py b/modeltranslation/tests/translation.py index 1dcef643..9b9919ed 100644 --- a/modeltranslation/tests/translation.py +++ b/modeltranslation/tests/translation.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- from django import VERSION +from django.conf import settings from django.utils.translation import ugettext_lazy from modeltranslation.translator import translator, register, TranslationOptions @@ -208,7 +209,7 @@ class DecoratedTranslationOptions(TranslationOptions): # ######### 3-rd party with custom manager -if VERSION >= (1, 8): +if VERSION >= (1, 8) and "django.contrib.auth" in settings.INSTALLED_APPS: from django.contrib.auth.models import Group @register(Group) diff --git a/modeltranslation/translator.py b/modeltranslation/translator.py index 36ef1eef..2f12823f 100644 --- a/modeltranslation/translator.py +++ b/modeltranslation/translator.py @@ -11,11 +11,12 @@ TranslatedRelationIdDescriptor, LanguageCacheSingleObjectDescriptor) from modeltranslation.manager import (MultilingualManager, MultilingualQuerysetManager, - rewrite_lookup_key) + rewrite_lookup_key, append_translated) from modeltranslation.utils import build_localized_fieldname, parse_field NEW_RELATED_API = VERSION >= (1, 9) +NEW_DEFERRED_API = NEW_MANAGER_API = NEW_ABSTRACT_API = VERSION >= (1, 10) class AlreadyRegistered(Exception): @@ -142,10 +143,16 @@ def add_translation_fields(model, opts): # Construct the name for the localized field localized_field_name = build_localized_fieldname(field_name, l) # Check if the model already has a field by that name + if hasattr(model, localized_field_name): - raise ValueError( - "Error adding translation field. Model '%s' already contains a field named" - "'%s'." % (model._meta.object_name, localized_field_name)) + # Check if are not dealing with abstract field inherited. + for cls in model.__mro__: + if hasattr(cls, '_meta') and cls.__dict__.get(localized_field_name, None): + cls_opts = translator._get_options_for_model(cls) + if not cls._meta.abstract or field_name not in cls_opts.local_fields: + raise ValueError("Error adding translation field. Model '%s' already" + " contains a field named '%s'." % + (model._meta.object_name, localized_field_name)) # This approach implements the translation fields as full valid # django model fields and therefore adds them via add_to_class model.add_to_class(localized_field_name, translation_field) @@ -203,8 +210,10 @@ def deconstruct(self): manager.__class__ = NewMultilingualManager - for _, attname, cls in model._meta.concrete_managers + model._meta.abstract_managers: - current_manager = getattr(model, attname) + managers = (model._meta.local_managers if NEW_MANAGER_API else + (getattr(model, x[1]) for x in + model._meta.concrete_managers + model._meta.abstract_managers)) + for current_manager in managers: prev_class = current_manager.__class__ patch_manager_class(current_manager) if model._default_manager.__class__ is prev_class: @@ -215,6 +224,8 @@ def deconstruct(self): # share the same class. model._default_manager.__class__ = current_manager.__class__ patch_manager_class(model._base_manager) + if hasattr(model._meta, "_expire_cache"): + model._meta._expire_cache() def patch_constructor(model): @@ -225,7 +236,7 @@ def patch_constructor(model): def new_init(self, *args, **kwargs): self._mt_init = True - if not self._deferred: + if NEW_DEFERRED_API or not self._deferred: populate_translation_fields(self.__class__, kwargs) for key, val in list(kwargs.items()): new_key = rewrite_lookup_key(model, key) @@ -279,6 +290,21 @@ def new_get_deferred_fields(self): model.get_deferred_fields = new_get_deferred_fields +def patch_refresh_from_db(model): + """ + Django >= 1.10: patch refreshing deferred fields. Crucial for only/defer to work. + """ + if not hasattr(model, 'refresh_from_db'): + return + old_refresh_from_db = model.refresh_from_db + + def new_refresh_from_db(self, using=None, fields=None): + if fields is not None: + fields = append_translated(self.__class__, fields) + return old_refresh_from_db(self, using, fields) + model.refresh_from_db = new_refresh_from_db + + def patch_metaclass(model): """ Monkey patches original model metaclass to exclude translated fields on deferred subclasses. @@ -426,86 +452,96 @@ def register(self, model_or_iterable, opts_class=None, **options): # Find inherited fields and create options instance for the model. opts = self._get_options_for_model(model, opts_class, **options) - # Now, when all fields are initialized and inherited, validate configuration. - opts.validate() + # If an exception is raised during registration, mark model as not-registered + try: + self._register_single_model(model, opts) + except Exception: + self._registry[model].registered = False + raise - # Mark the object explicitly as registered -- registry caches - # options of all models, registered or not. - opts.registered = True + def _register_single_model(self, model, opts): + # Now, when all fields are initialized and inherited, validate configuration. + opts.validate() - # Add translation fields to the model. - if model._meta.proxy: - delete_cache_fields(model) - else: - add_translation_fields(model, opts) + # Mark the object explicitly as registered -- registry caches + # options of all models, registered or not. + opts.registered = True + + # Add translation fields to the model. + if model._meta.proxy: + delete_cache_fields(model) + else: + add_translation_fields(model, opts) - # Delete all fields cache for related model (parent and children) - related = (( - f for f in model._meta.get_fields() - if (f.one_to_many or f.one_to_one) and - f.auto_created - ) if NEW_RELATED_API else model._meta.get_all_related_objects()) + # Delete all fields cache for related model (parent and children) + related = (( + f for f in model._meta.get_fields() + if (f.one_to_many or f.one_to_one) and + f.auto_created + ) if NEW_RELATED_API else model._meta.get_all_related_objects()) - for related_obj in related: - delete_cache_fields(related_obj.model) + for related_obj in related: + delete_cache_fields(related_obj.model) - # Set MultilingualManager - add_manager(model) + # Set MultilingualManager + add_manager(model) - # Patch __init__ to rewrite fields - patch_constructor(model) + # Patch __init__ to rewrite fields + patch_constructor(model) - # Connect signal for model - post_init.connect(delete_mt_init, sender=model) + # Connect signal for model + post_init.connect(delete_mt_init, sender=model) - # Patch clean_fields to verify form field clearing - patch_clean_fields(model) + # Patch clean_fields to verify form field clearing + patch_clean_fields(model) - # Patch __metaclass__ and other methods to allow deferring to work + # Patch __metaclass__ and other methods to allow deferring to work + if not NEW_DEFERRED_API: patch_metaclass(model) - patch_get_deferred_fields(model) - - # Substitute original field with descriptor - model_fallback_languages = getattr(opts, 'fallback_languages', None) - model_fallback_values = getattr(opts, 'fallback_values', NONE) - model_fallback_undefined = getattr(opts, 'fallback_undefined', NONE) - for field_name in opts.local_fields.keys(): - field = model._meta.get_field(field_name) - field_fallback_value = parse_field(model_fallback_values, field_name, NONE) - field_fallback_undefined = parse_field(model_fallback_undefined, field_name, NONE) - descriptor = TranslationFieldDescriptor( - field, - fallback_languages=model_fallback_languages, - fallback_value=field_fallback_value, - fallback_undefined=field_fallback_undefined) - setattr(model, field_name, descriptor) - if isinstance(field, ForeignKey): - # We need to use a special descriptor so that - # _id fields on translated ForeignKeys work - # as expected. - desc = TranslatedRelationIdDescriptor(field_name, model_fallback_languages) - setattr(model, field.get_attname(), desc) - - # Set related field names on other model - if NEW_RELATED_API and not field.remote_field.is_hidden(): - other_opts = self._get_options_for_model(field.remote_field.to) - other_opts.related = True - other_opts.related_fields.append(field.related_query_name()) - # Add manager in case of non-registered model - add_manager(field.remote_field.to) - elif not NEW_RELATED_API and not field.rel.is_hidden(): - other_opts = self._get_options_for_model(field.rel.to) - other_opts.related = True - 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.remote_field.to, field.remote_field.get_accessor_name()) - if NEW_RELATED_API - else getattr(field.rel.to, field.related.get_accessor_name())) - patch_related_object_descriptor_caching(sro_descriptor) + patch_get_deferred_fields(model) + patch_refresh_from_db(model) + + # Substitute original field with descriptor + model_fallback_languages = getattr(opts, 'fallback_languages', None) + model_fallback_values = getattr(opts, 'fallback_values', NONE) + model_fallback_undefined = getattr(opts, 'fallback_undefined', NONE) + for field_name in opts.local_fields.keys(): + field = model._meta.get_field(field_name) + field_fallback_value = parse_field(model_fallback_values, field_name, NONE) + field_fallback_undefined = parse_field(model_fallback_undefined, field_name, NONE) + descriptor = TranslationFieldDescriptor( + field, + fallback_languages=model_fallback_languages, + fallback_value=field_fallback_value, + fallback_undefined=field_fallback_undefined) + setattr(model, field_name, descriptor) + if isinstance(field, ForeignKey): + # We need to use a special descriptor so that + # _id fields on translated ForeignKeys work + # as expected. + desc = TranslatedRelationIdDescriptor(field_name, model_fallback_languages) + setattr(model, field.get_attname(), desc) + + # Set related field names on other model + if NEW_RELATED_API and not field.remote_field.is_hidden(): + other_opts = self._get_options_for_model(field.remote_field.to) + other_opts.related = True + other_opts.related_fields.append(field.related_query_name()) + # Add manager in case of non-registered model + add_manager(field.remote_field.to) + elif not NEW_RELATED_API and not field.rel.is_hidden(): + other_opts = self._get_options_for_model(field.rel.to) + other_opts.related = True + 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.remote_field.to, field.remote_field.get_accessor_name()) + if NEW_RELATED_API + else getattr(field.rel.to, field.related.get_accessor_name())) + patch_related_object_descriptor_caching(sro_descriptor) def unregister(self, model_or_iterable): """ @@ -547,7 +583,7 @@ def _get_options_for_model(self, model, opts_class=None, **options): Returns an instance of translation options with translated fields defined for the ``model`` and inherited from superclasses. """ - if model._deferred: + if not NEW_DEFERRED_API and model._deferred: model = model._meta.proxy_for_model if model not in self._registry: # Create a new type for backwards compatibility. From 9e10e49543cd5ae19571cb59afd0a18de3b02ba8 Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Thu, 18 Aug 2016 19:33:16 +0200 Subject: [PATCH 130/170] Add Django 1.10 to tox/travis. --- .travis.yml | 30 ++++++++++++++++++++++++++++++ tox.ini | 21 +++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/.travis.yml b/.travis.yml index af5d6ef5..12c0f4fa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,6 +25,9 @@ env: - DJANGO=1.9 DB=sqlite - DJANGO=1.9 DB=postgres - DJANGO=1.9 DB=mysql + - DJANGO=1.10 DB=sqlite + - DJANGO=1.10 DB=postgres + - DJANGO=1.10 DB=mysql matrix: exclude: - python: "3.2" @@ -70,6 +73,13 @@ matrix: env: DJANGO=1.9 DB=postgres - python: "2.6" env: DJANGO=1.9 DB=mysql + - python: "2.6" + env: DJANGO=1.10 DB=sqlite + - python: "2.6" + env: DJANGO=1.10 DB=postgres + - python: "2.6" + env: DJANGO=1.10 DB=mysql + - python: "3.2" env: DJANGO=1.9 DB=sqlite - python: "3.2" @@ -82,6 +92,18 @@ matrix: env: DJANGO=1.9 DB=postgres - python: "3.3" env: DJANGO=1.9 DB=mysql + - python: "3.2" + env: DJANGO=1.10 DB=sqlite + - python: "3.2" + env: DJANGO=1.10 DB=postgres + - python: "3.2" + env: DJANGO=1.10 DB=mysql + - python: "3.3" + env: DJANGO=1.10 DB=sqlite + - python: "3.3" + env: DJANGO=1.10 DB=postgres + - python: "3.3" + env: DJANGO=1.10 DB=mysql - python: "3.5" env: DJANGO=1.5 DB=sqlite - python: "3.5" @@ -141,6 +163,14 @@ matrix: env: DJANGO=1.9 DB=mysql - python: "3.5" env: DJANGO=1.9 DB=mysql + - python: "3.2" + env: DJANGO=1.10 DB=mysql + - python: "3.3" + env: DJANGO=1.10 DB=mysql + - python: "3.4" + env: DJANGO=1.10 DB=mysql + - python: "3.5" + env: DJANGO=1.10 DB=mysql before_install: - pip install -q flake8 --use-mirrors - PYFLAKES_NODOCTEST=1 flake8 modeltranslation diff --git a/tox.ini b/tox.ini index 0bb2759b..a6bef8dd 100644 --- a/tox.ini +++ b/tox.ini @@ -5,6 +5,9 @@ exclude = .tox,docs/modeltranslation/conf.py [tox] distribute = False envlist = + py35-1.10.X, + py34-1.10.X, + py27-1.10.X, py35-1.9.X, py34-1.9.X, py27-1.9.X, @@ -34,6 +37,24 @@ commands = {envpython} runtests.py +[testenv:py35-1.10.X] +basepython = python3.5 +deps = + Django>=1.10,<1.11 + Pillow + +[testenv:py34-1.10.X] +basepython = python3.4 +deps = + Django>=1.10,<1.11 + Pillow + +[testenv:py27-1.10.X] +basepython = python2.7 +deps = + Django>=1.10,<1.11 + Pillow + [testenv:py35-1.9.X] basepython = python3.5 deps = From a7b5102c43eaa88e39297ce8da35be7b5f9b2655 Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Fri, 19 Aug 2016 21:20:36 +0200 Subject: [PATCH 131/170] Fix deferred classes signal connection (close #379). --- modeltranslation/tests/tests.py | 7 +++++++ modeltranslation/translator.py | 7 ++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/modeltranslation/tests/tests.py b/modeltranslation/tests/tests.py index 1d52c179..3f894ef7 100644 --- a/modeltranslation/tests/tests.py +++ b/modeltranslation/tests/tests.py @@ -2958,6 +2958,13 @@ def test_deferred_spanning(self): self.assertIn('text_en', dir(item1.__class__)) self.assertIn('text_de', dir(item1.__class__)) + def test_deferred_rule2(self): + models.TestModel.objects.create(title_de='title_de', title_en='title_en') + o = models.TestModel.objects.only('title')[0] + self.assertEqual(o.title, "title_en") + o.title = "bla" + self.assertEqual(o.title, "bla") + def test_translation_fields_appending(self): from modeltranslation.manager import append_lookup_keys, append_lookup_key self.assertEqual(set(['untrans']), append_lookup_key(models.ForeignKeyModel, 'untrans')) diff --git a/modeltranslation/translator.py b/modeltranslation/translator.py index 2f12823f..11880271 100644 --- a/modeltranslation/translator.py +++ b/modeltranslation/translator.py @@ -490,7 +490,12 @@ def _register_single_model(self, model, opts): patch_constructor(model) # Connect signal for model - post_init.connect(delete_mt_init, sender=model) + if NEW_DEFERRED_API: + post_init.connect(delete_mt_init, sender=model) + else: + # deferred models have their own classes and the `sender` does not match. + # Connect signal for all models. + post_init.connect(delete_mt_init, dispatch_uid="modeltranslation") # Patch clean_fields to verify form field clearing patch_clean_fields(model) From 5c93fd6b7f70ddb76d925430254094573dcac21f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Primo=C5=BE=20Kerin?= Date: Wed, 7 Sep 2016 13:51:11 +0200 Subject: [PATCH 132/170] Make sync_translation_fields command Django 1.10 compatible --- .../commands/sync_translation_fields.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/modeltranslation/management/commands/sync_translation_fields.py b/modeltranslation/management/commands/sync_translation_fields.py index 9bd6147d..cbe131ad 100644 --- a/modeltranslation/management/commands/sync_translation_fields.py +++ b/modeltranslation/management/commands/sync_translation_fields.py @@ -11,6 +11,7 @@ """ from optparse import make_option import django +from django import VERSION from django.core.management.base import BaseCommand from django.core.management.color import no_style from django.db import connection, transaction @@ -51,10 +52,16 @@ class Command(BaseCommand): ' sync database structure. Does not remove columns of removed' ' languages or undeclared fields.') - option_list = BaseCommand.option_list + ( - make_option('--noinput', action='store_false', dest='interactive', default=True, - help='Do NOT prompt the user for input of any kind.'), - ) + if VERSION < (1, 8): + from optparse import make_option + option_list = BaseCommand.option_list + ( + make_option('--noinput', action='store_false', dest='interactive', default=True, + help='Do NOT prompt the user for input of any kind.'), + ) + else: + def add_arguments(self, parser): + parser.add_argument('--noinput', action='store_false', dest='interactive', default=True, + help='Do NOT prompt the user for input of any kind.'), def handle(self, *args, **options): """ From b6cc6888af622ed836164604f1d190e4753fedcf Mon Sep 17 00:00:00 2001 From: Matthias Kestenholz Date: Wed, 14 Sep 2016 11:20:59 +0200 Subject: [PATCH 133/170] Remove the unsupported use-mirrors pip option Remove the duplicated make_option import Pin flake8's version to <3 as long as we are still supporting Python 2.6 Stop complaining (F999) about the assert Fix travis.py for now This will fail again as soon as Django 2.0 is released. --- .travis.yml | 12 ++++++------ .../management/commands/sync_translation_fields.py | 1 - modeltranslation/tests/urls.py | 3 ++- travis.py | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index 12c0f4fa..1d066d1b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -172,20 +172,20 @@ matrix: - python: "3.5" env: DJANGO=1.10 DB=mysql before_install: - - pip install -q flake8 --use-mirrors + - pip install -q 'flake8<3' - PYFLAKES_NODOCTEST=1 flake8 modeltranslation before_script: - mysql -e 'create database modeltranslation;' - psql -c 'create database modeltranslation;' -U postgres install: - - if [[ $DB == mysql ]]; then pip install -q mysql-python --use-mirrors; fi - - if [[ $DB == postgres ]]; then pip install -q psycopg2 --use-mirrors; fi - - pip install -q Pillow --use-mirrors + - if [[ $DB == mysql ]]; then pip install -q mysql-python; fi + - if [[ $DB == postgres ]]; then pip install -q psycopg2; fi + - pip install -q Pillow - IDJANGO=$(./travis.py $DJANGO) - pip install -q $IDJANGO - - pip install -e . --use-mirrors + - pip install -e . - if [[ $TRAVIS_PYTHON_VERSION == '3.2' ]]; then pip install 'coverage<4.0.0'; fi - - pip install -q coveralls --use-mirrors + - pip install -q coveralls script: - django-admin.py --version - coverage run --source=modeltranslation ./runtests.py diff --git a/modeltranslation/management/commands/sync_translation_fields.py b/modeltranslation/management/commands/sync_translation_fields.py index cbe131ad..5518ae08 100644 --- a/modeltranslation/management/commands/sync_translation_fields.py +++ b/modeltranslation/management/commands/sync_translation_fields.py @@ -9,7 +9,6 @@ Credits: Heavily inspired by django-transmeta's sync_transmeta_db command. """ -from optparse import make_option import django from django import VERSION from django.core.management.base import BaseCommand diff --git a/modeltranslation/tests/urls.py b/modeltranslation/tests/urls.py index 653df6d0..2cd14ef3 100644 --- a/modeltranslation/tests/urls.py +++ b/modeltranslation/tests/urls.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- try: from django.conf.urls import include, patterns, url - assert (include, patterns, url) # Workaround for pyflakes issue #13 + # Workaround for pyflakes issue #13 + assert (include, patterns, url) # noqa except ImportError: # Django 1.3 fallback from django.conf.urls.defaults import include, patterns, url # NOQA from django.contrib import admin diff --git a/travis.py b/travis.py index 6f4c4f2c..a4768f5c 100755 --- a/travis.py +++ b/travis.py @@ -5,5 +5,5 @@ if version.startswith('http'): print(version) else: - next_version = float(version) + 0.1 - print('Django>=%s,<%.1f' % (version, next_version)) + next_version = version[:-1] + '%d' % (int(version[-1]) + 1) + print('Django>=%s,<%s' % (version, next_version)) From fd3a674e9f3d0dbb9517272e44fb66ea00ce2fbd Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Tue, 20 Sep 2016 14:52:11 +0200 Subject: [PATCH 134/170] Update docs about original field value status. --- CHANGELOG.txt | 9 +++++++++ docs/modeltranslation/usage.rst | 4 ++++ 2 files changed, 13 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index b71736e4..46def079 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,12 @@ +v0.12 +===== +Date: ??? + + ADDED: Support for Django 1.10 +CHANGED: Original field value became more unreliable and undetermined; + please make sure you're not using it anywhere. See + http://django-modeltranslation.readthedocs.io/en/latest/usage.html#the-state-of-the-original-field + v0.11 ===== Date: 2016-01-31 diff --git a/docs/modeltranslation/usage.rst b/docs/modeltranslation/usage.rst index e65c70e6..9bbe7847 100644 --- a/docs/modeltranslation/usage.rst +++ b/docs/modeltranslation/usage.rst @@ -347,6 +347,7 @@ The State of the Original Field ------------------------------- .. versionchanged:: 0.5 +.. versionchanged:: 0.12 As defined by the :ref:`rules`, accessing the original field is guaranteed to work on the associated translation field of the current language. This applies @@ -359,6 +360,9 @@ Attempts to keep the value in sync with either the default or current language's field value has raised a boatload of unpredictable side effects in older versions of modeltranslation. +Since version 0.12 the original field is expected to have even more undetermined value. +It's because Django 1.10 changed the way deferred fields work. + .. warning:: Do not rely on the underlying value of the *original field* in any way! From 257f59de97e4cadf53ada966d9971041aeb08930 Mon Sep 17 00:00:00 2001 From: Dirk Eschler Date: Tue, 20 Sep 2016 18:07:15 +0200 Subject: [PATCH 135/170] Prepared 0.12 release. --- CHANGELOG.txt | 16 ++++++++++++++-- PKG-INFO | 2 +- modeltranslation/__init__.py | 2 +- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 46def079..ec65ffde 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,11 +1,23 @@ v0.12 ===== -Date: ??? +Date: 2016-09-20 + + ADDED: Support for Django 1.10. + (resolves issue #360, thanks Jacek Tomaszewski and Primož Kerin) - ADDED: Support for Django 1.10 CHANGED: Original field value became more unreliable and undetermined; please make sure you're not using it anywhere. See http://django-modeltranslation.readthedocs.io/en/latest/usage.html#the-state-of-the-original-field +CHANGED: Let register decorator return decorated class + (resolves issue #360, thanks spacediver) + + FIXED: Deferred classes signal connection. + (resolves issue #379, thanks Jacek Tomaszewski) + FIXED: values_list + annotate combo bug. + (resolves issue #374, thanks Jacek Tomaszewski) + FIXED: Several flake8 and travis related issues. + (resolves issues #363, thanks Matthias K) + v0.11 ===== diff --git a/PKG-INFO b/PKG-INFO index 1847281a..a65f6027 100644 --- a/PKG-INFO +++ b/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 1.0 Name: django-modeltranslation -Version: 0.11 +Version: 0.12 Summary: Translates Django models using a registration approach. Home-page: https://github.com/deschler/django-modeltranslation Author: Peter Eschler, diff --git a/modeltranslation/__init__.py b/modeltranslation/__init__.py index 98f4e18c..26111c1c 100644 --- a/modeltranslation/__init__.py +++ b/modeltranslation/__init__.py @@ -3,7 +3,7 @@ Version code adopted from Django development version. https://github.com/django/django """ -VERSION = (0, 11, 0, 'final', 0) +VERSION = (0, 12, 0, 'final', 0) default_app_config = 'modeltranslation.apps.ModeltranslationConfig' From 1b529023687a9e01bf8d0e7a77c453f0ec2e78de Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Wed, 21 Sep 2016 22:14:14 +0200 Subject: [PATCH 136/170] Update version table and make it (hopefully) more readable. Add missing build to tox, by the way. --- docs/modeltranslation/installation.rst | 55 +++++++++++++------------- tox.ini | 7 ++++ 2 files changed, 35 insertions(+), 27 deletions(-) diff --git a/docs/modeltranslation/installation.rst b/docs/modeltranslation/installation.rst index 21ddae5c..cec291fd 100644 --- a/docs/modeltranslation/installation.rst +++ b/docs/modeltranslation/installation.rst @@ -6,33 +6,34 @@ Installation Requirements ------------ -+------------------+------------+-----------+ -| Modeltranslation | Python | Django | -+==================+============+===========+ -| >=0.9 | 3.2 - 3.4 | 1.5 - 1.8 | -| +------------+-----------+ -| | 2.7 | 1.8 | -| +------------+-----------+ -| | 2.6 - 2.7 | 1.4 - 1.6 | -+------------------+------------+-----------+ -| ==0.8 | 3.2 - 3.4 | 1.5 - 1.7 | -| +------------+-----------+ -| | 2.7 | 1.7 | -| +------------+-----------+ -| | 2.6 - 2.7 | 1.4 - 1.6 | -+------------------+------------+-----------+ -| ==0.7 | 3.2 - 3.3 | 1.5 - 1.6 | -| +------------+-----------+ -| | 2.6 - 2.7 | 1.4 - 1.6 | -+------------------+------------+-----------+ -| ==0.5, ==0.6 | 2.6 - 2.7 | 1.5 | -| +------------+-----------+ -| | 2.5 - 2.7 | 1.3 - 1.4 | -+------------------+------------+-----------+ -| ==0.4 | 2.5 - 2.7 | 1.3 - 1.4 | -+------------------+------------+-----------+ -| <=0.3 | 2.4 - 2.7 | 1.0 - 1.4 | -+------------------+------------+-----------+ +Which Modeltranslation version is required for given Django-Python combination to work? + +======= ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== +Python Django +------- ------------------------------------------------------ +version 1.0 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 1.9 1.10 +======= ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== +2.4 |u3| |u3| |u3| |u3| +2.5 |u3| |u3| |u3| |36| |36| +2.6 |u3| |u3| |u3| |36| |3| |5| |7| +2.7 |u3| |u3| |u3| |36| |3| |5| |7| |8| |9| |11| |12| +3.2 |7| |7| |8| |9| +3.3 |7| |7| |8| |9| +3.4 |8| |9| |11| |12| +3.5 |9| |11| |12| +======= ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== + +(``-X`` denotes "up to version X", whereas ``X+`` means "from version X upwards") + +.. |u3| replace:: -0.3 +.. |3| replace:: 0.3+ +.. |36| replace:: 0.3-0.6 +.. |5| replace:: 0.5+ +.. |7| replace:: 0.7+ +.. |8| replace:: 0.8+ +.. |9| replace:: 0.9+ +.. |11| replace:: 0.11+ +.. |12| replace:: 0.12+ Using Pip diff --git a/tox.ini b/tox.ini index a6bef8dd..9cbb295c 100644 --- a/tox.ini +++ b/tox.ini @@ -16,6 +16,7 @@ envlist = py33-1.8.X, py32-1.8.X, py27-1.8.X, + py34-1.7.X, py33-1.7.X, py32-1.7.X, py27-1.7.X, @@ -103,6 +104,12 @@ deps = Django>=1.8,<1.9 Pillow +[testenv:py34-1.7.X] +basepython = python3.4 +deps = + Django>=1.7,<1.8 + Pillow + [testenv:py33-1.7.X] basepython = python3.3 deps = From 85e26e3842324cd34597ebd55295ac358db73e73 Mon Sep 17 00:00:00 2001 From: Dirk Eschler Date: Wed, 22 Feb 2017 14:17:27 +0100 Subject: [PATCH 137/170] Removed superfluous can_import_settings check. The option has been removed in Django 1.11. --- modeltranslation/management/commands/loaddata.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modeltranslation/management/commands/loaddata.py b/modeltranslation/management/commands/loaddata.py index 0fb039de..b936eefd 100644 --- a/modeltranslation/management/commands/loaddata.py +++ b/modeltranslation/management/commands/loaddata.py @@ -49,7 +49,7 @@ def __init__(self): self.locale = translation.get_language() def handle(self, *fixture_labels, **options): - if self.can_import_settings and hasattr(self, 'locale'): + if hasattr(self, 'locale'): from django.utils import translation translation.activate(self.locale) From 4b07119c3021fa1d64cbcf3db93b1cd9416699db Mon Sep 17 00:00:00 2001 From: Helio Correia Date: Wed, 8 Mar 2017 12:00:23 +0000 Subject: [PATCH 138/170] Modify javascript to also consider the div for using wysiwyg like Quill --- .../static/modeltranslation/js/tabbed_translation_fields.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modeltranslation/static/modeltranslation/js/tabbed_translation_fields.js b/modeltranslation/static/modeltranslation/js/tabbed_translation_fields.js index a0e4266f..ce66f36f 100644 --- a/modeltranslation/static/modeltranslation/js/tabbed_translation_fields.js +++ b/modeltranslation/static/modeltranslation/js/tabbed_translation_fields.js @@ -392,7 +392,7 @@ var google, django, gettext; // Group normal fields and fields in (existing) stacked inlines var grouper = new TranslationFieldGrouper({ $fields: $('.mt').filter( - 'input:visible, textarea:visible, select:visible, iframe').filter(':parents(.tabular)') + 'input:visible, textarea:visible, select:visible, iframe, div').filter(':parents(.tabular)') }); MainSwitch.init(grouper.groupedTranslations, createTabs(grouper.groupedTranslations)); From 576fe159ed4363ce3326a0c473fb6ec4a7f754cd Mon Sep 17 00:00:00 2001 From: Florian Verdet Date: Sun, 19 Mar 2017 06:09:36 +0100 Subject: [PATCH 139/170] settings: fix keep FALLBACK_LANGUAGES as tuple required to be tuple at: modeltranslation/utils.py#L117 --- modeltranslation/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modeltranslation/settings.py b/modeltranslation/settings.py index 4e897303..309075d7 100644 --- a/modeltranslation/settings.py +++ b/modeltranslation/settings.py @@ -36,7 +36,7 @@ # By default we fallback to the default language FALLBACK_LANGUAGES = getattr(settings, 'MODELTRANSLATION_FALLBACK_LANGUAGES', (DEFAULT_LANGUAGE,)) if isinstance(FALLBACK_LANGUAGES, (tuple, list)): - FALLBACK_LANGUAGES = {'default': FALLBACK_LANGUAGES} + FALLBACK_LANGUAGES = {'default': tuple(FALLBACK_LANGUAGES)} if 'default' not in FALLBACK_LANGUAGES: raise ImproperlyConfigured( 'MODELTRANSLATION_FALLBACK_LANGUAGES does not contain "default" key.') From c0826dbcf8cda5b3d18fd9015792ab9bfe4a6cd7 Mon Sep 17 00:00:00 2001 From: Dirk Eschler Date: Wed, 5 Apr 2017 13:48:18 +0200 Subject: [PATCH 140/170] Added Django 1.11 to travis/tox. --- .travis.yml | 29 +++++++++++++++++++++++++++++ tox.ini | 21 +++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/.travis.yml b/.travis.yml index 1d066d1b..b839939a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,6 +28,9 @@ env: - DJANGO=1.10 DB=sqlite - DJANGO=1.10 DB=postgres - DJANGO=1.10 DB=mysql + - DJANGO=1.11 DB=sqlite + - DJANGO=1.11 DB=postgres + - DJANGO=1.11 DB=mysql matrix: exclude: - python: "3.2" @@ -79,6 +82,12 @@ matrix: env: DJANGO=1.10 DB=postgres - python: "2.6" env: DJANGO=1.10 DB=mysql + - python: "2.6" + env: DJANGO=1.11 DB=sqlite + - python: "2.6" + env: DJANGO=1.11 DB=postgres + - python: "2.6" + env: DJANGO=1.11 DB=mysql - python: "3.2" env: DJANGO=1.9 DB=sqlite @@ -104,6 +113,18 @@ matrix: env: DJANGO=1.10 DB=postgres - python: "3.3" env: DJANGO=1.10 DB=mysql + - python: "3.2" + env: DJANGO=1.11 DB=sqlite + - python: "3.2" + env: DJANGO=1.11 DB=postgres + - python: "3.2" + env: DJANGO=1.11 DB=mysql + - python: "3.3" + env: DJANGO=1.11 DB=sqlite + - python: "3.3" + env: DJANGO=1.11 DB=postgres + - python: "3.3" + env: DJANGO=1.11 DB=mysql - python: "3.5" env: DJANGO=1.5 DB=sqlite - python: "3.5" @@ -171,6 +192,14 @@ matrix: env: DJANGO=1.10 DB=mysql - python: "3.5" env: DJANGO=1.10 DB=mysql + - python: "3.2" + env: DJANGO=1.11 DB=mysql + - python: "3.3" + env: DJANGO=1.11 DB=mysql + - python: "3.4" + env: DJANGO=1.11 DB=mysql + - python: "3.5" + env: DJANGO=1.11 DB=mysql before_install: - pip install -q 'flake8<3' - PYFLAKES_NODOCTEST=1 flake8 modeltranslation diff --git a/tox.ini b/tox.ini index 9cbb295c..c8b272d5 100644 --- a/tox.ini +++ b/tox.ini @@ -5,6 +5,9 @@ exclude = .tox,docs/modeltranslation/conf.py [tox] distribute = False envlist = + py35-1.11.X, + py34-1.11.X, + py27-1.11.X, py35-1.10.X, py34-1.10.X, py27-1.10.X, @@ -38,6 +41,24 @@ commands = {envpython} runtests.py +[testenv:py35-1.11.X] +basepython = python3.5 +deps = + Django>=1.11,<1.12 + Pillow + +[testenv:py34-1.11.X] +basepython = python3.4 +deps = + Django>=1.11,<1.12 + Pillow + +[testenv:py27-1.11.X] +basepython = python2.7 +deps = + Django>=1.11,<1.12 + Pillow + [testenv:py35-1.10.X] basepython = python3.5 deps = From 0dad9beefdc89d6fc80afdd872951a6575bfc303 Mon Sep 17 00:00:00 2001 From: Dirk Eschler Date: Wed, 5 Apr 2017 14:03:25 +0200 Subject: [PATCH 141/170] Prepared 0.12.1 release. --- CHANGELOG.txt | 8 ++++++++ PKG-INFO | 2 +- modeltranslation/__init__.py | 2 +- setup.py | 1 + 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index ec65ffde..8e6ec3bc 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,11 @@ +v0.12.1 +======= +Date: 2017-04-05 + + FIXED: Issue in loaddata management command in combination with Django 1.11. + (resolves issue #401) + + v0.12 ===== Date: 2016-09-20 diff --git a/PKG-INFO b/PKG-INFO index a65f6027..b9b05b2c 100644 --- a/PKG-INFO +++ b/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 1.0 Name: django-modeltranslation -Version: 0.12 +Version: 0.12.1 Summary: Translates Django models using a registration approach. Home-page: https://github.com/deschler/django-modeltranslation Author: Peter Eschler, diff --git a/modeltranslation/__init__.py b/modeltranslation/__init__.py index 26111c1c..5f006545 100644 --- a/modeltranslation/__init__.py +++ b/modeltranslation/__init__.py @@ -3,7 +3,7 @@ Version code adopted from Django development version. https://github.com/django/django """ -VERSION = (0, 12, 0, 'final', 0) +VERSION = (0, 12, 1, 'final', 0) default_app_config = 'modeltranslation.apps.ModeltranslationConfig' diff --git a/setup.py b/setup.py index 296361bf..158b6308 100755 --- a/setup.py +++ b/setup.py @@ -35,6 +35,7 @@ 'Programming Language :: Python :: 3.2', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', 'Operating System :: OS Independent', 'Environment :: Web Environment', 'Intended Audience :: Developers', From cc214e3c660970f6d60502c68d96bfca5dc83bc4 Mon Sep 17 00:00:00 2001 From: Dirk Eschler Date: Thu, 13 Apr 2017 10:40:28 +0200 Subject: [PATCH 142/170] Added Django 1.11 with Python 3.6. Dropped Django <1.8 and Python 2.6. --- .travis.yml | 141 ++-------------------------------------------------- tox.ini | 112 +++-------------------------------------- 2 files changed, 12 insertions(+), 241 deletions(-) diff --git a/.travis.yml b/.travis.yml index b839939a..25541a66 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,24 +1,11 @@ language: python python: - - "2.6" - "2.7" - - "3.2" - "3.3" - "3.4" - "3.5" + - "3.6" env: - - DJANGO=1.4 DB=sqlite - - DJANGO=1.4 DB=postgres - - DJANGO=1.4 DB=mysql - - DJANGO=1.5 DB=sqlite - - DJANGO=1.5 DB=postgres - - DJANGO=1.5 DB=mysql - - DJANGO=1.6 DB=sqlite - - DJANGO=1.6 DB=postgres - - DJANGO=1.6 DB=mysql - - DJANGO=1.7 DB=sqlite - - DJANGO=1.7 DB=postgres - - DJANGO=1.7 DB=mysql - DJANGO=1.8 DB=sqlite - DJANGO=1.8 DB=postgres - DJANGO=1.8 DB=mysql @@ -33,67 +20,6 @@ env: - DJANGO=1.11 DB=mysql matrix: exclude: - - python: "3.2" - env: DJANGO=1.4 DB=sqlite - - python: "3.2" - env: DJANGO=1.4 DB=postgres - - python: "3.2" - env: DJANGO=1.4 DB=mysql - - python: "3.3" - env: DJANGO=1.4 DB=sqlite - - python: "3.3" - env: DJANGO=1.4 DB=postgres - - python: "3.3" - env: DJANGO=1.4 DB=mysql - - python: "3.4" - env: DJANGO=1.4 DB=sqlite - - python: "3.4" - env: DJANGO=1.4 DB=postgres - - python: "3.4" - env: DJANGO=1.4 DB=mysql - - python: "3.5" - env: DJANGO=1.4 DB=sqlite - - python: "3.5" - env: DJANGO=1.4 DB=postgres - - python: "3.5" - env: DJANGO=1.4 DB=mysql - - - python: "2.6" - env: DJANGO=1.7 DB=sqlite - - python: "2.6" - env: DJANGO=1.7 DB=postgres - - python: "2.6" - env: DJANGO=1.7 DB=mysql - - python: "2.6" - env: DJANGO=1.8 DB=sqlite - - python: "2.6" - env: DJANGO=1.8 DB=postgres - - python: "2.6" - env: DJANGO=1.8 DB=mysql - - python: "2.6" - env: DJANGO=1.9 DB=sqlite - - python: "2.6" - env: DJANGO=1.9 DB=postgres - - python: "2.6" - env: DJANGO=1.9 DB=mysql - - python: "2.6" - env: DJANGO=1.10 DB=sqlite - - python: "2.6" - env: DJANGO=1.10 DB=postgres - - python: "2.6" - env: DJANGO=1.10 DB=mysql - - python: "2.6" - env: DJANGO=1.11 DB=sqlite - - python: "2.6" - env: DJANGO=1.11 DB=postgres - - python: "2.6" - env: DJANGO=1.11 DB=mysql - - - python: "3.2" - env: DJANGO=1.9 DB=sqlite - - python: "3.2" - env: DJANGO=1.9 DB=postgres - - python: "3.2" env: DJANGO=1.9 DB=mysql - python: "3.3" env: DJANGO=1.9 DB=sqlite @@ -101,75 +27,19 @@ matrix: env: DJANGO=1.9 DB=postgres - python: "3.3" env: DJANGO=1.9 DB=mysql - - python: "3.2" - env: DJANGO=1.10 DB=sqlite - - python: "3.2" - env: DJANGO=1.10 DB=postgres - - python: "3.2" - env: DJANGO=1.10 DB=mysql - python: "3.3" env: DJANGO=1.10 DB=sqlite - python: "3.3" env: DJANGO=1.10 DB=postgres - python: "3.3" env: DJANGO=1.10 DB=mysql - - python: "3.2" - env: DJANGO=1.11 DB=sqlite - - python: "3.2" - env: DJANGO=1.11 DB=postgres - - python: "3.2" - env: DJANGO=1.11 DB=mysql - python: "3.3" env: DJANGO=1.11 DB=sqlite - python: "3.3" env: DJANGO=1.11 DB=postgres - python: "3.3" env: DJANGO=1.11 DB=mysql - - python: "3.5" - env: DJANGO=1.5 DB=sqlite - - python: "3.5" - env: DJANGO=1.5 DB=postgres - - python: "3.5" - env: DJANGO=1.5 DB=mysql - - python: "3.5" - env: DJANGO=1.6 DB=sqlite - - python: "3.5" - env: DJANGO=1.6 DB=postgres - - python: "3.5" - env: DJANGO=1.6 DB=mysql - - python: "3.5" - env: DJANGO=1.7 DB=sqlite - - python: "3.5" - env: DJANGO=1.7 DB=postgres - - python: "3.5" - env: DJANGO=1.7 DB=mysql - - python: "3.2" - env: DJANGO=1.5 DB=mysql - - python: "3.3" - env: DJANGO=1.5 DB=mysql - - python: "3.4" - env: DJANGO=1.5 DB=mysql - - python: "3.5" - env: DJANGO=1.5 DB=mysql - - python: "3.2" - env: DJANGO=1.6 DB=mysql - - python: "3.3" - env: DJANGO=1.6 DB=mysql - - python: "3.4" - env: DJANGO=1.6 DB=mysql - - python: "3.5" - env: DJANGO=1.6 DB=mysql - - python: "3.2" - env: DJANGO=1.7 DB=mysql - - python: "3.3" - env: DJANGO=1.7 DB=mysql - - python: "3.4" - env: DJANGO=1.7 DB=mysql - - python: "3.5" - env: DJANGO=1.7 DB=mysql - - python: "3.2" - env: DJANGO=1.8 DB=mysql - python: "3.3" env: DJANGO=1.8 DB=mysql - python: "3.4" @@ -184,16 +54,16 @@ matrix: env: DJANGO=1.9 DB=mysql - python: "3.5" env: DJANGO=1.9 DB=mysql - - python: "3.2" - env: DJANGO=1.10 DB=mysql + - python: "3.6" + env: DJANGO=1.9 DB=mysql - python: "3.3" env: DJANGO=1.10 DB=mysql - python: "3.4" env: DJANGO=1.10 DB=mysql - python: "3.5" env: DJANGO=1.10 DB=mysql - - python: "3.2" - env: DJANGO=1.11 DB=mysql + - python: "3.6" + env: DJANGO=1.10 DB=mysql - python: "3.3" env: DJANGO=1.11 DB=mysql - python: "3.4" @@ -213,7 +83,6 @@ install: - IDJANGO=$(./travis.py $DJANGO) - pip install -q $IDJANGO - pip install -e . - - if [[ $TRAVIS_PYTHON_VERSION == '3.2' ]]; then pip install 'coverage<4.0.0'; fi - pip install -q coveralls script: - django-admin.py --version diff --git a/tox.ini b/tox.ini index c8b272d5..7bf0847b 100644 --- a/tox.ini +++ b/tox.ini @@ -5,6 +5,7 @@ exclude = .tox,docs/modeltranslation/conf.py [tox] distribute = False envlist = + py36-1.11.X, py35-1.11.X, py34-1.11.X, py27-1.11.X, @@ -17,22 +18,7 @@ envlist = py35-1.8.X, py34-1.8.X, py33-1.8.X, - py32-1.8.X, py27-1.8.X, - py34-1.7.X, - py33-1.7.X, - py32-1.7.X, - py27-1.7.X, - py33-1.6.X, - py32-1.6.X, - py27-1.6.X, - py26-1.6.X, - py33-1.5.X, - py32-1.5.X, - py27-1.5.X, - py26-1.5.X, - py27-1.4.X, - py26-1.4.X [testenv] downloadcache = {toxworkdir}/_download/ @@ -41,6 +27,12 @@ commands = {envpython} runtests.py +[testenv:py36-1.11.X] +basepython = python3.6 +deps = + Django>=1.11,<1.12 + Pillow + [testenv:py35-1.11.X] basepython = python3.5 deps = @@ -113,98 +105,8 @@ deps = Django>=1.8,<1.9 Pillow -[testenv:py32-1.8.X] -basepython = python3.2 -deps = - Django>=1.8,<1.9 - Pillow - [testenv:py27-1.8.X] basepython = python2.7 deps = Django>=1.8,<1.9 Pillow - -[testenv:py34-1.7.X] -basepython = python3.4 -deps = - Django>=1.7,<1.8 - Pillow - -[testenv:py33-1.7.X] -basepython = python3.3 -deps = - Django>=1.7,<1.8 - Pillow - -[testenv:py32-1.7.X] -basepython = python3.2 -deps = - Django>=1.7,<1.8 - Pillow - -[testenv:py27-1.7.X] -basepython = python2.7 -deps = - Django>=1.7,<1.8 - Pillow - -[testenv:py33-1.6.X] -basepython = python3.3 -deps = - Django>=1.6,<1.7 - Pillow - -[testenv:py32-1.6.X] -basepython = python3.2 -deps = - Django>=1.6,<1.7 - Pillow - -[testenv:py27-1.6.X] -basepython = python2.7 -deps = - Django>=1.6,<1.7 - Pillow - -[testenv:py26-1.6.X] -basepython = python2.6 -deps = - Django>=1.6,<1.7 - Pillow - -[testenv:py33-1.5.X] -basepython = python3.3 -deps = - Django>=1.5,<1.6 - Pillow - -[testenv:py32-1.5.X] -basepython = python3.2 -deps = - Django>=1.5,<1.6 - Pillow - -[testenv:py27-1.5.X] -basepython = python2.7 -deps = - Django>=1.5,<1.6 - Pillow - -[testenv:py26-1.5.X] -basepython = python2.6 -deps = - Django>=1.5,<1.6 - Pillow - -[testenv:py27-1.4.X] -basepython = python2.7 -deps = - Django>=1.4,<1.5 - Pillow - -[testenv:py26-1.4.X] -basepython = python2.6 -deps = - Django>=1.4,<1.5 - Pillow From 693f088a828223c20a6d3097d367b5206b924edd Mon Sep 17 00:00:00 2001 From: Dirk Eschler Date: Thu, 13 Apr 2017 10:42:54 +0200 Subject: [PATCH 143/170] Updated supported Django and Python version. --- setup.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 158b6308..45ea859a 100755 --- a/setup.py +++ b/setup.py @@ -25,17 +25,16 @@ 'modeltranslation.management.commands'], package_data={'modeltranslation': ['static/modeltranslation/css/*.css', 'static/modeltranslation/js/*.js']}, - requires=['django(>=1.4)'], + requires=['Django(>=1.8)'], download_url='https://github.com/deschler/django-modeltranslation/archive/%s.tar.gz' % version, classifiers=[ 'Programming Language :: Python', - 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.2', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', 'Operating System :: OS Independent', 'Environment :: Web Environment', 'Intended Audience :: Developers', From 86eb8ba530f5952eceb874451f4d274e7c6826ef Mon Sep 17 00:00:00 2001 From: Dirk Eschler Date: Thu, 13 Apr 2017 10:48:05 +0200 Subject: [PATCH 144/170] Fixed leftover. --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 25541a66..23f79065 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,7 +20,6 @@ env: - DJANGO=1.11 DB=mysql matrix: exclude: - env: DJANGO=1.9 DB=mysql - python: "3.3" env: DJANGO=1.9 DB=sqlite - python: "3.3" From 46488f1abce1e8f11b879dfa7fb43368a3b7f98b Mon Sep 17 00:00:00 2001 From: Dirk Eschler Date: Thu, 13 Apr 2017 12:02:38 +0200 Subject: [PATCH 145/170] Handled import_models args change for Django 1.11. --- modeltranslation/tests/tests.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/modeltranslation/tests/tests.py b/modeltranslation/tests/tests.py index 3f894ef7..13e11ca7 100644 --- a/modeltranslation/tests/tests.py +++ b/modeltranslation/tests/tests.py @@ -149,9 +149,15 @@ def setUpClass(cls): sys.modules.pop('modeltranslation.tests.translation', None) if MIGRATIONS: sys.modules.pop('django.contrib.auth.models', None) - cls.cache.get_app_config('tests').import_models(cls.cache.all_models['tests']) + tests_args = [] + if django.VERSION < (1, 11): + tests_args = [cls.cache.all_models['tests']] + cls.cache.get_app_config('tests').import_models(*tests_args) if MIGRATIONS: - cls.cache.get_app_config('auth').import_models(cls.cache.all_models['auth']) + auth_args = [] + if django.VERSION < (1, 11): + auth_args = [cls.cache.all_models['auth']] + cls.cache.get_app_config('auth').import_models(*auth_args) # 4. Autodiscover from modeltranslation.models import handle_translation_registrations From dbb9becd09bbb9808060272b74e664afc354dfa8 Mon Sep 17 00:00:00 2001 From: Dirk Eschler Date: Thu, 13 Apr 2017 12:05:38 +0200 Subject: [PATCH 146/170] Set empty MIGRATION_MODULES setting for Django 1.11. Should make the tests run again. --- modeltranslation/tests/settings.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/modeltranslation/tests/settings.py b/modeltranslation/tests/settings.py index 75d98037..bb0d6cf2 100644 --- a/modeltranslation/tests/settings.py +++ b/modeltranslation/tests/settings.py @@ -2,6 +2,7 @@ """ Settings overrided for test time """ +import django from django.conf import settings @@ -23,4 +24,8 @@ ROOT_URLCONF = 'modeltranslation.tests.urls' -MIGRATION_MODULES = {'auth': 'modeltranslation.tests.auth_migrations'} +if django.VERSION < (1, 11): + # TODO: Check what this was about + MIGRATION_MODULES = {'auth': 'modeltranslation.tests.auth_migrations'} +else: + MIGRATION_MODULES = {} From ab2f2e14f6259563045aaa4fec2444488e0209b0 Mon Sep 17 00:00:00 2001 From: Dirk Eschler Date: Thu, 13 Apr 2017 13:38:02 +0200 Subject: [PATCH 147/170] Don't exclude mysql for Python3 anymore, install mysqlclient instead. Set proper excludes for Python 3.3 and 3.6. --- .travis.yml | 55 ++++++++++++++++++++++------------------------------- 1 file changed, 23 insertions(+), 32 deletions(-) diff --git a/.travis.yml b/.travis.yml index 23f79065..7651ee02 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,55 +20,45 @@ env: - DJANGO=1.11 DB=mysql matrix: exclude: + - python: "3.6" + env: DJANGO=1.8 DB=sqlite + - python: "3.6" + env: DJANGO=1.8 DB=mysql + - python: "3.6" + env: DJANGO=1.8 DB=postgres + - python: "3.3" env: DJANGO=1.9 DB=sqlite - python: "3.3" - env: DJANGO=1.9 DB=postgres + env: DJANGO=1.9 DB=mysql - python: "3.3" + env: DJANGO=1.9 DB=postgres + - python: "3.6" + env: DJANGO=1.9 DB=sqlite + - python: "3.6" env: DJANGO=1.9 DB=mysql + - python: "3.6" + env: DJANGO=1.9 DB=postgres + - python: "3.3" env: DJANGO=1.10 DB=sqlite - python: "3.3" env: DJANGO=1.10 DB=postgres - python: "3.3" env: DJANGO=1.10 DB=mysql + - python: "3.6" + env: DJANGO=1.10 DB=sqlite + - python: "3.6" + env: DJANGO=1.10 DB=postgres + - python: "3.6" + env: DJANGO=1.10 DB=mysql + - python: "3.3" env: DJANGO=1.11 DB=sqlite - python: "3.3" env: DJANGO=1.11 DB=postgres - python: "3.3" env: DJANGO=1.11 DB=mysql - - - python: "3.3" - env: DJANGO=1.8 DB=mysql - - python: "3.4" - env: DJANGO=1.8 DB=mysql - - python: "3.5" - env: DJANGO=1.8 DB=mysql - - python: "3.2" - env: DJANGO=1.9 DB=mysql - - python: "3.3" - env: DJANGO=1.9 DB=mysql - - python: "3.4" - env: DJANGO=1.9 DB=mysql - - python: "3.5" - env: DJANGO=1.9 DB=mysql - - python: "3.6" - env: DJANGO=1.9 DB=mysql - - python: "3.3" - env: DJANGO=1.10 DB=mysql - - python: "3.4" - env: DJANGO=1.10 DB=mysql - - python: "3.5" - env: DJANGO=1.10 DB=mysql - - python: "3.6" - env: DJANGO=1.10 DB=mysql - - python: "3.3" - env: DJANGO=1.11 DB=mysql - - python: "3.4" - env: DJANGO=1.11 DB=mysql - - python: "3.5" - env: DJANGO=1.11 DB=mysql before_install: - pip install -q 'flake8<3' - PYFLAKES_NODOCTEST=1 flake8 modeltranslation @@ -77,6 +67,7 @@ before_script: - psql -c 'create database modeltranslation;' -U postgres install: - if [[ $DB == mysql ]]; then pip install -q mysql-python; fi + - if [[ $DB == mysql ]] && [[ ${PYTHON:0:1} == "2" ]]; then echo "mysql-python"; elif [[ $DB == mysql ]] && [[ ${PYTHON:0:1} == "3" ]]; then echo "mysqlclient"; fi - if [[ $DB == postgres ]]; then pip install -q psycopg2; fi - pip install -q Pillow - IDJANGO=$(./travis.py $DJANGO) From 23cd10432b2d03425d8f44fbe761d0756ca303a1 Mon Sep 17 00:00:00 2001 From: Dirk Eschler Date: Thu, 13 Apr 2017 13:45:13 +0200 Subject: [PATCH 148/170] Fixed leftover. --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 7651ee02..7033524b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -66,7 +66,6 @@ before_script: - mysql -e 'create database modeltranslation;' - psql -c 'create database modeltranslation;' -U postgres install: - - if [[ $DB == mysql ]]; then pip install -q mysql-python; fi - if [[ $DB == mysql ]] && [[ ${PYTHON:0:1} == "2" ]]; then echo "mysql-python"; elif [[ $DB == mysql ]] && [[ ${PYTHON:0:1} == "3" ]]; then echo "mysqlclient"; fi - if [[ $DB == postgres ]]; then pip install -q psycopg2; fi - pip install -q Pillow From c4f146cc499c60d0a1f9d9ffabd279756fb606f4 Mon Sep 17 00:00:00 2001 From: Dirk Eschler Date: Thu, 13 Apr 2017 14:08:15 +0200 Subject: [PATCH 149/170] Fixed pip installation of mysql module. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 7033524b..de8455e9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -66,7 +66,7 @@ before_script: - mysql -e 'create database modeltranslation;' - psql -c 'create database modeltranslation;' -U postgres install: - - if [[ $DB == mysql ]] && [[ ${PYTHON:0:1} == "2" ]]; then echo "mysql-python"; elif [[ $DB == mysql ]] && [[ ${PYTHON:0:1} == "3" ]]; then echo "mysqlclient"; fi + - if [[ $DB == mysql ]] && [[ ${PYTHON:0:1} == "2" ]]; then pip install -q mysql-python; elif [[ $DB == mysql ]] && [[ ${PYTHON:0:1} == "3" ]]; then pip install -q mysqlclient; fi - if [[ $DB == postgres ]]; then pip install -q psycopg2; fi - pip install -q Pillow - IDJANGO=$(./travis.py $DJANGO) From 44139c30b3eca38467db7d31ffee309670012d07 Mon Sep 17 00:00:00 2001 From: Dirk Eschler Date: Thu, 13 Apr 2017 15:00:35 +0200 Subject: [PATCH 150/170] Set python version env variable. --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index de8455e9..d59a014f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -66,6 +66,7 @@ before_script: - mysql -e 'create database modeltranslation;' - psql -c 'create database modeltranslation;' -U postgres install: + - PYTHON=`python -c 'import sys; version=sys.version_info[:3]; print("{0}.{1}".format(*version))'` - if [[ $DB == mysql ]] && [[ ${PYTHON:0:1} == "2" ]]; then pip install -q mysql-python; elif [[ $DB == mysql ]] && [[ ${PYTHON:0:1} == "3" ]]; then pip install -q mysqlclient; fi - if [[ $DB == postgres ]]; then pip install -q psycopg2; fi - pip install -q Pillow From 45097424e421c99799d776e45c98f820545fa8f2 Mon Sep 17 00:00:00 2001 From: Dirk Eschler Date: Thu, 13 Apr 2017 17:35:13 +0200 Subject: [PATCH 151/170] Added Django 1.11 and Python 3.6 to the support matrix. --- docs/modeltranslation/installation.rst | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/modeltranslation/installation.rst b/docs/modeltranslation/installation.rst index cec291fd..269a0b3d 100644 --- a/docs/modeltranslation/installation.rst +++ b/docs/modeltranslation/installation.rst @@ -8,20 +8,21 @@ Requirements Which Modeltranslation version is required for given Django-Python combination to work? -======= ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== +======= ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== Python Django -------- ------------------------------------------------------ -version 1.0 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 1.9 1.10 -======= ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== +------- ----------------------------------------------------------- +version 1.0 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 1.9 1.10 1.11 +======= ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== 2.4 |u3| |u3| |u3| |u3| 2.5 |u3| |u3| |u3| |36| |36| 2.6 |u3| |u3| |u3| |36| |3| |5| |7| -2.7 |u3| |u3| |u3| |36| |3| |5| |7| |8| |9| |11| |12| +2.7 |u3| |u3| |u3| |36| |3| |5| |7| |8| |9| |11| |12| |13| 3.2 |7| |7| |8| |9| 3.3 |7| |7| |8| |9| -3.4 |8| |9| |11| |12| -3.5 |9| |11| |12| -======= ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== +3.4 |8| |9| |11| |12| |13| +3.5 |9| |11| |12| |13| +3.6 |13| +======= ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== (``-X`` denotes "up to version X", whereas ``X+`` means "from version X upwards") @@ -34,6 +35,7 @@ version 1.0 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 1.9 1.10 .. |9| replace:: 0.9+ .. |11| replace:: 0.11+ .. |12| replace:: 0.12+ +.. |13| replace:: 0.13+ Using Pip From 60c632fc6a320092a89c4bf79d952735ff0b4ecc Mon Sep 17 00:00:00 2001 From: Benjamin Toueg Date: Tue, 27 Dec 2016 17:43:45 +0100 Subject: [PATCH 152/170] Fix order_by with expression Fix #375 --- modeltranslation/manager.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/modeltranslation/manager.py b/modeltranslation/manager.py index 271e8390..d7a974e7 100644 --- a/modeltranslation/manager.py +++ b/modeltranslation/manager.py @@ -351,7 +351,11 @@ def order_by(self, *field_names): return super(MultilingualQuerySet, self).order_by(*field_names) new_args = [] for key in field_names: - new_args.append(rewrite_order_lookup_key(self.model, key)) + try: + new_arg = rewrite_order_lookup_key(self.model, key) + except AttributeError: + new_arg = key + new_args.append(new_arg) return super(MultilingualQuerySet, self).order_by(*new_args) def update(self, **kwargs): From 0b7d356f236bb8f6b89b3457cb6908f29506d61d Mon Sep 17 00:00:00 2001 From: Benjamin Toueg Date: Mon, 2 Jan 2017 10:39:53 +0100 Subject: [PATCH 153/170] Fix `.select_related(None)` --- modeltranslation/manager.py | 58 +++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/modeltranslation/manager.py b/modeltranslation/manager.py index d7a974e7..7ab6c07e 100644 --- a/modeltranslation/manager.py +++ b/modeltranslation/manager.py @@ -48,25 +48,28 @@ def get_translatable_fields_for_model(model): def rewrite_lookup_key(model, lookup_key): - pieces = lookup_key.split('__', 1) - original_key = pieces[0] - - translatable_fields = get_translatable_fields_for_model(model) - if translatable_fields is not None: - # 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: - pieces[0] = build_localized_fieldname(pieces[0], get_language()) - - 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) - # Check ``original key``, as pieces[0] may have been already rewritten. - if original_key in fields_to_trans_models: - transmodel = fields_to_trans_models[original_key] - pieces[1] = rewrite_lookup_key(transmodel, pieces[1]) - return '__'.join(pieces) + try: + pieces = lookup_key.split('__', 1) + original_key = pieces[0] + + translatable_fields = get_translatable_fields_for_model(model) + if translatable_fields is not None: + # 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: + pieces[0] = build_localized_fieldname(pieces[0], get_language()) + + 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) + # Check ``original key``, as pieces[0] may have been already rewritten. + if original_key in fields_to_trans_models: + transmodel = fields_to_trans_models[original_key] + pieces[1] = rewrite_lookup_key(transmodel, pieces[1]) + return '__'.join(pieces) + except AttributeError: + return lookup_key def append_fallback(model, fields): @@ -121,10 +124,13 @@ def append_lookup_keys(model, fields): def rewrite_order_lookup_key(model, lookup_key): - if lookup_key.startswith('-'): - return '-' + rewrite_lookup_key(model, lookup_key[1:]) - else: - return rewrite_lookup_key(model, lookup_key) + try: + if lookup_key.startswith('-'): + return '-' + rewrite_lookup_key(model, lookup_key[1:]) + else: + return rewrite_lookup_key(model, lookup_key) + except AttributeError: + return lookup_key _F2TM_CACHE = {} @@ -351,11 +357,7 @@ def order_by(self, *field_names): return super(MultilingualQuerySet, self).order_by(*field_names) new_args = [] for key in field_names: - try: - new_arg = rewrite_order_lookup_key(self.model, key) - except AttributeError: - new_arg = key - new_args.append(new_arg) + new_args.append(rewrite_order_lookup_key(self.model, key)) return super(MultilingualQuerySet, self).order_by(*new_args) def update(self, **kwargs): From ff189d68f521881a042906bf145115590a3c627a Mon Sep 17 00:00:00 2001 From: Benjamin Toueg Date: Tue, 3 Jan 2017 10:46:46 +0100 Subject: [PATCH 154/170] Fix `.defer(None)` --- modeltranslation/manager.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/modeltranslation/manager.py b/modeltranslation/manager.py index 7ab6c07e..d56c895b 100644 --- a/modeltranslation/manager.py +++ b/modeltranslation/manager.py @@ -120,7 +120,15 @@ def append_lookup_key(model, lookup_key): def append_lookup_keys(model, fields): - return moves.reduce(set.union, (append_lookup_key(model, field) for field in fields), set()) + new_fields = [] + for field in fields: + try: + new_field = append_lookup_key(model, field) + except AttributeError: + new_field = (field,) + new_fields.append(new_field) + + return moves.reduce(set.union, new_fields, set()) def rewrite_order_lookup_key(model, lookup_key): From 7f269b61f81258e370281d4e72fae808ed2d908c Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Thu, 22 Jun 2017 20:56:14 +0200 Subject: [PATCH 155/170] Added DeprecationWarning display. --- runtests.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/runtests.py b/runtests.py index c6667e79..5f688e2b 100755 --- a/runtests.py +++ b/runtests.py @@ -1,6 +1,7 @@ #!/usr/bin/env python import os import sys +import warnings import django from django.conf import settings @@ -47,6 +48,7 @@ def runtests(): MIDDLEWARE_CLASSES=(), ) + warnings.simplefilter('always', DeprecationWarning) if django.VERSION >= (1, 7): django.setup() failures = call_command( From 7a823ea474c3ac6586dc91d60cd8efa17532e140 Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Thu, 22 Jun 2017 22:03:07 +0200 Subject: [PATCH 156/170] Added required on_delete attribute to OneToOneField and ForeignKey fields. --- modeltranslation/tests/models.py | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/modeltranslation/tests/models.py b/modeltranslation/tests/models.py index 67ee24b2..a965f921 100644 --- a/modeltranslation/tests/models.py +++ b/modeltranslation/tests/models.py @@ -60,19 +60,31 @@ class NonTranslated(models.Model): class ForeignKeyModel(models.Model): title = models.CharField(ugettext_lazy('title'), max_length=255) - test = models.ForeignKey(TestModel, null=True, related_name="test_fks") - optional = models.ForeignKey(TestModel, blank=True, null=True) - hidden = models.ForeignKey(TestModel, blank=True, null=True, related_name="+") - non = models.ForeignKey(NonTranslated, blank=True, null=True, related_name="test_fks") - untrans = models.ForeignKey(TestModel, blank=True, null=True, related_name="test_fks_un") + test = models.ForeignKey( + TestModel, null=True, related_name="test_fks", on_delete=models.CASCADE, + ) + optional = models.ForeignKey(TestModel, blank=True, null=True, on_delete=models.CASCADE) + hidden = models.ForeignKey( + TestModel, blank=True, null=True, related_name="+", on_delete=models.CASCADE, + ) + non = models.ForeignKey( + NonTranslated, blank=True, null=True, related_name="test_fks", on_delete=models.CASCADE, + ) + untrans = models.ForeignKey( + TestModel, blank=True, null=True, related_name="test_fks_un", on_delete=models.CASCADE, + ) 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) + test = models.OneToOneField( + TestModel, null=True, related_name="test_o2o", on_delete=models.CASCADE, + ) + optional = models.OneToOneField(TestModel, blank=True, null=True, on_delete=models.CASCADE) # No hidden option for OneToOne - non = models.OneToOneField(NonTranslated, blank=True, null=True, related_name="test_o2o") + non = models.OneToOneField( + NonTranslated, blank=True, null=True, related_name="test_o2o", on_delete=models.CASCADE, + ) # ######### Custom fields testing From b64ddf2e200292a1bb9c327706650a996d3856d9 Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Thu, 22 Jun 2017 22:15:37 +0200 Subject: [PATCH 157/170] Used model attribute instead of deprecated ForeignObjectRel.to. --- modeltranslation/fields.py | 4 ++-- modeltranslation/translator.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/modeltranslation/fields.py b/modeltranslation/fields.py index 7172eb46..9ded57b2 100644 --- a/modeltranslation/fields.py +++ b/modeltranslation/fields.py @@ -182,8 +182,8 @@ def __init__(self, translated_field, language, empty_value, *args, **kwargs): self.related_query_name = lambda: loc_related_query_name self.remote_field.related_name = build_localized_fieldname(current, self.language) self.remote_field.field = self # Django 1.6 - if hasattr(self.remote_field.to._meta, '_related_objects_cache'): - del self.remote_field.to._meta._related_objects_cache + if hasattr(self.remote_field.model._meta, '_related_objects_cache'): + del self.remote_field.model._meta._related_objects_cache # Django 1.5 changed definition of __hash__ for fields to be fine with hash requirements. # It spoiled our machinery, since TranslationField has the same creation_counter as its diff --git a/modeltranslation/translator.py b/modeltranslation/translator.py index 11880271..bcca0a5b 100644 --- a/modeltranslation/translator.py +++ b/modeltranslation/translator.py @@ -529,11 +529,11 @@ def _register_single_model(self, model, opts): # Set related field names on other model if NEW_RELATED_API and not field.remote_field.is_hidden(): - other_opts = self._get_options_for_model(field.remote_field.to) + other_opts = self._get_options_for_model(field.remote_field.model) other_opts.related = True other_opts.related_fields.append(field.related_query_name()) # Add manager in case of non-registered model - add_manager(field.remote_field.to) + add_manager(field.remote_field.model) elif not NEW_RELATED_API and not field.rel.is_hidden(): other_opts = self._get_options_for_model(field.rel.to) other_opts.related = True @@ -543,7 +543,7 @@ def _register_single_model(self, model, opts): if isinstance(field, OneToOneField): # Fix translated_field caching for SingleRelatedObjectDescriptor sro_descriptor = ( - getattr(field.remote_field.to, field.remote_field.get_accessor_name()) + getattr(field.remote_field.model, field.remote_field.get_accessor_name()) if NEW_RELATED_API else getattr(field.rel.to, field.related.get_accessor_name())) patch_related_object_descriptor_caching(sro_descriptor) From c93857c6233cb643a05ae36784c588ecc5b3da86 Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Thu, 22 Jun 2017 22:20:06 +0200 Subject: [PATCH 158/170] Used set() instead of direct assignment to the reverse side of a related set. --- modeltranslation/tests/tests.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/modeltranslation/tests/tests.py b/modeltranslation/tests/tests.py index 13e11ca7..f94b78b6 100644 --- a/modeltranslation/tests/tests.py +++ b/modeltranslation/tests/tests.py @@ -989,9 +989,12 @@ def test_reverse_relations(self): trans_real.activate("de") test_inst2 = models.TestModel(title_en='title_en', title_de='title_de') test_inst2.save() - test_inst2.test_fks = [fk_inst_de, fk_inst_both] - test_inst2.test_fks_en = (fk_inst_en, fk_inst_both) - + if django.VERSION >= (1, 9): + test_inst2.test_fks.set((fk_inst_de, fk_inst_both)) + test_inst2.test_fks_en.set((fk_inst_en, fk_inst_both)) + else: + test_inst2.test_fks = [fk_inst_de, fk_inst_both] + test_inst2.test_fks_en = (fk_inst_en, fk_inst_both) self.assertEqual(fk_inst_both.test.pk, test_inst2.pk) self.assertEqual(fk_inst_both.test_id, test_inst2.pk) self.assertEqual(fk_inst_both.test_de, test_inst2) From 5f66a44649bf34ffba5dcca066603a562ce6329e Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Fri, 23 Jun 2017 06:37:22 +0200 Subject: [PATCH 159/170] Removed unused code. Unused since cc214e3c660970f6d60502c68d96bfca5dc83bc4. --- modeltranslation/manager.py | 15 ++------------- modeltranslation/models.py | 16 +++------------- modeltranslation/tests/__init__.py | 2 -- modeltranslation/tests/tests.py | 23 +++++++---------------- modeltranslation/widgets.py | 10 ---------- runtests.py | 5 +---- 6 files changed, 13 insertions(+), 58 deletions(-) diff --git a/modeltranslation/manager.py b/modeltranslation/manager.py index 271e8390..b8e03305 100644 --- a/modeltranslation/manager.py +++ b/modeltranslation/manager.py @@ -543,13 +543,6 @@ def _clone(self, *args, **kwargs): return clone -def get_queryset(obj): - if hasattr(obj, 'get_queryset'): - return obj.get_queryset() - else: # Django 1.4 / 1.5 compat - return obj.get_query_set() - - def multilingual_queryset_factory(old_cls, instantiate=True): if old_cls == models.query.QuerySet: NewClass = MultilingualQuerySet @@ -566,7 +559,7 @@ class MultilingualQuerysetManager(models.Manager): get_queryset returns MultilingualQuerySet. """ def get_queryset(self): - qs = get_queryset(super(MultilingualQuerysetManager, self)) + qs = super(MultilingualQuerysetManager, self).get_queryset() return self._patch_queryset(qs) def _patch_queryset(self, qs): @@ -575,8 +568,6 @@ def _patch_queryset(self, qs): qs._rewrite_applied_operations() return qs - get_query_set = get_queryset - class MultilingualManager(MultilingualQuerysetManager): use_for_related_fields = True @@ -595,11 +586,9 @@ def get_queryset(self): This method is repeated because some managers that don't use super() or alter queryset class may return queryset that is not subclass of MultilingualQuerySet. """ - qs = get_queryset(super(MultilingualManager, self)) + qs = super(MultilingualManager, self).get_queryset() if isinstance(qs, MultilingualQuerySet): # Is already patched by MultilingualQuerysetManager - in most of the cases # when custom managers use super() properly in get_queryset. return qs return self._patch_queryset(qs) - - get_query_set = get_queryset diff --git a/modeltranslation/models.py b/modeltranslation/models.py index a54c835b..bc018bb4 100644 --- a/modeltranslation/models.py +++ b/modeltranslation/models.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -import django def autodiscover(): @@ -11,18 +10,13 @@ def autodiscover(): import os import sys import copy - from django.conf import settings from django.utils.module_loading import module_has_submodule from modeltranslation.translator import translator from modeltranslation.settings import TRANSLATION_FILES, DEBUG - if django.VERSION < (1, 7): - from django.utils.importlib import import_module - mods = [(app, import_module(app)) for app in settings.INSTALLED_APPS] - else: - from importlib import import_module - from django.apps import apps - mods = [(app_config.name, app_config.module) for app_config in apps.get_app_configs()] + from importlib import import_module + from django.apps import apps + mods = [(app_config.name, app_config.module) for app_config in apps.get_app_configs()] for (app, mod) in mods: # Attempt to import the app's translation module. @@ -79,7 +73,3 @@ def handle_translation_registrations(*args, **kwargs): # Trigger autodiscover, causing any TranslationOption initialization # code to execute. autodiscover() - - -if django.VERSION < (1, 7): - handle_translation_registrations() diff --git a/modeltranslation/tests/__init__.py b/modeltranslation/tests/__init__.py index 47945aff..e69de29b 100644 --- a/modeltranslation/tests/__init__.py +++ b/modeltranslation/tests/__init__.py @@ -1,2 +0,0 @@ -# For Django < 1.6 testrunner -from .tests import * # NOQA diff --git a/modeltranslation/tests/tests.py b/modeltranslation/tests/tests.py index 13e11ca7..1e0c45b7 100644 --- a/modeltranslation/tests/tests.py +++ b/modeltranslation/tests/tests.py @@ -1528,18 +1528,11 @@ def test_dates_queryset(self): qs = Model.objects.dates('datetime', 'year', 'DESC') - if django.VERSION[:2] < (1, 6): - self.assertEqual(list(qs), [ - datetime.datetime(2015, 1, 1, 0, 0), - datetime.datetime(2014, 1, 1, 0, 0), - datetime.datetime(2013, 1, 1, 0, 0) - ]) - else: - self.assertEqual(list(qs), [ - datetime.date(2015, 1, 1), - datetime.date(2014, 1, 1), - datetime.date(2013, 1, 1) - ]) + self.assertEqual(list(qs), [ + datetime.date(2015, 1, 1), + datetime.date(2014, 1, 1), + datetime.date(2013, 1, 1) + ]) def test_descriptors(self): # Descriptor store ints in database and returns string of 'a' of that length @@ -2427,8 +2420,7 @@ def test_form(self): class CreationForm(forms.ModelForm): class Meta: model = self.model - if django.VERSION >= (1, 6): - fields = '__all__' + fields = '__all__' creation_form = CreationForm({'name': 'abc'}) inst = creation_form.save() @@ -3004,8 +2996,7 @@ def test_fields(self): class TestModelForm(TranslationModelForm): class Meta: model = models.TestModel - if django.VERSION >= (1, 6): - fields = '__all__' + fields = '__all__' form = TestModelForm() self.assertEqual(list(form.base_fields), diff --git a/modeltranslation/widgets.py b/modeltranslation/widgets.py index 6b98186e..942d786f 100644 --- a/modeltranslation/widgets.py +++ b/modeltranslation/widgets.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals -from django import VERSION from django.forms.widgets import Media, Widget, CheckboxInput from django.utils.html import conditional_escape from django.utils.safestring import mark_safe @@ -85,15 +84,6 @@ def value_from_datadict(self, data, files, name): return self.empty_value return self.widget.value_from_datadict(data, files, name) - if VERSION < (1, 6): # In Django 1.6 formfields should implement _has_changed - def _has_changed(self, initial, data): - """ - Widget implementation equates ``None``s with empty strings. - """ - if (initial is None and data is not None) or (initial is not None and data is None): - return True - return self.widget._has_changed(initial, data) - def clear_checkbox_name(self, name): """ Given the name of the input, returns the name of the clear checkbox. diff --git a/runtests.py b/runtests.py index c6667e79..57b1927f 100755 --- a/runtests.py +++ b/runtests.py @@ -29,8 +29,6 @@ def runtests(): 'USER': 'postgres', 'NAME': 'modeltranslation', }) - if django.VERSION < (1, 6): - DATABASES['default']['OPTIONS'] = {'autocommit': True} # Configure test environment settings.configure( @@ -47,8 +45,7 @@ def runtests(): MIDDLEWARE_CLASSES=(), ) - if django.VERSION >= (1, 7): - django.setup() + django.setup() failures = call_command( 'test', 'modeltranslation', interactive=False, failfast=False, verbosity=2) From a7b484d10aa16003a7d912332744699fbbc23014 Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Fri, 23 Jun 2017 21:10:23 +0200 Subject: [PATCH 160/170] Used Meta.base_manager_name on model instead of Manager.use_for_related_fields. --- modeltranslation/manager.py | 4 +++- modeltranslation/translator.py | 10 ++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/modeltranslation/manager.py b/modeltranslation/manager.py index b8e03305..08d3adcc 100644 --- a/modeltranslation/manager.py +++ b/modeltranslation/manager.py @@ -7,6 +7,7 @@ """ import itertools +import django from django.db import models from django.db.models import FieldDoesNotExist try: @@ -570,7 +571,8 @@ def _patch_queryset(self, qs): class MultilingualManager(MultilingualQuerysetManager): - use_for_related_fields = True + if django.VERSION < (1, 10): + use_for_related_fields = True def rewrite(self, *args, **kwargs): return self.get_queryset().rewrite(*args, **kwargs) diff --git a/modeltranslation/translator.py b/modeltranslation/translator.py index bcca0a5b..a773edc1 100644 --- a/modeltranslation/translator.py +++ b/modeltranslation/translator.py @@ -194,8 +194,12 @@ def patch_manager_class(manager): else: class NewMultilingualManager(MultilingualManager, manager.__class__, MultilingualQuerysetManager): - use_for_related_fields = getattr( - manager.__class__, "use_for_related_fields", not has_custom_queryset(manager)) + if VERSION < (1, 10): + use_for_related_fields = getattr( + manager.__class__, + "use_for_related_fields", + not has_custom_queryset(manager), + ) _old_module = manager.__module__ _old_class = manager.__class__.__name__ @@ -224,6 +228,8 @@ def deconstruct(self): # share the same class. model._default_manager.__class__ = current_manager.__class__ patch_manager_class(model._base_manager) + if VERSION >= (1, 10): + model._meta.base_manager_name = 'objects' if hasattr(model._meta, "_expire_cache"): model._meta._expire_cache() From 74929c20552b247df14bbcfdb8b714eb3124e9aa Mon Sep 17 00:00:00 2001 From: Tocho Tochev Date: Tue, 25 Jul 2017 13:31:57 +0300 Subject: [PATCH 161/170] Fix reset of .select_related --- modeltranslation/manager.py | 5 ++++- modeltranslation/tests/tests.py | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/modeltranslation/manager.py b/modeltranslation/manager.py index 08d3adcc..0d41d4be 100644 --- a/modeltranslation/manager.py +++ b/modeltranslation/manager.py @@ -255,7 +255,10 @@ def select_related(self, *fields, **kwargs): # new_args = append_lookup_keys(self.model, fields) new_args = [] for key in fields: - new_args.append(rewrite_lookup_key(self.model, key)) + if key is None: + new_args.append(None) + else: + new_args.append(rewrite_lookup_key(self.model, key)) return super(MultilingualQuerySet, self).select_related(*new_args, **kwargs) # This method was not present in django-linguo diff --git a/modeltranslation/tests/tests.py b/modeltranslation/tests/tests.py index ce7d92ce..678d59fe 100644 --- a/modeltranslation/tests/tests.py +++ b/modeltranslation/tests/tests.py @@ -2966,6 +2966,21 @@ def test_deferred_rule2(self): o.title = "bla" self.assertEqual(o.title, "bla") + def test_select_related(self): + test = models.TestModel.objects.create(title_de='title_de', title_en='title_en') + with auto_populate('all'): + models.ForeignKeyModel.objects.create(untrans=test) + + fk_qs = models.ForeignKeyModel.objects.all() + self.assertNotIn('_untrans_cache', fk_qs[0].__dict__) + self.assertIn('_untrans_cache', fk_qs.select_related('untrans')[0].__dict__) + self.assertNotIn( + '_untrans_cache', + fk_qs.select_related('untrans').select_related(None)[0].__dict__ + ) + # untrans is nullable so not included when select_related=True + self.assertNotIn('_untrans_cache', fk_qs.select_related()[0].__dict__) + def test_translation_fields_appending(self): from modeltranslation.manager import append_lookup_keys, append_lookup_key self.assertEqual(set(['untrans']), append_lookup_key(models.ForeignKeyModel, 'untrans')) From f64dd560026372c95afd65b7a80d4250d4d895cd Mon Sep 17 00:00:00 2001 From: Donald Harvey Date: Sat, 9 Sep 2017 12:09:35 +0100 Subject: [PATCH 162/170] Support django-nested-admin stacked inlines --- .../static/modeltranslation/js/tabbed_translation_fields.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/modeltranslation/static/modeltranslation/js/tabbed_translation_fields.js b/modeltranslation/static/modeltranslation/js/tabbed_translation_fields.js index 993cd2e2..0b3c4de5 100644 --- a/modeltranslation/static/modeltranslation/js/tabbed_translation_fields.js +++ b/modeltranslation/static/modeltranslation/js/tabbed_translation_fields.js @@ -187,7 +187,10 @@ var google, django, gettext; // TODO: Refactor $('.mt').parents('.inline-group').not('.tabular').find('.add-row a').click(function () { var grouper = new TranslationFieldGrouper({ - $fields: $(this).parent().prev().prev().find('.mt') + $fields: $(this).parent().prev().prev().find('.mt').add( + // Support django-nested-admin stacked inlines + $(this).parent().prev('.djn-items').children('.djn-item').last().find('.mt') + ) }); var tabs = createTabs(grouper.groupedTranslations); // Update the main switch as it is not aware of the newly created tabs From 0207102972accea8cc0eebe9f2bd1038660d6786 Mon Sep 17 00:00:00 2001 From: Mikkel Munch Mortensen <3xm@detfalskested.dk> Date: Sun, 10 Sep 2017 11:11:28 +0200 Subject: [PATCH 163/170] Add section about django-audit log Improve the documentation by describing how to use `django-modeltranslation` in combination with `django-audit-log`. --- docs/modeltranslation/caveats.rst | 34 +++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/docs/modeltranslation/caveats.rst b/docs/modeltranslation/caveats.rst index ba999d34..2939e21e 100644 --- a/docs/modeltranslation/caveats.rst +++ b/docs/modeltranslation/caveats.rst @@ -29,3 +29,37 @@ which guarantees that the returned language is listed in the ``LANGUAGES`` setti The unittests use the ``django.utils.translation.trans_real`` functions to activate and deactive a specific language outside a view function. + +Using in combination with ``django-audit-log`` +---------------------------------------------- + +``django-audit-log`` is a package that allows you to track changes to your +model instances (`documentation`_). As ``django-audit-log`` behind the scenes +automatically creates "shadow" models for your tracked models, you have to +remember to register these shadow models for translation as well as your +regular models. Here's an example: + +.. code:: python + + from modeltranslation.translator import register, TranslationOptions + + from my_app import models + + + @register(models.MyModel) + @register(models.MyModel.audit_log.model) + class MyModelTranslationOptions(TranslationOptions): + """Translation options for MyModel.""" + + fields = ( + 'text', + 'title', + ) + +If you forget to register the shadow models, you will get an error like: + +.. code:: + + TypeError: 'text_es' is an invalid keyword argument for this function + +.. _documentation: https://django-audit-log.readthedocs.io/ From 84577823808b6143876c1021bd3b02ac2f5e1780 Mon Sep 17 00:00:00 2001 From: Florian Demmer Date: Wed, 11 Jan 2017 17:01:38 +0100 Subject: [PATCH 164/170] Get related model from field's path_info related_model is a cached property, that contains a string instead of the model eg. when overriding the default User model. --- modeltranslation/manager.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/modeltranslation/manager.py b/modeltranslation/manager.py index 0d41d4be..42745e5a 100644 --- a/modeltranslation/manager.py +++ b/modeltranslation/manager.py @@ -8,6 +8,7 @@ import itertools import django +from django.contrib.admin.utils import get_model_from_relation from django.db import models from django.db.models import FieldDoesNotExist try: @@ -142,8 +143,9 @@ def get_fields_to_translatable_models(model): # In that case the 'related_model' attribute is set to None # so it is necessary to check for this value before trying to # get translatable fields. - if get_translatable_fields_for_model(f.related_model) is not None: - results.append((f.name, f.related_model)) + related_model = get_model_from_relation(f) + if get_translatable_fields_for_model(related_model) is not None: + results.append((f.name, related_model)) else: for field_name in model._meta.get_all_field_names(): field_object, modelclass, direct, m2m = model._meta.get_field_by_name(field_name) From 4ec3a4b4b01d6758b31b02f0f661c1ec30963726 Mon Sep 17 00:00:00 2001 From: Florian Demmer Date: Mon, 18 Sep 2017 01:17:27 +0200 Subject: [PATCH 165/170] Fix import with django < 1.7 --- modeltranslation/manager.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/modeltranslation/manager.py b/modeltranslation/manager.py index 42745e5a..8388dd81 100644 --- a/modeltranslation/manager.py +++ b/modeltranslation/manager.py @@ -8,7 +8,11 @@ import itertools import django -from django.contrib.admin.utils import get_model_from_relation +try: + from django.contrib.admin.utils import get_model_from_relation +except ImportError: + from django.contrib.admin.util import get_model_from_relation + from django.db import models from django.db.models import FieldDoesNotExist try: From 019d116434356bdc5cad731204ec56cd80b95b84 Mon Sep 17 00:00:00 2001 From: Thomas Jost Date: Tue, 26 Sep 2017 15:26:27 +0200 Subject: [PATCH 166/170] Add (failing) tests for inherited managers and M2M relationships With several level of inheritance, custom managers with custom querysets, and a ManyToMany field with an explicit intermediary table. This reproduces bugs #389 and #413. --- modeltranslation/tests/models.py | 58 +++++++++++++++++++++++++++ modeltranslation/tests/tests.py | 30 +++++++++++++- modeltranslation/tests/translation.py | 14 ++++++- 3 files changed, 100 insertions(+), 2 deletions(-) diff --git a/modeltranslation/tests/models.py b/modeltranslation/tests/models.py index a965f921..aa281b31 100644 --- a/modeltranslation/tests/models.py +++ b/modeltranslation/tests/models.py @@ -351,3 +351,61 @@ class MultitableConflictModelA(models.Model): class MultitableConflictModelB(MultitableConflictModelA): title = models.CharField(ugettext_lazy('title'), max_length=255) + + +# ######### Complex M2M with abstract classes and custom managers + +class CustomQuerySetX(models.query.QuerySet): + pass + + +class CustomManagerX(models.Manager): + def get_queryset(self): + return CustomQuerySetX(self.model, using=self._db) + get_query_set = get_queryset + + +class AbstractBaseModelX(models.Model): + name = models.CharField(max_length=255) + objects = CustomManagerX() + + class Meta: + abstract = True + + +class AbstractModelX(AbstractBaseModelX): + class Meta: + abstract = True + + +class ModelX(AbstractModelX): + pass + + +class AbstractModelXY(models.Model): + model_x = models.ForeignKey('ModelX') + model_y = models.ForeignKey('ModelY') + + class Meta: + abstract = True + + +class ModelXY(AbstractModelXY): + pass + + +class CustomManagerY(models.Manager): + pass + + +class AbstractModelY(models.Model): + title = models.CharField(max_length=255) + xs = models.ManyToManyField('ModelX', through='ModelXY') + objects = CustomManagerY() + + class Meta: + abstract = True + + +class ModelY(AbstractModelY): + pass diff --git a/modeltranslation/tests/tests.py b/modeltranslation/tests/tests.py index 678d59fe..bf2dc628 100644 --- a/modeltranslation/tests/tests.py +++ b/modeltranslation/tests/tests.py @@ -56,7 +56,7 @@ def decorator(test_item): request = None # How many models are registered for tests. -TEST_MODELS = 29 + (1 if MIGRATIONS else 0) +TEST_MODELS = 31 + (1 if MIGRATIONS else 0) class reload_override_settings(override_settings): @@ -3090,3 +3090,31 @@ def test_required(self): self.assertEqual(set(('req_reg_en', 'req_en_reg', 'req_en_reg_en')), error_fields) else: self.fail('ValidationError not raised!') + + +class M2MTest(ModeltranslationTestBase): + def test_m2m(self): + # Create 1 instance of Y, linked to 2 instance of X, with different + # English and German names. + x1 = models.ModelX.objects.create(name_en="foo", name_de="bar") + x2 = models.ModelX.objects.create(name_en="bar", name_de="baz") + y = models.ModelY.objects.create(title='y1') + models.ModelXY.objects.create(model_x=x1, model_y=y) + models.ModelXY.objects.create(model_x=x2, model_y=y) + + with override("en"): + # There's 1 X named "foo" and it's x1 + y_foo = models.ModelY.objects.filter(xs__name="foo") + self.assertEqual(1, y_foo.count()) + + # There's 1 X named "bar" and it's x2 (in English) + y_bar = models.ModelY.objects.filter(xs__name="bar") + self.assertEqual(1, y_bar.count()) + + # But in English, there's no X named "baz" + y_baz = models.ModelY.objects.filter(xs__name="baz") + self.assertEqual(0, y_baz.count()) + + # Again: 1 X named "bar" (but through the M2M field) + x_bar = y.xs.filter(name="bar") + self.assertIn(x2, x_bar) diff --git a/modeltranslation/tests/translation.py b/modeltranslation/tests/translation.py index 9b9919ed..7f89475e 100644 --- a/modeltranslation/tests/translation.py +++ b/modeltranslation/tests/translation.py @@ -10,7 +10,7 @@ RichText, RichTextPage, MultitableModelA, MultitableModelB, MultitableModelC, ManagerTestModel, CustomManagerTestModel, CustomManager2TestModel, GroupFieldsetsModel, NameModel, ThirdPartyRegisteredModel, ProxyTestModel, UniqueNullableModel, OneToOneFieldModel, - RequiredModel, DecoratedModel) + RequiredModel, DecoratedModel, ModelX, ModelY) class TestTranslationOptions(TranslationOptions): @@ -207,6 +207,18 @@ class DecoratedTranslationOptions(TranslationOptions): fields = ('title',) +# ######### Complex M2M with abstract classes and custom managers + +class ModelXOptions(TranslationOptions): + fields = ('name',) +translator.register(ModelX, ModelXOptions) + + +class ModelYOptions(TranslationOptions): + fields = ('title',) +translator.register(ModelY, ModelYOptions) + + # ######### 3-rd party with custom manager if VERSION >= (1, 8) and "django.contrib.auth" in settings.INSTALLED_APPS: From 22423047e9d8d3af052ef95c4dcb3bee98ba63f5 Mon Sep 17 00:00:00 2001 From: Thomas Jost Date: Tue, 26 Sep 2017 16:22:22 +0200 Subject: [PATCH 167/170] Correctly patch all managers in Django 1.10+ Fixes #389 and #413. --- modeltranslation/translator.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/modeltranslation/translator.py b/modeltranslation/translator.py index a773edc1..ebe93ccd 100644 --- a/modeltranslation/translator.py +++ b/modeltranslation/translator.py @@ -214,9 +214,24 @@ def deconstruct(self): manager.__class__ = NewMultilingualManager - managers = (model._meta.local_managers if NEW_MANAGER_API else - (getattr(model, x[1]) for x in - model._meta.concrete_managers + model._meta.abstract_managers)) + if NEW_MANAGER_API: + # Inspired by django.db.models.options.Options.managers (find all + # managers by following the normal Python MRO rules), but keeps the + # original managers instead of making copies. + managers = [] + seen = set() + bases = (b for b in model.mro() if hasattr(b, '_meta')) + for base in bases: + for manager in base._meta.local_managers: + if manager.name in seen: + continue + managers.append(manager) + seen.add(manager.name) + + else: + managers = ((getattr(model, x[1]) for x in + model._meta.concrete_managers + model._meta.abstract_managers)) + for current_manager in managers: prev_class = current_manager.__class__ patch_manager_class(current_manager) From 24383009156474aa987f9b771c398c1e16c48102 Mon Sep 17 00:00:00 2001 From: Ben Lopatin Date: Sat, 28 Oct 2017 20:56:49 -0400 Subject: [PATCH 168/170] Add docstring documentation for utils.unique --- modeltranslation/utils.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modeltranslation/utils.py b/modeltranslation/utils.py index c6712285..95ba9d13 100755 --- a/modeltranslation/utils.py +++ b/modeltranslation/utils.py @@ -95,6 +95,10 @@ def build_css_class(localized_fieldname, prefix=''): def unique(seq): """ + Returns a generator yielding unique sequence members in order + + A set by itself will return unique values without any regard for order. + >>> list(unique([1, 2, 3, 2, 2, 4, 1])) [1, 2, 3, 4] """ From 49836c210de467b6e01e077cdab15e6926166dcc Mon Sep 17 00:00:00 2001 From: Dirk Eschler Date: Fri, 24 Nov 2017 15:14:40 +0100 Subject: [PATCH 169/170] Satisfied flake8. --- modeltranslation/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modeltranslation/utils.py b/modeltranslation/utils.py index 95ba9d13..81ca7e0f 100755 --- a/modeltranslation/utils.py +++ b/modeltranslation/utils.py @@ -96,9 +96,9 @@ def build_css_class(localized_fieldname, prefix=''): def unique(seq): """ Returns a generator yielding unique sequence members in order - + A set by itself will return unique values without any regard for order. - + >>> list(unique([1, 2, 3, 2, 2, 4, 1])) [1, 2, 3, 4] """ From 90e0a3630a6a46d68f9caa9aa60a2365c1b3baf5 Mon Sep 17 00:00:00 2001 From: Dirk Eschler Date: Fri, 26 Jan 2018 10:23:12 +0100 Subject: [PATCH 170/170] Prepared 0.12.2 release. --- AUTHORS.rst | 1 + CHANGELOG.txt | 8 ++++++++ PKG-INFO | 2 +- modeltranslation/__init__.py | 2 +- 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index f12e1916..fc2415ec 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -44,6 +44,7 @@ Contributors * oliphunt * Venelin Stoykov * Stratos Moros +* Benjamin Toueg * And many more ... (if you miss your name here, please let us know!) .. _django-linguo: https://github.com/zmathew/django-linguo diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 8e6ec3bc..25dcafd6 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,11 @@ +v0.12.2 +======= +Date: 2017-01-26 + + FIXED: order_by with expression + (resolves issue #398, thanks Benjamin Toueg) + + v0.12.1 ======= Date: 2017-04-05 diff --git a/PKG-INFO b/PKG-INFO index b9b05b2c..2ebc3eff 100644 --- a/PKG-INFO +++ b/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 1.0 Name: django-modeltranslation -Version: 0.12.1 +Version: 0.12.2 Summary: Translates Django models using a registration approach. Home-page: https://github.com/deschler/django-modeltranslation Author: Peter Eschler, diff --git a/modeltranslation/__init__.py b/modeltranslation/__init__.py index 5f006545..188cd00d 100644 --- a/modeltranslation/__init__.py +++ b/modeltranslation/__init__.py @@ -3,7 +3,7 @@ Version code adopted from Django development version. https://github.com/django/django """ -VERSION = (0, 12, 1, 'final', 0) +VERSION = (0, 12, 2, 'final', 0) default_app_config = 'modeltranslation.apps.ModeltranslationConfig'