def clean(self): super(BaseTranslationFormSet, self).clean() # Trigger combined instance validation master = self.instance stashed = get_cached_translation(master) for form in self.forms: set_cached_translation(master, form.instance) exclusions = form._get_validation_exclusions() # fields from the shared model should not be validated exclusions.extend(f.name for f in master._meta.fields) try: master.clean() except ValidationError as e: form._update_errors(e) set_cached_translation(master, stashed) # Validate that at least one translation exists forms_to_delete = self.deleted_forms provided = [ form for form in self.forms if (getattr(form.instance, 'pk', None) is not None or form.has_changed()) and not form in forms_to_delete ] if len(provided) < 1: raise ValidationError( _('At least one translation must be provided'), code='notranslation')
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] set_cached_translation(self, translation) return getattr(translation, name, default)
def clean(self): super(BaseTranslationFormSet, self).clean() # Trigger combined instance validation master = self.instance stashed = get_cached_translation(master) for form in self.forms: set_cached_translation(master, form.instance) exclusions = form._get_validation_exclusions() # fields from the shared model should not be validated exclusions.extend(f.name for f in master._meta.fields) try: master.clean() except ValidationError as e: form._update_errors(e) set_cached_translation(master, stashed) # Validate that at least one translation exists forms_to_delete = self.deleted_forms provided = [form for form in self.forms if (getattr(form.instance, 'pk', None) is not None or form.has_changed()) and not form in forms_to_delete] if len(provided) < 1: raise ValidationError(_('At least one translation must be provided'), code='notranslation')
def save(self, commit=True): ''' Saves the model If will always use the language specified in self.cleaned_data, with the usual None meaning 'call get_language()'. If instance has another language loaded, it gets reloaded with the new language. If no language is specified in self.cleaned_data, assume the instance is preloaded with correct language. ''' assert self.is_valid(), ( 'Method save() must not be called on an invalid ' 'form. Check the result of .is_valid() before ' 'calling save().') # Get the right translation for object and language # It should have been done in _post_clean, but instance may have been # changed since. enforce = 'language_code' in self.cleaned_data language = self.cleaned_data.get('language_code') or get_language() translation = load_translation(self.instance, language, enforce) # Fill the translated fields with values from the form excludes = list(self._meta.exclude) + ['master', 'language_code'] translation = construct_instance(self, translation, self._meta.fields, excludes) set_cached_translation(self.instance, translation) # Delegate shared fields to super() return super(BaseTranslatableModelForm, self).save(commit=commit)
def translate(self, language_code): ''' Create a new translation for current instance. Does NOT check if the translation already exists! ''' set_cached_translation( self, self._meta.translations_model(language_code=language_code)) return self
def update(self, instance, data): accessor = self.Meta.model._meta.translations_accessor translations_data = data.pop(accessor, None) if translations_data: arbitrary = translations_data.popitem() data.update(arbitrary[1]) stashed = set_cached_translation( instance, load_translation(instance, arbitrary[0], enforce=True)) instance = super(TranslationsMixin, self).update(instance, data) for language, translation_data in translations_data.items(): set_cached_translation( instance, load_translation(instance, language, enforce=True)) self.update_translation(instance, translation_data) set_cached_translation(instance, stashed) qs = instance._meta.translations_model.objects (qs.filter(master=instance).exclude( language_code__in=(arbitrary[0], ) + tuple(translations_data.keys())).delete()) else: instance = super(TranslationsMixin, self).update(instance, data) return instance
def save(self, commit=True): ''' Saves the model If will always use the language specified in self.cleaned_data, with the usual None meaning 'call get_language()'. If instance has another language loaded, it gets reloaded with the new language. If no language is specified in self.cleaned_data, assume the instance is preloaded with correct language. ''' assert self.is_valid(), ('Method save() must not be called on an invalid ' 'form. Check the result of .is_valid() before ' 'calling save().') # Get the right translation for object and language # It should have been done in _post_clean, but instance may have been # changed since. enforce = 'language_code' in self.cleaned_data if getattr(self, 'is_edit', False): language = self.language or get_language() else: language = self.cleaned_data.get('language_code') or get_language() translation = load_translation(self.instance, language, enforce) # Fill the translated fields with values from the form excludes = list(self._meta.exclude) + ['master', 'language_code'] translation = construct_instance(self, translation, self._meta.fields, excludes) if getattr(self, 'is_edit', False): translation.language_code = \ self.cleaned_data.get('language_code', None) or self.new_lang set_cached_translation(self.instance, translation) # Delegate shared fields to super() return super(BaseTranslatableModelForm, self).save(commit=commit)
def update(self, instance, data): 'Handle switching to correct translation before actual update' enforce = 'language_code' in data language = data.pop('language_code', None) or get_language() translation = load_translation(instance, language, enforce) set_cached_translation(instance, translation) return super(TranslatableModelMixin, self).update(instance, data)
def to_representation(self, instance): ''' Combine each translation in turn so the serializer has a full object ''' result = {} stashed = get_cached_translation(instance) for translation in getattr(instance, self.source).all(): set_cached_translation(instance, translation) result[translation.language_code] = self.child.to_representation(instance) set_cached_translation(instance, stashed) return result
def to_representation(self, instance): 'Switch language if we are in enforce mode' enforce = hasattr(self, 'language') language = getattr(self, 'language', None) or get_language() translation = load_translation(instance, language, enforce) set_cached_translation(instance, translation) return super(TranslatableModelMixin, self).to_representation(instance)
def translate(self, language_code): ''' Create a new translation for current instance. Does NOT check if the translation already exists! ''' set_cached_translation( self, self._meta.translations_model(language_code=language_code) ) return self
def translation(self, instance): translation = get_cached_translation(instance) if translation is None: try: translation = get_translation(instance) except self.opts.translations_model.DoesNotExist: language_code = instance.default_language translation = instance.translations.get(language_code = language_code) set_cached_translation(instance, translation) return translation
def to_representation(self, instance): ''' Combine each translation in turn so the serializer has a full object ''' result = {} stashed = get_cached_translation(instance) for translation in getattr(instance, self.source).all(): set_cached_translation(instance, translation) result[translation.language_code] = self.child.to_representation( instance) set_cached_translation(instance, stashed) return result
def translation(self, instance): translation = get_cached_translation(instance) if translation is None: try: translation = get_translation(instance) except self.opts.translations_model.DoesNotExist: raise self._NoTranslationError('Accessing a translated field requires that ' 'the instance has a translation loaded, or a ' 'valid translation in current language (%s) ' 'loadable from the database' % get_language()) set_cached_translation(instance, translation) return translation
def _save_translation(self, form, commit=True): obj = form.save(commit=False) assert isinstance(obj, BaseTranslationModel) if commit: # We need to trigger custom save actions on the combined model stashed = set_cached_translation(self.instance, obj) self.instance.save() if hasattr(obj, 'save_m2m'): # pragma: no cover # cannot happen, but feature could be added, be ready obj.save_m2m() set_cached_translation(self.instance, stashed) return obj
def _post_clean(self): ''' Switch the translation on self.instance This cannot (and should not) be done in clean() because it could be overriden to change the language. Yet it should be done before save() to allow an overriden save to set some translated field values before invoking super(). ''' result = super(BaseTranslatableModelForm, self)._post_clean() enforce = 'language_code' in self.cleaned_data language = self.cleaned_data.get('language_code') or get_language() translation = load_translation(self.instance, language, enforce) set_cached_translation(self.instance, translation) return result
def load_translation(self, instance): if not hvad_settings.AUTOLOAD_TRANSLATIONS: raise AttributeError('Field %r is a translatable field, but no translation is loaded ' 'and auto-loading is disabled because ' 'settings.HVAD[\'AUTOLOAD_TRANSLATIONS\'] is False' % self.name) try: translation = get_translation(instance) except instance._meta.translations_model.DoesNotExist: raise self._NoTranslationError('Accessing a translated field requires that ' 'the instance has a translation loaded, or a ' 'valid translation in current language (%s) ' 'loadable from the database' % get_language()) set_cached_translation(instance, translation) return translation
def __init__(self, *args, **kwargs): # Split arguments into shared/translatd veto_names = ('pk', 'master', 'master_id', self._meta.translations_model._meta.pk.name) skwargs, tkwargs = {}, {} for key, value in kwargs.items(): if key in self._translated_field_names and not key in veto_names: tkwargs[key] = value else: skwargs[key] = value super(TranslatableModel, self).__init__(*args, **skwargs) # Create a translation if there are translated fields if tkwargs: tkwargs['language_code'] = tkwargs.get('language_code') or get_language() set_cached_translation(self, self._meta.translations_model(**tkwargs))
def _save_translation(self, form, commit=True): """ Save translation for given translation form. Do it by loading it onto the master object and saving the master object so custom save() behavior is properly triggered. """ obj = form.save(commit=False) assert isinstance(obj, BaseTranslationModel) if commit: # We need to trigger custom save actions on the combined model stashed = set_cached_translation(self.instance, obj) self.instance.save() if hasattr(obj, 'save_m2m'): # pragma: no cover # cannot happen, but feature could be added, be ready obj.save_m2m() set_cached_translation(self.instance, stashed) return obj
def __init__(self, *args, **kwargs): # Split arguments into shared/translatd veto_names = ('pk', 'master', 'master_id', self._meta.translations_model._meta.pk.name) skwargs, tkwargs = {}, {} translations_opts = self._meta.translations_model._meta for key, value in kwargs.items(): if key in veto_names: skwargs[key] = value else: try: translations_opts.get_field(key) except FieldDoesNotExist: skwargs[key] = value else: tkwargs[key] = value super(TranslatableModel, self).__init__(*args, **skwargs) if tkwargs: tkwargs['language_code'] = tkwargs.get('language_code') or get_language() set_cached_translation(self, self._meta.translations_model(**tkwargs))
def _post_clean(self): ''' Switch the translation on self.instance This cannot (and should not) be done in clean() because it could be overriden to change the language. Yet it should be done before save() to allow an overriden save to set some translated field values before invoking super(). ''' enforce = 'language_code' in self.cleaned_data if getattr(self, 'is_edit', False): language = self.language or get_language() result = super(BaseTranslatableModelForm, self)._post_clean() else: language = self.cleaned_data.get('language_code') or get_language() translation = load_translation(self.instance, language, enforce) exclude = self._get_validation_exclusions() translation = construct_instance(self, translation, self._meta.fields, exclude) set_cached_translation(self.instance, translation) if not getattr(self, 'is_edit', False): result = super(BaseTranslatableModelForm, self)._post_clean() return result
def __init__(self, *args, **kwargs): # Split arguments into shared/translatd veto_names = ('pk', 'master', 'master_id', self._meta.translations_model._meta.pk.name) skwargs, tkwargs = {}, {} translations_opts = self._meta.translations_model._meta for key, value in kwargs.items(): if key in veto_names: skwargs[key] = value else: try: translations_opts.get_field(key) except FieldDoesNotExist: skwargs[key] = value else: tkwargs[key] = value super(TranslatableModel, self).__init__(*args, **skwargs) if tkwargs: tkwargs['language_code'] = tkwargs.get( 'language_code') or get_language() set_cached_translation(self, self._meta.translations_model(**tkwargs))
def save(self, commit=True): ''' Saves the model If will always use the language specified in self.cleaned_data, with the usual None meaning 'call get_language()'. If instance has another language loaded, it gets reloaded with the new language. If no language is specified in self.cleaned_data, assume the instance is preloaded with correct language. ''' if not self.is_valid(): # raise in 1.3, remove in 1.5 warnings.warn( 'Calling save() on an invalid form is deprecated and ' 'will fail in the future. Check the result of .is_valid() ' 'before calling save().', DeprecationWarning, stacklevel=2) raise ValueError((_( "The %s could not be created because the data didn't validate." ) if self.instance.pk is None else _( "The %s could not be changed because the data didn't validate." )) % self.instance._meta.object_name) # Get the right translation for object and language # It should have been done in _post_clean, but instance may have been # changed since. enforce = 'language_code' in self.cleaned_data language = self.cleaned_data.get('language_code') or get_language() translation = load_translation(self.instance, language, enforce) # Fill the translated fields with values from the form excludes = list(self._meta.exclude) + ['master', 'language_code'] translation = construct_instance(self, translation, self._meta.fields, excludes) set_cached_translation(self.instance, translation) # Delegate shared fields to super() return super(BaseTranslatableModelForm, self).save(commit=commit)
def test_get_cached_translation(self): obj = Normal.objects.untranslated().get(pk=self.normal_id[1]) with self.assertNumQueries(0): self.assertIs(get_cached_translation(obj), None) self.assertFalse(obj._meta.get_field('_hvad_query').is_cached(obj)) obj.translate('sr') self.assertIsNot(get_cached_translation(obj), None) self.assertEqual(get_cached_translation(obj).language_code, 'sr') old = set_cached_translation(obj, obj.translations.get_language('ja')) with self.assertNumQueries(0): self.assertEqual(old.language_code, 'sr') self.assertEqual(obj.language_code, 'ja') self.assertEqual(get_cached_translation(obj).language_code, 'ja') set_cached_translation(obj, None) self.assertIs(get_cached_translation(obj), None) self.assertFalse(obj._meta.get_field('_hvad_query').is_cached(obj)) obj = Normal.objects.language('en').get(pk=self.normal_id[1]) with self.assertNumQueries(0): self.assertEqual(get_cached_translation(obj).language_code, 'en')
def update(self, instance, data): accessor = self.Meta.model._meta.translations_accessor translations_data = data.pop(accessor, None) if translations_data: arbitrary = translations_data.popitem() data.update(arbitrary[1]) stashed = set_cached_translation( instance, load_translation(instance, arbitrary[0], enforce=True) ) instance = super(TranslationsMixin, self).update(instance, data) for language, translation_data in translations_data.items(): set_cached_translation(instance, load_translation(instance, language, enforce=True)) self.update_translation(instance, translation_data) set_cached_translation(instance, stashed) qs = instance._meta.translations_model.objects (qs.filter(master=instance) .exclude(language_code__in=(arbitrary[0],)+tuple(translations_data.keys())) .delete()) else: instance = super(TranslationsMixin, self).update(instance, data) return instance
def test_get_cached_translation(self): obj = Normal.objects.untranslated().get(pk=self.normal_id[1]) with self.assertNumQueries(0): self.assertIs(get_cached_translation(obj), None) self.assertFalse(hasattr(obj, obj._meta.translations_cache)) obj.translate('sr') self.assertIsNot(get_cached_translation(obj), None) self.assertEqual(get_cached_translation(obj).language_code, 'sr') old = set_cached_translation(obj, obj.translations.get_language('ja')) with self.assertNumQueries(0): self.assertEqual(old.language_code, 'sr') self.assertEqual(obj.language_code, 'ja') self.assertEqual(get_cached_translation(obj).language_code, 'ja') set_cached_translation(obj, None) self.assertIs(get_cached_translation(obj), None) self.assertFalse(hasattr(obj, obj._meta.translations_cache)) obj = Normal.objects.language('en').get(pk=self.normal_id[1]) with self.assertNumQueries(0): self.assertEqual(get_cached_translation(obj).language_code, 'en')
def activate(self, language): """ Make translation in specified language current for the instance - Only available from shared model translations accessor - Load all translations if they were not already loaded - Passing None unloads current translation - Raise a DoesNotExist exception if no translation exist for that language """ if language is None: translation = None elif language.__class__ is self.model: if language.master_id is not None and language.master_id != self.instance.pk: raise ValueError( 'Trying to activate a translation that does not ' 'belong to this %s' % (self.instance.__class__.__name__, )) translation = language else: self.prefetch() try: translation = next(obj for obj in self.all() if obj.language_code == language) except StopIteration: raise self.model.DoesNotExist set_cached_translation(self.instance, translation)
def save(self, commit=True): ''' Saves the model If will always use the language specified in self.cleaned_data, with the usual None meaning 'call get_language()'. If instance has another language loaded, it gets reloaded with the new language. If no language is specified in self.cleaned_data, assume the instance is preloaded with correct language. ''' if not self.is_valid(): # raise in 1.3, remove in 1.5 warnings.warn('Calling save() on an invalid form is deprecated and ' 'will fail in the future. Check the result of .is_valid() ' 'before calling save().', DeprecationWarning, stacklevel=2) raise ValueError(( _("The %s could not be created because the data didn't validate.") if self.instance.pk is None else _("The %s could not be changed because the data didn't validate.") ) % self.instance._meta.object_name ) # Get the right translation for object and language # It should have been done in _post_clean, but instance may have been # changed since. enforce = 'language_code' in self.cleaned_data language = self.cleaned_data.get('language_code') or get_language() translation = load_translation(self.instance, language, enforce) # Fill the translated fields with values from the form excludes = list(self._meta.exclude) + ['master', 'language_code'] translation = construct_instance(self, translation, self._meta.fields, excludes) set_cached_translation(self.instance, translation) # Delegate shared fields to super() return super(BaseTranslatableModelForm, self).save(commit=commit)