Source code for shuup.utils.multilanguage_model_form

# -*- coding: utf-8 -*-
# This file is part of Shuup.
#
# Copyright (c) 2012-2021, Shuup Commerce Inc. All rights reserved.
#
# This source code is licensed under the OSL-3.0 license found in the
# LICENSE file in the root directory of this source tree.
import copy
import six
import warnings
from collections import defaultdict
from django.conf import settings
from django.forms.models import ModelForm, model_to_dict
from django.utils.translation import get_language, ugettext_lazy as _
from parler.forms import TranslatableModelForm
from parler.utils.context import switch_language

from shuup.utils.i18n import get_language_name


[docs]def to_language_codes(languages, default_language): languages = languages or (get_language(),) if languages and isinstance(languages[0], (list, tuple)): # `languages` looks like a `settings.LANGUAGES`, so fix it languages = [code for (code, name) in languages] if default_language not in languages: raise ValueError("Error! Default language `%r` not in the list: `%r`." % (default_language, languages)) languages = [default_language] + [code for code in languages if code != default_language] return languages
[docs]class MultiLanguageModelForm(TranslatableModelForm): def _get_translation_models(self): return self._meta.model._parler_meta.get_all_models() def _get_translation_model(self): warnings.warn( "Warning! `_get_translation_model` is deprecated in Shuup 2.x as unused for this util.", DeprecationWarning, ) return self._meta.model._parler_meta.root_model def __init__(self, **kwargs): # noqa (C901) self.default_language = kwargs.pop( "default_language", getattr(self, "language", getattr(settings, "PARLER_DEFAULT_LANGUAGE_CODE")) ) self.languages = to_language_codes(kwargs.pop("languages", ()), self.default_language) self.required_languages = kwargs.pop("required_languages", [self.default_language]) opts = self._meta translations_models = self._get_translation_models() object_data = {} # We're not mutating the existing fields, so the shallow copy should be okay self.base_fields = self.base_fields.copy() self.translation_fields = [] for translations_model in translations_models: for field in translations_model._meta.get_fields(): if field.name not in ("language_code", "master", "id") and field.name in self.base_fields: self.translation_fields.append(field) self.trans_field_map = defaultdict(dict) self.trans_name_map = defaultdict(dict) self.translated_field_names = [] self.required_translated_field_names = [] self.non_default_languages = sorted(set(self.languages) - set([self.default_language])) self.language_names = dict((lang, get_language_name(lang)) for lang in self.languages) for f in self.translation_fields: base = self.base_fields.pop(f.name, None) if not base: continue for lang in self.languages: language_field = copy.deepcopy(base) language_field_name = "%s__%s" % (f.name, lang) if language_field.required: self.required_translated_field_names.append(language_field_name) language_field.required = language_field.required and (lang in self.required_languages) language_field.label = self._get_label(f.name, language_field, lang) self.base_fields[language_field_name] = language_field self.trans_field_map[lang][language_field_name] = f self.trans_name_map[lang][f.name] = language_field_name self.translated_field_names.append(language_field_name) instance = kwargs.get("instance") initial = kwargs.get("initial") if instance is not None: assert isinstance(instance, self._meta.model) current_translations = defaultdict(list) for translations_model in translations_models: for trans in translations_model.objects.filter(master=instance): current_translations[trans.language_code].append(trans) object_data = {} for lang, translations in six.iteritems(current_translations): for trans in translations: model_dict = model_to_dict(trans, opts.fields, opts.exclude) object_data.update(("%s__%s" % (fn, lang), f) for (fn, f) in six.iteritems(model_dict)) if initial is not None: object_data.update(initial) kwargs["initial"] = object_data super(MultiLanguageModelForm, self).__init__(**kwargs) def __getitem__(self, key): try: return super(MultiLanguageModelForm, self).__getitem__(key) except KeyError: return super(MultiLanguageModelForm, self).__getitem__(key + "__" + self.default_language)
[docs] def clean(self): """ Avoid partially translated languages where the translated fields that are required are not set. """ data = self.cleaned_data for language, field_names in self.trans_name_map.items(): if not any(data.get(field_name) for field_name in field_names.values()): continue # No need to check this language further for field_name in field_names.values(): if field_name in self.required_translated_field_names and not data.get(field_name): self.add_error(field_name, _("This field is required.")) return data
def _save_translations(self, instance, data): for translations_model in self._get_translation_models(): current_translations = dict( (trans.language_code, trans) for trans in translations_model.objects.filter(master_id=instance.id, language_code__in=self.languages) ) for lang, field_map in six.iteritems(self.trans_field_map): translation_fields = dict((src_name, data.get(src_name)) for src_name in field_map) translation = current_translations.get(lang) # Add translation only if at least one translated field is given if not any(translation_fields.values()): if translation: translation.delete() # No translations set so delete the object also. continue current_translations[lang] = translation = translation or translations_model( master=instance, language_code=lang ) for src_name, field in six.iteritems(field_map): field.save_form_data(translation, translation_fields[src_name]) self._save_translation(instance, translation) def _save_translation(self, instance, translation): """ Process saving a single translation. This could be used to delete unnecessary/cleared translations or skip saving translations altogether. :param instance: Parent model instance. :type instance: django.db.models.Model :param translation: Translation model. :type translation: parler.models.TranslatedFieldsModelBase """ translation.save()
[docs] def save(self, commit=True): self._set_fields_for_language(self.default_language) self.pre_master_save(self.instance) # Save is necessary here since translations can not be # attached to non-saved object self.instance = self._save_master(commit) self._save_translations(self.instance, self.cleaned_data) # Save the master once again since the save might involve # some procedures that requires translations like generating # slug from translated name. self._save_master(commit) return self.instance
def _set_fields_for_language(self, language): with switch_language(self.instance, language): self.instance.set_current_language(language) for field in self.translation_fields: value = self.cleaned_data["%s__%s" % (field.name, language)] field.save_form_data(self.instance, value) def _save_master(self, commit=True): # We skip TranslatableModelForm on purpose! return super(ModelForm, self).save(True)
[docs] def pre_master_save(self, instance): # Subclass hook pass
def _get_cleaned_data_without_translations(self): """ Get cleaned data without translated fields. """ translated_field_names = set(self.translated_field_names) return dict((k, v) for (k, v) in six.iteritems(self.cleaned_data) if k not in translated_field_names) def _get_label(self, field_name, field, lang): label = field.label if self._meta.labels: label = self._meta.labels.get(field_name, field.label) if len(self.languages) > 1: return "%s [%s]" % (label, self.language_names.get(lang)) return label