Source code for modeltrans.fields

# -*- coding: utf-8 -*-

from django.contrib.postgres.fields import JSONField
from django.core.exceptions import ImproperlyConfigured
from django.db.models import fields
from django.db.models.expressions import RawSQL
from django.db.models.functions import Cast, Coalesce
from django.utils.translation import ugettext as _

from .utils import build_localized_fieldname, get_default_language, get_language

SUPPORTED_FIELDS = (
    fields.CharField,
    fields.TextField,
)

DEFAULT_LANGUAGE = get_default_language()


def translated_field_factory(original_field, language=None, *args, **kwargs):
    if not isinstance(original_field, SUPPORTED_FIELDS):
        raise ImproperlyConfigured(
            '{} is not supported by django-modeltrans.'.format(original_field.__class__.__name__)
        )

    class Specific(TranslatedVirtualField, original_field.__class__):
        pass

    Specific.__name__ = 'Translated{}'.format(original_field.__class__.__name__)

    return Specific(original_field, language, *args, **kwargs)


[docs]class TranslatedVirtualField(object): ''' A field representing a single field translated to a specific language. Arguments: original_field: The original field to be translated language: The lanuage to translate to, or `None` to track the current active Django language. ''' # Implementation inspired by HStoreVirtualMixin from: # https://github.com/djangonauts/django-hstore/blob/master/django_hstore/virtual.py def __init__(self, original_field, language=None, *args, **kwargs): # TODO: this feels like a big hack. self.__dict__.update(original_field.__dict__) self.original_field = original_field self.language = language self.blank = kwargs['blank'] self.null = kwargs['null'] self.editable = kwargs.get('editable', True) self.concrete = False @property def original_name(self): return self.original_field.name def contribute_to_class(self, cls, name): self.model = cls self.attname = name self.name = name self.column = None # Use a translated verbose name: translated_field_name = _(self.original_field.verbose_name) if self.language is not None: translated_field_name += ' ({})'.format(self.language.upper()) self.verbose_name = translated_field_name setattr(cls, name, self) cls._meta.add_field(self, private=True) def db_type(self, connection): return None def __get__(self, instance, instance_type=None): # this method is apparantly called with instance=None from django. # django-hstor raises AttributeError here, but that doesn't solve # our problem. if instance is None: return language = self.get_language() if language == DEFAULT_LANGUAGE: return getattr(instance, self.original_name) # Make sure we test for containment in a dict, not in None if instance.i18n is None: instance.i18n = {} field_name = build_localized_fieldname(self.original_name, language) # fallback if key does not exist, or contains the empty string (only for <original_field>_i18n fields) if self.language is None and (field_name not in instance.i18n or not instance.i18n[field_name]): return getattr(instance, self.original_name) return instance.i18n.get(field_name) def __set__(self, instance, value): if instance.i18n is None: instance.i18n = {} language = self.get_language() if language == DEFAULT_LANGUAGE: setattr(instance, self.original_name, value) else: field_name = build_localized_fieldname(self.original_name, language) # if value is None, remove field from `i18n`. if value is None: instance.i18n.pop(field_name, None) else: instance.i18n[field_name] = value
[docs] def get_field_name(self): ''' Returns the field name for the current virtual field. The field name is ``<original_field_name>_<language>`` in case of a specific translation or ``<original_field_name>_i18n`` for the currently active language. ''' if self.language is None: lang = 'i18n' else: lang = self.get_language() return build_localized_fieldname(self.original_name, lang)
[docs] def get_language(self): ''' Returns the language for this field. In case of an explicit language (title_en), it returns 'en', in case of `title_i18n`, it returns the currently active Django language. ''' return self.language if self.language is not None else get_language()
def output_field(self): ''' The type of field used to Cast/Coalesce to. Mainly because a max_length argument is required for CharField until this PR is merged: https://github.com/django/django/pull/8758 ''' Field = self.original_field.__class__ if isinstance(self.original_field, fields.CharField): return Field(max_length=self.original_field.max_length) return Field() def sql_lookup(self, fallback=True): ''' Compose the sql lookup to get the value for this virtual field in a query. ''' language = self.get_language() if language == DEFAULT_LANGUAGE: return self.original_name name = build_localized_fieldname(self.original_name, language) i18n_lookup = RawSQL('{}.i18n->>%s'.format(self.model._meta.db_table), (name, )) if fallback: return Coalesce(i18n_lookup, self.original_name, output_field=self.output_field()) else: return Cast(i18n_lookup, self.output_field())
[docs]class TranslationField(JSONField): ''' This model field is used to store the translations in the translated model. Arguments: fields (iterable): List of column names to make translatable. required_languages (iterable): List of languages required for the model. virtual_fields (bool): If False, do not add virtual fields to access translated values with. Set to `True` during migration from django-modeltranslation to prevent collisions with it's database fields while having the `i18n` field available. ''' description = 'Translation storage for a model' def __init__(self, fields=None, required_languages=None, virtual_fields=True, *args, **kwargs): self.fields = fields or () self.required_languages = required_languages or () self.virtual_fields = virtual_fields kwargs['editable'] = False kwargs['null'] = True super(TranslationField, self).__init__(*args, **kwargs) def deconstruct(self): name, path, args, kwargs = super(TranslationField, self).deconstruct() del kwargs['editable'] del kwargs['null'] kwargs['fields'] = self.fields kwargs['required_languages'] = self.required_languages kwargs['virtual_fields'] = self.virtual_fields return name, path, args, kwargs def contribute_to_class(self, cls, name): if name != 'i18n': raise ImproperlyConfigured('{} must have name "i18n"'.format(self.__name__)) super(TranslationField, self).contribute_to_class(cls, name)