Commits

Kostas Papadimitriou committed 38d0f7e Merge

merge with googlecode

Comments (0)

Files changed (7)

+  ADDED: Configurable default value per field instance.
+         (thanks to bmihelac, resolves issue 28)
+  ADDED: Support for NullBooleanField.
+         (thanks to jaap, resolves issue 34)
+  ADDED: Setting to override the default language.
+         (thanks to jaap, resolves issue 2)
   ADDED: Support for related fields - ForeignKey, ManyToManyField and
          OneToOneField.
          (resolves issue 15)
 
+CHANGED: Refactored creation of translation fields and added handling of
+         supported fields.
+         (resolves issue 37)
+
+  FIXED: Kept backwards compatibility with Django-1.0.
+         (thanks to jaap, resolves issue 34)
+  FIXED: Regression in south_field_triple caused by r55.
+         (thanks to jaap, resolves issue 29)
   FIXED: TranslationField pre_save does not get the default language
          correctly.
          (thanks to jaap, resolves issue 31)

docs/modeltranslation/modeltranslation.txt

 (statically) translate the verbose names of the languages using the standard
 ``i18n`` solution.
 
+**settings.MODELTRANSLATION_DEFAULT_LANGUAGE**
+
+*New in development version*
+To override the default language as described in settings.LANGUAGES, define
+``MODELTRANSLATION_DEFAULT_LANGUAGE``. Note that the value has to be in
+settings.LANGUAGES, otherwise an exception will be raised.
+
 **settings.TRANSLATION_REGISTRY**
 
+TODO: Rename setting to MODELTRANSLATION_TRANSLATION_REGISTRY.
+
 In order to be able to import the project's ``translation.py`` registration
 file the ``TRANSLATION_REGISTRY`` must be set to a value in the form
 ``<PROJECT_MODULE>.translation``. E.g. if your project is located in a folder
 
 Inlines
 -------
-*New in development version*
+*New in 0.2*
 Support for tabular and stacked inlines, common and generic ones.
 
 A translated inline must derive from one of the following classes:
 admin.site.register(News, NewsAdmin)
 }}}
 
+Set a default value
+-------------------
+*New in developement version*
+TODO
+
 
 The ``update_translation_fields`` command
 =========================================

modeltranslation/fields.py

 # -*- coding: utf-8 -*-
+import sys
+
 from django.conf import settings
+from django.core.exceptions import ImproperlyConfigured
 from django.db.models.fields import Field, CharField
 from django.db.models.fields.related import (ForeignKey, OneToOneField,
                                              ManyToManyField)
 
