diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 95bbbe6..3f53c76 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -8,6 +8,10 @@ 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. diff --git a/modeltranslation/fields.py b/modeltranslation/fields.py index d040772..233fa5f 100644 --- a/modeltranslation/fields.py +++ b/modeltranslation/fields.py @@ -1,13 +1,53 @@ # -*- 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): """ @@ -31,8 +71,6 @@ 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): @@ -54,8 +92,7 @@ class TranslationField(Field): # 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) @@ -106,31 +143,31 @@ class RelatedTranslationField(object): 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) @@ -138,7 +175,7 @@ class ForeignKeyTranslationField(ForeignKey, TranslationField, 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): @@ -147,9 +184,69 @@ class OneToOneTranslationField(OneToOneField, TranslationField, 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 diff --git a/modeltranslation/translator.py b/modeltranslation/translator.py index 9c864ed..b159c0b 100644 --- a/modeltranslation/translator.py +++ b/modeltranslation/translator.py @@ -7,13 +7,14 @@ from django.db.models.base import ModelBase from django.utils.functional import curry from modeltranslation.fields import (TranslationField, - ForeignKeyTranslationField, - OneToOneTranslationField, - ManyToManyTranslationField) -from modeltranslation.utils import (TranslationFieldDescriptor, - RelatedTranslationFieldDescriptor, - ManyToManyTranslationFieldDescriptor, - build_localized_fieldname) + TranslationForeignKey, + TranslationOneToOneField, + TranslationManyToManyField, + TranslationFieldDescriptor, + RelatedTranslationFieldDescriptor, + ManyToManyTranslationFieldDescriptor, + create_translation_field) +from modeltranslation.utils import build_localized_fieldname class AlreadyRegistered(Exception): @@ -58,32 +59,15 @@ def add_localized_fields(model): 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 ('ManyToOneRel', 'OneToOneRel', - 'ManyToManyRel',): - 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) diff --git a/modeltranslation/utils.py b/modeltranslation/utils.py index 21b5ea9..d69e864 100644 --- a/modeltranslation/utils.py +++ b/modeltranslation/utils.py @@ -56,65 +56,3 @@ 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="", 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 - self.loc_field_name = "" - - 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) - self.loc_field_name = build_localized_fieldname(self.name, - get_language()) - if hasattr(instance, self.loc_field_name): - return getattr(instance, self.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 - #instance_id = instance.__dict__['%s_id' % self.name] - 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