class TranslatableModelForm( with_metaclass(TranslatableModelFormMetaclass, ModelForm)): def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, initial=None, error_class=ErrorList, label_suffix=':', empty_permitted=False, instance=None): opts = self._meta model_opts = opts.model._meta object_data = {} language = getattr(self, 'language', get_language()) if instance is not None: trans = get_cached_translation(instance) if not trans: try: trans = get_translation(instance, language) except model_opts.translations_model.DoesNotExist: trans = None if trans: object_data = model_to_dict(trans, opts.fields, opts.exclude) if initial is not None: object_data.update(initial) initial = object_data super(TranslatableModelForm, self).__init__(data, files, auto_id, prefix, object_data, error_class, label_suffix, empty_permitted, instance) def save(self, commit=True): if self.instance.pk is None: fail_message = 'created' new = True else: fail_message = 'changed' new = False if self.errors: opts = self.instance._meta raise ValueError("The %s could not be %s because the data didn't" " validate." % (opts.object_name, fail_message)) trans_model = self.instance._meta.translations_model language_code = self.cleaned_data.get('language_code', get_language()) if not new: trans = get_cached_translation(self.instance) if not trans or trans.language_code != language_code: try: trans = get_translation(self.instance, language_code) except trans_model.DoesNotExist: trans = trans_model() else: trans = trans_model() trans = construct_instance(self, trans, self._meta.fields) trans.language_code = language_code trans.master = self.instance self.instance = combine(trans, self.Meta.model) super(TranslatableModelForm, self).save(commit=commit) return self.instance def _post_clean(self): if self.instance.pk: try: # Don't use self.instance.language_code here! That will fail if # the instance is not translated into the current language. If # it succeeded, then the instance would already be translated, # and there'd be no point combining it with the same # translation again. trans = get_translation(self.instance, self.language) trans.master = self.instance self.instance = combine(trans, self.Meta.model) except self.instance._meta.translations_model.DoesNotExist: self.instance = self.instance.translate(self.language) return super(TranslatableModelForm, self)._post_clean()
class CustomMetaclassModel( with_metaclass(CustomMetaclass, TranslatableModel)): translations = TranslatedFields()
class TranslatableModel(with_metaclass(TranslatableModelBase, models.Model)): """ Base model for all models supporting translated fields (via TranslatedFields). """ # change the default manager to the translation manager objects = TranslationManager() class Meta: abstract = True def __init__(self, *args, **kwargs): tkwargs = {} # translated fields skwargs = {} # shared fields if 'master' in kwargs.keys(): raise RuntimeError( "Cannot init %s class with a 'master' argument" % \ self.__class__.__name__ ) # filter out all the translated fields (including 'master' and 'language_code') primary_key_names = ('pk', self._meta.pk.name) for key in list(kwargs.keys()): if key in self._translated_field_names: if not key in primary_key_names: # we exclude the pk of the shared model tkwargs[key] = kwargs.pop(key) if not tkwargs.keys(): # if there where no translated options, then we assume this is a # regular init and don't want to do any funky stuff super(TranslatableModel, self).__init__(*args, **kwargs) return # there was at least one of the translated fields (or a language_code) # in kwargs. We need to do magic. # extract all the shared fields (including the pk) for key in list(kwargs.keys()): if key in self._shared_field_names: skwargs[key] = kwargs.pop(key) # do the regular init minus the translated fields super(TranslatableModel, self).__init__(*args, **skwargs) # prepopulate the translations model cache with an translation model tkwargs['language_code'] = tkwargs.get('language_code', get_language()) tkwargs['master'] = self translated = self._meta.translations_model(*args, **tkwargs) setattr(self, self._meta.translations_cache, translated) @classmethod def contribute_translations(cls, rel): """ Contribute translations options to the inner Meta class and set the descriptors. This get's called from TranslatableModelBase.__new__ """ opts = cls._meta opts.translations_accessor = rel.get_accessor_name() opts.translations_model = rel.model opts.translations_cache = '%s_cache' % rel.get_accessor_name() trans_opts = opts.translations_model._meta # Set descriptors ignore_fields = [ 'pk', 'master', opts.translations_model._meta.pk.name, ] for field in trans_opts.fields: if field.name in ignore_fields: continue if field.name == 'language_code': attr = LanguageCodeAttribute(opts) else: attr = TranslatedAttribute(opts, field.name) setattr(cls, field.name, attr) @classmethod def save_translations(cls, instance, **kwargs): """ When this instance is saved, also save the (cached) translation """ opts = cls._meta if hasattr(instance, opts.translations_cache): trans = getattr(instance, opts.translations_cache) if not trans.master_id: trans.master = instance trans.save() def translate(self, language_code): """ Returns an Model instance in the specified language. Does NOT check if the translation already exists! Does NOT interact with the database. This will refresh the translations cache attribute on the instance. """ tkwargs = { 'language_code': language_code, 'master': self, } translated = self._meta.translations_model(**tkwargs) setattr(self, self._meta.translations_cache, translated) return self def safe_translation_getter(self, name, default=None): cache = getattr(self, self._meta.translations_cache, None) if not cache: return default return getattr(cache, name, default) def lazy_translation_getter(self, name, default=None): """ Lazy translation getter that fetches translations from DB in case the instance is currently untranslated and saves the translation instance in the translation cache """ stuff = self.safe_translation_getter(name, NoTranslation) if stuff is not NoTranslation: return stuff # get all translations translations = getattr(self, self._meta.translations_accessor).all() # if no translation exists, bail out now if len(translations) == 0: return default # organize translations into a nice dict translation_dict = dict((t.language_code, t) for t in translations) # see if we have the right language, or any language in fallbacks for code in (get_language(), settings.LANGUAGE_CODE) + FALLBACK_LANGUAGES: try: translation = translation_dict[code] except KeyError: continue break else: # none of the fallbacks was found, pick an arbitrary translation translation = translation_dict.popitem()[1] setattr(self, self._meta.translations_cache, translation) return getattr(translation, name, default) def get_available_languages(self): qs = getattr(self, self._meta.translations_accessor).all() if qs._result_cache is not None: return [obj.language_code for obj in qs] return qs.values_list('language_code', flat=True) #=========================================================================== # Internals #=========================================================================== @property def _shared_field_names(self): if getattr(self, '_shared_field_names_cache', None) is None: self._shared_field_names_cache = self._meta.get_all_field_names() return self._shared_field_names_cache @property def _translated_field_names(self): if getattr(self, '_translated_field_names_cache', None) is None: self._translated_field_names_cache = self._meta.translations_model._meta.get_all_field_names( ) return self._translated_field_names_cache
class TranslatableModelForm( with_metaclass(TranslatableModelFormMetaclass, ModelForm)): def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, initial=None, error_class=ErrorList, label_suffix=':', empty_permitted=False, instance=None): opts = self._meta model_opts = opts.model._meta object_data = {} language = getattr(self, 'language', get_language()) if instance is not None: trans = get_cached_translation(instance) if not trans: try: trans = get_translation(instance, language) except model_opts.translations_model.DoesNotExist: trans = None if trans: object_data = model_to_dict(trans, opts.fields, opts.exclude) if initial is not None: object_data.update(initial) initial = object_data super(TranslatableModelForm, self).__init__(data, files, auto_id, prefix, object_data, error_class, label_suffix, empty_permitted, instance) def save(self, commit=True): if self.instance.pk is None: fail_message = 'created' new = True else: fail_message = 'changed' new = False super(TranslatableModelForm, self).save(True) trans_model = self.instance._meta.translations_model language_code = self.cleaned_data.get('language_code', get_language()) if not new: trans = get_cached_translation(self.instance) if not trans or trans.language_code != language_code: try: trans = get_translation(self.instance, language_code) except trans_model.DoesNotExist: trans = trans_model() else: trans = trans_model() trans.language_code = language_code trans.master = self.instance trans = save_instance(self, trans, self._meta.fields, fail_message, commit, construct=True) return combine(trans, self.Meta.model) def _post_clean(self): if self.instance.pk: try: trans = trans = get_translation(self.instance, self.instance.language_code) trans.master = self.instance self.instance = combine(trans, self.Meta.model) except self.instance._meta.translations_model.DoesNotExist: language_code = self.cleaned_data.get('language_code', get_language()) self.instance = self.instance.translate(language_code) return super(TranslatableModelForm, self)._post_clean()
class TranslatableModel(with_metaclass(TranslatableModelBase, models.Model)): """ Base model for all models supporting translated fields (via TranslatedFields). """ # change the default manager to the translation manager objects = TranslationManager() class Meta: abstract = True def __init__(self, *args, **kwargs): tkwargs = {} # translated fields skwargs = {} # shared fields if 'master' in kwargs.keys(): raise RuntimeError( "Cannot init %s class with a 'master' argument" % \ self.__class__.__name__ ) # filter out all the translated fields (including 'master' and 'language_code') primary_key_names = ('pk', self._meta.pk.name) for key in list(kwargs.keys()): if key in self._translated_field_names: if not key in primary_key_names: # we exclude the pk of the shared model tkwargs[key] = kwargs.pop(key) if not tkwargs.keys(): # if there where no translated options, then we assume this is a # regular init and don't want to do any funky stuff super(TranslatableModel, self).__init__(*args, **kwargs) return # there was at least one of the translated fields (or a language_code) # in kwargs. We need to do magic. # extract all the shared fields (including the pk) for key in list(kwargs.keys()): if key in self._shared_field_names: skwargs[key] = kwargs.pop(key) # do the regular init minus the translated fields super(TranslatableModel, self).__init__(*args, **skwargs) # prepopulate the translations model cache with an translation model tkwargs['language_code'] = tkwargs.get('language_code', get_language()) tkwargs['master'] = self translated = self._meta.translations_model(*args, **tkwargs) setattr(self, self._meta.translations_cache, translated) @classmethod def contribute_translations(cls, rel): """ Contribute translations options to the inner Meta class and set the descriptors. This get's called from TranslatableModelBase.__new__ """ opts = cls._meta opts.translations_accessor = rel.get_accessor_name() opts.translations_model = rel.model opts.translations_cache = '%s_cache' % rel.get_accessor_name() trans_opts = opts.translations_model._meta # Set descriptors ignore_fields = [ 'pk', 'master', opts.translations_model._meta.pk.name, ] for field in trans_opts.fields: if field.name in ignore_fields: continue if field.name == 'language_code': attr = LanguageCodeAttribute(opts) else: attr = TranslatedAttribute(opts, field.name) setattr(cls, field.name, attr) @classmethod def save_translations(cls, instance, **kwargs): """ When this instance is saved, also save the (cached) translation """ opts = cls._meta if hasattr(instance, opts.translations_cache): trans = getattr(instance, opts.translations_cache) if not trans.master_id: trans.master = instance trans.save() def translate(self, language_code): """ Returns an Model instance in the specified language. Does NOT check if the translation already exists! Does NOT interact with the database. This will refresh the translations cache attribute on the instance. """ tkwargs = { 'language_code': language_code, 'master': self, } translated = self._meta.translations_model(**tkwargs) setattr(self, self._meta.translations_cache, translated) return self def safe_translation_getter(self, name, default=None): cache = getattr(self, self._meta.translations_cache, None) if not cache: return default return getattr(cache, name, default) def lazy_translation_getter(self, name, default=None): """ Lazy translation getter that fetches translations from DB in case the instance is currently untranslated and saves the translation instance in the translation cache """ stuff = self.safe_translation_getter(name, NoTranslation) if stuff is not NoTranslation: return stuff # get all translations translations = self._meta.translations_model.objects.filter( master__pk=self.pk) # make them a nice dict translation_dict = {} for translation in translations: translation_dict[translation.language_code] = translation # see if we have the right language correct_language = translation_dict.get(get_language()) if correct_language: # CACHE IT! setattr(self, self._meta.translations_cache, correct_language) return getattr(correct_language, name, default) # Nope? Ok, let's try the default language for the installation default_language = translation_dict.get(settings.LANGUAGE_CODE) if default_language: # CACHE IT! setattr(self, self._meta.translations_cache, default_language) return getattr(default_language, name, default) # *Sigh*, OK, any available langauge? try: any_language = list(translation_dict.values())[0] except IndexError: # OK, can't say we didn't try! return default setattr(self, self._meta.translations_cache, any_language) return getattr(any_language, name, default) def get_available_languages(self): manager = self._meta.translations_model.objects return manager.filter(master=self).values_list('language_code', flat=True) #=========================================================================== # Internals #=========================================================================== @property def _shared_field_names(self): if getattr(self, '_shared_field_names_cache', None) is None: self._shared_field_names_cache = self._meta.get_all_field_names() return self._shared_field_names_cache @property def _translated_field_names(self): if getattr(self, '_translated_field_names_cache', None) is None: self._translated_field_names_cache = self._meta.translations_model._meta.get_all_field_names( ) return self._translated_field_names_cache