-from modeltranslation.utils import (get_default_language,
+from modeltranslation.utils import (get_language,
+                                    get_default_language,
                                     build_localized_fieldname,
                                     build_localized_verbose_name)
 
+# List of fields which don't have to be subclassed to be supported
+STD_TRANSLATION_FIELDS = ('CharField', 'TextField', 'IntegerField',
+                          'BooleanField', 'NullBooleanField',)
+
+
+def create_translation_field(model, field_name, lang):
+    """
+    Translation field factory.
+
+    Tries to create an object in the form  ``'Translation%s' % cls_name``
+    (e.g. ``TranslationForeignKey``, ``TranslationManyToManyField``) based on
+    ``model`` and ``field_name``. The class is usually a subclass of
+    ``TranslationField`` and is supposed to be implemented in this module. If
+    the class is listed in ``STD_TRANSLATION_FIELDS`` then ``TranslationField``
+    will be used to instantiate the object. If the class is neither implemented
+    nor in ``STD_TRANSLATION_FIELDS`` ``ImproperlyConfigured`` will be raised.
+    """
+    field = model._meta.get_field(field_name)
+    cls_name = field.__class__.__name__
+    # No subclass required for text fields
+    if cls_name in STD_TRANSLATION_FIELDS:
+        return TranslationField(translated_field=field, language=lang)
+    # Try to instantiate translation field subclass
+    try:
+        translation_field = getattr(sys.modules['modeltranslation.fields'],
+                                    'Translation%s' % cls_name)
+    except AttributeError:
+        raise ImproperlyConfigured('%s is not supported by '
+                                   'modeltranslation.' % cls_name)
+    # Handle related fields
+    if cls_name in ('ForeignKey', 'OneToOneField', 'ManyToManyField'):
+        to = field.rel.to._meta.object_name
+        return translation_field(translated_field=field, language=lang, to=to)
+    # TODO: Should never be reached?
+    return TranslationField(field, lang)
+
 
 class TranslationField(Field):
     """
         # 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__)
-
-        # Common init
         self._post_init(translated_field, language)
 
     def _post_init(self, translated_field, language):
         # Copy the verbose name and append a language suffix
         # (will show up e.g. in the admin).
         self.verbose_name =\
-        build_localized_verbose_name(translated_field.verbose_name,
-                                     language)
+        build_localized_verbose_name(translated_field.verbose_name, language)
 
     def pre_save(self, model_instance, add):
         val = super(TranslationField, self).pre_save(model_instance, add)
             model_instance.__dict__[self.translated_field.name] = val
         return val
 
-    def get_db_prep_value(self, value, connection, prepared=False):
-        if value == "":
-            return None
-        else:
-            return value
+    def get_prep_value(self, value):
+        if value == '':
+            value = None
+        return self.translated_field.get_prep_value(value)
+
+    def get_prep_lookup(self, lookup_type, value):
+        return self.translated_field.get_prep_lookup(lookup_type, value)
+
+    def to_python(self, value):
+        return self.translated_field.to_python(value)
 
     def get_internal_type(self):
         return self.translated_field.get_internal_type()
         self.language = language
 
         self.field_name = self.translated_field.name
-        self.translated_field_name = \
-            build_localized_fieldname(self.translated_field.name,
-                                      self.language)
+        self.translated_field_name =\
+        build_localized_fieldname(self.translated_field.name,
+                                  self.language)
 
         # Dynamically add a related_name to the original field
-        translated_field.rel.related_name = \
-            '%s%s' % (self.translated_field.model._meta.module_name,
-                      self.field_name)
+        translated_field.rel.related_name =\
+        '%s%s' % (self.translated_field.model._meta.module_name,
+                  self.field_name)
 
         TranslationField.__init__(self, self.translated_field, self.language,
                                   *args, **kwargs)
 
     def _related_post_init(self):
         # Dynamically add a related_name to the translation fields
-        self.rel.related_name = \
-            '%s%s' % (self.translated_field.model._meta.module_name,
-                      self.translated_field_name)
+        self.rel.related_name =\
+        '%s%s' % (self.translated_field.model._meta.module_name,
+                  self.translated_field_name)
 
         # ForeignKey's init overrides some essential values from
         # TranslationField, they have to be reassigned.
         TranslationField._post_init(self, self.translated_field, self.language)
 
 
-class ForeignKeyTranslationField(ForeignKey, TranslationField,
-                                 RelatedTranslationField):
+class TranslationForeignKey(ForeignKey, TranslationField,
+                            RelatedTranslationField):
     def __init__(self, translated_field, language, to, to_field=None, *args,
                  **kwargs):
         self._related_pre_init(translated_field, language, *args, **kwargs)
         self._related_post_init()
 
 
-class OneToOneTranslationField(OneToOneField, TranslationField,
+class TranslationOneToOneField(OneToOneField, TranslationField,
                                RelatedTranslationField):
     def __init__(self, translated_field, language, to, to_field=None, *args,
                  **kwargs):
         self._related_post_init()
 
 
-class ManyToManyTranslationField(ManyToManyField, TranslationField,
+class TranslationManyToManyField(ManyToManyField, TranslationField,
                                  RelatedTranslationField):
     def __init__(self, translated_field, language, to, *args, **kwargs):
         self._related_pre_init(translated_field, language, *args, **kwargs)
         ManyToManyField.__init__(self, to, **kwargs)
         self._related_post_init()
+
+
+class TranslationFieldDescriptor(object):
+    """A descriptor used for the original translated field."""
+    def __init__(self, name, initial_val="", fallback_value=None):
+        """
+        The ``name`` is the name of the field (which is not available in the
+        descriptor by default - this is Python behaviour).
+        """
+        self.name = name
+        self.val = initial_val
+        self.fallback_value = fallback_value
+
+    def __set__(self, instance, value):
+        lang = get_language()
+        loc_field_name = build_localized_fieldname(self.name, lang)
+        # also update the translation field of the current language
+        setattr(instance, loc_field_name, value)
+        # update the original field via the __dict__ to prevent calling the
+        # descriptor
+        instance.__dict__[self.name] = value
+
+    def __get__(self, instance, owner):
+        if not instance:
+            raise ValueError(u"Translation field '%s' can only be accessed "
+                              "via an instance not via a class." % self.name)
+        loc_field_name = build_localized_fieldname(self.name,
+                                                   get_language())
+        if hasattr(instance, loc_field_name):
+            return getattr(instance, loc_field_name) or\
+                           (self.get_default_instance(instance) if\
+                            self.fallback_value is None else\
+                            self.fallback_value)
+
+    def get_default_instance(self, instance):
+        """
+        Returns default instance of the field. Supposed to be overidden by
+        related subclasses.
+        """
+        return instance.__dict__[self.name]
+
+
+class RelatedTranslationFieldDescriptor(TranslationFieldDescriptor):
+    def __init__(self, name, initial_val="", fallback_value=None):
+        TranslationFieldDescriptor.__init__(self, name, initial_val="",
+                                            fallback_value=None)
+
+    def get_default_instance(self, instance):
+        # TODO: Implement
+        pass
+
+
+class ManyToManyTranslationFieldDescriptor(TranslationFieldDescriptor):
+    def __init__(self, name, initial_val="", fallback_value=None):
+        TranslationFieldDescriptor.__init__(self, name, initial_val="",
+                                            fallback_value=None)
+
+    def get_default_instance(self, instance):
+        # TODO: Implement
+        pass

modeltranslation/models.py

 
 # Import the project's global "translation.py" which registers model
 # classes and their translation options with the translator object.
+# TODO: Rename setting to MODELTRANSLATION_TRANSLATION_REGISTRY.
 if getattr(settings, 'TRANSLATION_REGISTRY', False):
     try:
         __import__(settings.TRANSLATION_REGISTRY, {}, {}, [''])

modeltranslation/tests.py

 class TestTranslationOptions(translator.TranslationOptions):
     fields = ('title', 'text',)
 
-
 translator.translator._registry = {}
 translator.translator.register(TestModel, TestTranslationOptions)
 
 
+class TestModelWithFallback(models.Model):
+    title = models.CharField(ugettext_lazy('title'), max_length=255)
+    text = models.TextField(null=True)
+
+
+class TestTranslationOptionsWithFallback(translator.TranslationOptions):
+    fields = ('title', 'text',)
+    fallback_values = ""
+
+translator.translator.register(TestModelWithFallback,
+                               TestTranslationOptionsWithFallback)
+
+
+class TestModelWithFallback2(models.Model):
+    title = models.CharField(ugettext_lazy('title'), max_length=255)
+    text = models.TextField(null=True)
+
+
+class TestTranslationOptionsWithFallback2(translator.TranslationOptions):
+    fields = ('title', 'text',)
+    fallback_values = {'text': ugettext_lazy('Sorry, translation is not '
+                                             'available.')}
+
+translator.translator.register(TestModelWithFallback2,
+                               TestTranslationOptionsWithFallback2)
+
+
 class ModelTranslationTest(TestCase):
     """Basic tests for the modeltranslation application."""
     urls = 'modeltranslation.testurls'
         self.failUnless('en' in langs)
         self.failUnless(translator.translator)
 
-        # Check that only one model is registered for translation
-        self.failUnlessEqual(len(translator.translator._registry), 1)
+        # Check that three models are registered for translation
+        self.failUnlessEqual(len(translator.translator._registry), 3)
 
         # Try to unregister a model that is not registered
         self.assertRaises(translator.NotRegistered,
         self.failUnlessEqual(n.title, title_foo)
         self.failUnlessEqual(n.title_de, title_foo)
         self.failUnlessEqual(n.title_en, title2_en)
+
+    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 = TestModelWithFallback()
+        n.title = title1_de
+        n.save()
+        del n
+        n = TestModelWithFallback.objects.get(title=title1_de)
+        self.failUnlessEqual(n.title, title1_de)
+        trans_real.activate("en")
+        self.failUnlessEqual(n.title, "")
+
+    def test_fallback_values_2(self):
+        """
+        If `fallback_values' is set to `dic`, all untranslated fields`in `dic`
+        would return this mapped value.
+        Fields not in `dic` would return default translation.
+        """
+        title1_de = "title de"
+        text1_de = "text in german"
+        n = TestModelWithFallback2()
+        n.title = title1_de
+        n.text = text1_de
+        n.save()
+        del n
+        n = TestModelWithFallback2.objects.get(title=title1_de)
+        trans_real.activate("en")
+        self.failUnlessEqual(n.title, title1_de)
+        self.failUnlessEqual(n.text,\
+        TestTranslationOptionsWithFallback2.fallback_values['text'])

modeltranslation/translator.py

 from django.utils.functional import curry
 
 from modeltranslation.fields import (TranslationField,
-                                     ForeignKeyTranslationField,
-                                     OneToOneTranslationField,
-                                     ManyToManyTranslationField)
-from modeltranslation.utils import (TranslationFieldDescriptor,
-                                    build_localized_fieldname)
-
-
-RELATED_CLASSES = ('ManyToOneRel', 'OneToOneRel', 'ManyToManyRel',)
+                                     TranslationForeignKey,
+                                     TranslationOneToOneField,
+                                     TranslationManyToManyField,
+                                     TranslationFieldDescriptor,
+                                     RelatedTranslationFieldDescriptor,
+                                     ManyToManyTranslationFieldDescriptor,
+                                     create_translation_field)
+from modeltranslation.utils import build_localized_fieldname
 
 
 class AlreadyRegistered(Exception):
             localized_field_name = build_localized_fieldname(field_name, l[0])
             # Check if the model already has a field by that name
             if hasattr(model, localized_field_name):
-                raise ValueError("Error adding translation field. The model "\
-                                 "'%s' already contains a field named '%s'. "\
-                                 % (instance.__class__.__name__,
-                                    localized_field_name))
-
+                raise ValueError("Error adding translation field. Model '%s' "
+                                 "already contains a field named '%s'." %\
+                                 (instance.__class__.__name__,
+                                  localized_field_name))
+            # Create a dynamic translation field
+            translation_field = create_translation_field(model=model,\
+                                field_name=field_name, lang=l[0])
             # This approach implements the translation fields as full valid
             # django model fields and therefore adds them via add_to_class
-            field = model._meta.get_field(field_name)
-            field_class_name = field.rel.__class__.__name__
-            if field_class_name in RELATED_CLASSES:
-                to = field.rel.to._meta.object_name
-                if field_class_name == 'ManyToOneRel':
-                    translation_field = ForeignKeyTranslationField(\
-                                        translated_field=field,
-                                        language=l[0], to=to)
-                elif field_class_name == 'OneToOneRel':
-                    translation_field = OneToOneTranslationField(\
-                                        translated_field=field,
-                                        language=l[0], to=to)
-                elif field_class_name == 'ManyToManyRel':
-                    translation_field = ManyToManyTranslationField(\
-                                        translated_field=field,
-                                        language=l[0], to=to)
-            else:
-                translation_field = TranslationField(field, l[0])
             localized_field = model.add_to_class(localized_field_name,
                                                  translation_field)
             localized_fields[field_name].append(localized_field_name)
                 translation_opts.localized_fieldnames.items():
                 for ln in loc_names:
                     rev_dict[ln] = orig_name
-
             translation_opts.localized_fieldnames_rev = rev_dict
 
-        #print "Applying descriptor field for model %s" % model
+        model_fallback_values = getattr(translation_opts, 'fallback_values',
+                                        None)
         for field_name in translation_opts.fields:
-            setattr(model, field_name, TranslationFieldDescriptor(field_name))
+            # TODO: Check if fallback_value is set to a type that the field
+            #       expects and raise ImproperlyConfigured in case it doesn't.
+            if model_fallback_values is None:
+                field_fallback_value = None
+            elif isinstance(model_fallback_values, dict):
+                field_fallback_value = model_fallback_values.get(field_name,
+                                                                 None)
+            else:
+                field_fallback_value = model_fallback_values
+
+            field = model._meta.get_field(field_name)
+            field_class_name = field.rel.__class__.__name__
+            if field_class_name in ('ManyToOneRel', 'OneToOneRel'):
+                descriptor = RelatedTranslationFieldDescriptor(field_name,
+                             fallback_value=field_fallback_value)
+            elif field_class_name == 'ManyToManyRel':
+                descriptor = ManyToManyTranslationFieldDescriptor(field_name,
+                             fallback_value=field_fallback_value)
+            else:
+                descriptor = TranslationFieldDescriptor(field_name,
+                             fallback_value=field_fallback_value)
+            setattr(model, field_name, descriptor)
 
         #signals.pre_init.connect(translated_model_initializing, sender=model,
                                  #weak=False)

modeltranslation/utils.py

 # -*- coding: utf-8 -*-
 from django.db import models
 from django.conf import settings
-from django.core.exceptions import ValidationError
+from django.core.exceptions import ValidationError, ImproperlyConfigured
 from django.contrib.contenttypes.models import ContentType
 from django.utils.translation import get_language as _get_language
 from django.utils.functional import lazy
 
 
+def get_available_languages():
+    """Returns a list of the language codes in settings.LANGUAGES"""
+    return [l[0] for l in settings.LANGUAGES]
+
+
 def get_language():
     """
     Return an active language code that is guaranteed to be in
-    settings.LANGUAGES (Django does not seem to guarantee this for us.)
+    settings.LANGUAGES (Django does not seem to guarantee this for us).
     """
     lang = _get_language()
-    available_languages = [l[0] for l in settings.LANGUAGES]
+    available_languages = get_available_languages()
     if lang not in available_languages and '-' in lang:
         lang = lang.split('-')[0]
     if lang in available_languages:
 
 
 def get_default_language():
-    return settings.LANGUAGES[0][0]
+    """
+    Returns the language to use as the default language. This is either
+    the value of settings.DEFAULT_LANGUAGE (if it's in the list of
+    settings.LANGUAGES) or the first item in settings.LANGUAGES.
+    """
+    available_languages = get_available_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.')
+    if not default_language:
+        default_language = available_languages[0]
+    return default_language
 
 
 def get_translation_fields(field):
     """Returns a list of localized fieldnames for a given field."""
-    return [build_localized_fieldname(field, l[0]) for l in settings.LANGUAGES]
+    return [build_localized_fieldname(field, l) for l in\
+            get_available_languages()]
 
 
 def build_localized_fieldname(field_name, lang):
 def _build_localized_verbose_name(verbose_name, lang):
     return u'%s [%s]' % (verbose_name, lang)
 build_localized_verbose_name = lazy(_build_localized_verbose_name, unicode)
-
-
-class TranslationFieldDescriptor(object):
-    """A descriptor used for the original translated field."""
-    def __init__(self, name, initial_val=""):
-        """
-        The ``name`` is the name of the field (which is not available in the
-        descriptor by default - this is Python behaviour).
-        """
-        self.name = name
-        self.val = initial_val
-
-    def __set__(self, instance, value):
-        lang = get_language()
-        loc_field_name = build_localized_fieldname(self.name, lang)
-        # also update the translation field of the current language
-        setattr(instance, loc_field_name, value)
-        # update the original field via the __dict__ to prevent calling the
-        # descriptor
-        instance.__dict__[self.name] = value
-
-    def __get__(self, instance, owner):
-        if not instance:
-            raise ValueError(u"Translation field '%s' can only be "
-                              "accessed via an instance not via "
-                              "a class." % self.name)
-        lang = get_language()
-        loc_field_name = build_localized_fieldname(self.name, lang)
-        if hasattr(instance, loc_field_name):
-            return getattr(instance, loc_field_name) or \
-                   instance.__dict__[self.name]
-        #return instance.__dict__[self.name]
-        # FIXME: KeyError raised for ForeignKeyTanslationField
-        #        in admin list view
-        try:
-            return instance.__dict__[self.name]
-        except KeyError:
-            return None