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 iterator(self): qs = self._clone()._add_language_filter() qs._known_related_objects = {} # super's iterator will attempt to set them if qs._forced_unique_fields: with ForcedUniqueFields(qs._forced_unique_fields): objects = list(super(TranslationQueryset, qs).iterator()) if type(qs.query.select_related) == dict: for obj in objects: qs._use_related_translations(obj, qs.query.select_related) else: objects = super(TranslationQueryset, qs).iterator() for obj in objects: for name in self._hvad_switch_fields: try: setattr(obj.master, name, getattr(obj, name)) except AttributeError: # pragma: no cover pass else: delattr(obj, name) obj = combine(obj, qs.shared_model) # use known objects from self, not qs as we cleared it earlier for field, rel_objs in self._known_related_objects.items(): if hasattr(obj, field.get_cache_name()): # should not happen, but we conform to Django behavior continue #pragma: no cover pk = getattr(obj, field.get_attname()) try: rel_obj = rel_objs[pk] except KeyError: #pragma: no cover pass else: setattr(obj, field.name, rel_obj) yield obj
def __iter__(self): qs = self.queryset._clone()._add_language_filter() qs._iterable_class = ModelIterable qs._known_related_objects = {} if qs._forced_unique_fields: with ForcedUniqueFields(qs._forced_unique_fields): objects = list(qs.iterator()) if type(qs.query.select_related) == dict: for obj in objects: qs._use_related_translations(obj, qs.query.select_related) else: objects = qs.iterator() for obj in objects: for name in qs._hvad_switch_fields: try: setattr(obj.master, name, getattr(obj, name)) except AttributeError: # pragma: no cover pass else: delattr(obj, name) obj = combine(obj, qs.shared_model) # use known objects from self.queryset, not qs as we cleared it earlier for field, rel_objs in self.queryset._known_related_objects.items(): if hasattr(obj, field.get_cache_name()): continue # pragma: no cover (conform to Django behavior) pk = getattr(obj, field.get_attname()) try: rel_obj = rel_objs[pk] except KeyError: # pragma: no cover pass else: setattr(obj, field.name, rel_obj) yield obj
def clean(self): super(BaseTranslationFormSet, self).clean() # Trigger combined instance validation master = self.instance stashed = get_cached_translation(master) for form in self.forms: form.instance.master = master combined = combine(form.instance, master.__class__) exclusions = form._get_validation_exclusions() # fields from the shared model should not be validated exclusions.extend(f.name for f in combined._meta.fields) try: if django.VERSION >= (1, 6): combined.full_clean(exclude=exclusions, validate_unique=form._validate_unique) else: combined.full_clean(exclude=exclusions) except ValidationError as e: form._update_errors(e) if stashed is None: delattr(master, master._meta.translations_cache) else: setattr(master, master._meta.translations_cache, stashed) # Validate that at least one translation exists forms_to_delete = self.deleted_forms valid = [((form.instance and form.instance.pk is not None) or (form.is_valid() and form.has_changed())) and not form in forms_to_delete for form in self.forms] if valid.count(True) < 1: raise ValidationError(_('At least one translation must be provided'), code='notranslation')
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 = 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 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: 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()
def test_combine(self): model = Normal._meta.translations_model qs = model.objects.select_related('master').filter(master=self.normal_id[1]) for translation in qs: combined = combine(translation, NormalProxy) self.assertEqual(combined.pk, self.normal_id[1]) self.assertEqual(combined.shared_field, NORMAL[1].shared_field) self.assertEqual(combined.translated_field, NORMAL[1].translated_field[translation.language_code]) self.assertIsInstance(combined, NormalProxy)
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()
def iterator(self): """ If this queryset is not filtered by a language code yet, it should be filtered first by calling self.language. If someone doesn't want a queryset filtered by language, they should use Model.objects.untranslated() """ if not self._language_code: for obj in self.language().iterator(): yield obj else: if self._forced_unique_fields: # In order for select_related to properly load data from # translated models, we have to force django to treat # certain fields as one-to-one relations # before this queryset calls get_cached_row() # We change it back so that things get reset to normal # before execution returns to user code. # It would be more direct and robust if we could wrap # django.db.models.query.get_cached_row() instead, but that's not a class # method, sadly, so we cannot override it just for this query # Enable temporary forced "unique" attribute for related translated models: for field in self._forced_unique_fields: field._unique = True # Pre-fetch all objects: objects = [ o for o in super(TranslationQueryset, self).iterator() ] # Disable temporary forced attribute: for field in self._forced_unique_fields: field._unique = False if type(self.query.select_related) == dict: for obj in objects: self._use_related_translations( obj, self.query.select_related) else: objects = super(TranslationQueryset, self).iterator() for obj in objects: # non-cascade-deletion hack: if not obj.master: yield obj else: yield combine(obj, self.shared_model)
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 master = self.instance stashed = get_cached_translation(master) obj.master = master combined = combine(obj, master.__class__) combined.save() if hasattr(combined, 'save_m2m'): # cannot happen, but feature combined.save_m2m() # could be added, be ready if stashed is None: delattr(master, master._meta.translations_cache) else: setattr(master, master._meta.translations_cache, stashed) return obj
def iterator(self): """ If this queryset is not filtered by a language code yet, it should be filtered first by calling self.language. If someone doesn't want a queryset filtered by language, they should use Model.objects.untranslated() """ if not self._language_code: for obj in self.language().iterator(): yield obj else: for obj in super(TranslationQueryset, self).iterator(): # non-cascade-deletion hack: if not obj.master: yield obj else: yield combine(obj)
def iterator(self): """ If this queryset is not filtered by a language code yet, it should be filtered first by calling self.language. If someone doesn't want a queryset filtered by language, they should use Model.objects.untranslated() """ if not self._language_code: for obj in self.language().iterator(): yield obj else: for obj in super(TranslationQueryset, self).iterator(): # non-cascade-deletion hack: if not obj.master: yield obj else: yield combine(obj, self.shared_model)
def iterator(self): """ If this queryset is not filtered by a language code yet, it should be filtered first by calling self.language. If someone doesn't want a queryset filtered by language, they should use Model.objects.untranslated() """ if not self._language_code: for obj in self.language().iterator(): yield obj else: if self._forced_unique_fields: # In order for select_related to properly load data from # translated models, we have to force django to treat # certain fields as one-to-one relations # before this queryset calls get_cached_row() # We change it back so that things get reset to normal # before execution returns to user code. # It would be more direct and robust if we could wrap # django.db.models.query.get_cached_row() instead, but that's not a class # method, sadly, so we cannot override it just for this query # Enable temporary forced "unique" attribute for related translated models: for field in self._forced_unique_fields: field._unique = True # Pre-fetch all objects: objects = [o for o in super(TranslationQueryset, self).iterator()] # Disable temporary forced attribute: for field in self._forced_unique_fields: field._unique = False if type(self.query.select_related) == dict: for obj in objects: self._use_related_translations(obj, self.query.select_related) else: objects = super(TranslationQueryset, self).iterator() for obj in objects: # non-cascade-deletion hack: if not obj.master: yield obj else: yield combine(obj, self.shared_model)
def _get_real_instances(self, base_results): """ The logic for this method was taken from django-polymorphic by Bert Constantin (https://github.com/bconstantin/django_polymorphic) and was slightly altered to fit the needs of django-hvad. """ # get the primary keys of the shared model results base_ids = [obj.pk for obj in base_results] fallbacks = [ get_language() if lang is None else lang for lang in self.translation_fallbacks ] # get all translations for the fallbacks chosen for those shared models, # note that this query is *BIG* and might return a lot of data, but it's # arguably faster than running one query for each result or even worse # one query per result per language until we find something translations_manager = self.model._meta.translations_model.objects baseqs = translations_manager.select_related('master') translations = baseqs.filter(language_code__in=fallbacks, master__pk__in=base_ids) fallback_objects = defaultdict(dict) # turn the results into a dict of dicts with shared model primary key as # keys for the first dict and language codes for the second dict for obj in translations: fallback_objects[obj.master.pk][obj.language_code] = obj # iterate over the share dmodel results for instance in base_results: translation = None # find the translation for fallback in fallbacks: translation = fallback_objects[instance.pk].get(fallback, None) if translation is not None: break # if we found a translation, yield the combined result if translation: yield combine(translation, self.model) else: # otherwise yield the shared instance only _logger.error("no translation for %s.%s (pk=%s)" % (instance._meta.app_label, instance.__class__.__name__, str(instance.pk))) yield instance
def _get_real_instances(self, base_results): """ The logic for this method was taken from django-polymorphic by Bert Constantin (https://github.com/bconstantin/django_polymorphic) and was slightly altered to fit the needs of django-hvad. """ # get the primary keys of the shared model results base_ids = [obj.pk for obj in base_results] fallbacks = [get_language() if lang is None else lang for lang in self.translation_fallbacks] # get all translations for the fallbacks chosen for those shared models, # note that this query is *BIG* and might return a lot of data, but it's # arguably faster than running one query for each result or even worse # one query per result per language until we find something translations_manager = self.model._meta.translations_model.objects baseqs = translations_manager.select_related('master') translations = baseqs.filter(language_code__in=fallbacks, master__pk__in=base_ids) fallback_objects = defaultdict(dict) # turn the results into a dict of dicts with shared model primary key as # keys for the first dict and language codes for the second dict for obj in translations: fallback_objects[obj.master.pk][obj.language_code] = obj # iterate over the share dmodel results for instance in base_results: translation = None # find the translation for fallback in fallbacks: translation = fallback_objects[instance.pk].get(fallback, None) if translation is not None: break # if we found a translation, yield the combined result if translation: yield combine(translation, self.model) else: # otherwise yield the shared instance only _logger.error("no translation for %s.%s (pk=%s)" % (instance._meta.app_label, instance.__class__.__name__, str(instance.pk))) yield instance
def clean(self): super(BaseTranslationFormSet, self).clean() # Trigger combined instance validation master = self.instance stashed = get_cached_translation(master) for form in self.forms: form.instance.master = master combined = combine(form.instance, master.__class__) exclusions = form._get_validation_exclusions() # fields from the shared model should not be validated exclusions.extend(f.name for f in combined._meta.fields) try: if django.VERSION >= (1, 6): combined.full_clean(exclude=exclusions, validate_unique=form._validate_unique) else: combined.full_clean(exclude=exclusions) except ValidationError as e: form._update_errors(e) if stashed is None: delattr(master, master._meta.translations_cache) else: setattr(master, master._meta.translations_cache, 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 iterator(self): qs = self._clone()._add_language_filter() qs._known_related_objects = { } # super's iterator will attempt to set them if qs._forced_unique_fields: with ForcedUniqueFields(qs._forced_unique_fields): objects = list(super(TranslationQueryset, qs).iterator()) if type(qs.query.select_related) == dict: for obj in objects: qs._use_related_translations(obj, qs.query.select_related) else: objects = super(TranslationQueryset, qs).iterator() for obj in objects: for name in self._hvad_switch_fields: try: setattr(obj.master, name, getattr(obj, name)) except AttributeError: # pragma: no cover pass else: delattr(obj, name) obj = combine(obj, qs.shared_model) # use known objects from self, not qs as we cleared it earlier for field, rel_objs in self._known_related_objects.items(): if hasattr(obj, field.get_cache_name()): # should not happen, but we conform to Django behavior continue #pragma: no cover pk = getattr(obj, field.get_attname()) try: rel_obj = rel_objs[pk] except KeyError: #pragma: no cover pass else: setattr(obj, field.name, rel_obj) yield obj
def __iter__(self): qs = self.queryset._clone()._add_language_filter() qs._iterable_class = ModelIterable qs._known_related_objects = {} if qs._forced_unique_fields: with ForcedUniqueFields(qs._forced_unique_fields): objects = list(qs.iterator()) if type(qs.query.select_related) == dict: for obj in objects: qs._use_related_translations(obj, qs.query.select_related) else: objects = qs.iterator() for obj in objects: for name in qs._hvad_switch_fields: try: setattr(obj.master, name, getattr(obj, name)) except AttributeError: # pragma: no cover pass else: delattr(obj, name) obj = combine(obj, qs.shared_model) # use known objects from self.queryset, not qs as we cleared it earlier for field, rel_objs in self.queryset._known_related_objects.items( ): if hasattr(obj, field.get_cache_name()): continue # pragma: no cover (conform to Django behavior) pk = getattr(obj, field.get_attname()) try: rel_obj = rel_objs[pk] except KeyError: # pragma: no cover pass else: setattr(obj, field.name, rel_obj) yield obj
def iterator(self): """ If this queryset is not filtered by a language code yet, it should be filtered first by calling self.language. If someone doesn't want a queryset filtered by language, they should use Model.objects.untranslated() """ qs = self._clone()._add_language_filter() if qs._forced_unique_fields: # HACK: In order for select_related to properly load data from # translated models, we have to force django to treat # certain fields as one-to-one relations # before this queryset calls get_cached_row() # We change it back so that things get reset to normal # before execution returns to user code. # It would be more direct and robust if we could wrap # django.db.models.query.get_cached_row() instead, but that's not a class # method, sadly, so we cannot override it just for this query with ForcedUniqueFields(qs._forced_unique_fields): # Pre-fetch all objects: objects = list(super(TranslationQueryset, qs).iterator()) if type(qs.query.select_related) == dict: for obj in objects: qs._use_related_translations(obj, qs.query.select_related) else: objects = super(TranslationQueryset, qs).iterator() for obj in objects: # non-cascade-deletion hack: if not obj.master: yield obj else: yield combine(obj, qs.shared_model)
def iterator(self): """ If this queryset is not filtered by a language code yet, it should be filtered first by calling self.language. If someone doesn't want a queryset filtered by language, they should use Model.objects.untranslated() """ qs = self._clone()._add_language_filter() qs._known_related_objects = {} # super's iterator will attempt to set them if qs._forced_unique_fields: # HACK: In order for select_related to properly load data from # translated models, we have to force django to treat # certain fields as one-to-one relations # before this queryset calls get_cached_row() # We change it back so that things get reset to normal # before execution returns to user code. # It would be more direct and robust if we could wrap # django.db.models.query.get_cached_row() instead, but that's not a class # method, sadly, so we cannot override it just for this query with ForcedUniqueFields(qs._forced_unique_fields): # Pre-fetch all objects: objects = list(super(TranslationQueryset, qs).iterator()) if type(qs.query.select_related) == dict: for obj in objects: qs._use_related_translations(obj, qs.query.select_related) else: objects = super(TranslationQueryset, qs).iterator() for obj in objects: # non-cascade-deletion hack: if not obj.master: yield obj else: for name in self._hvad_switch_fields: try: setattr(obj.master, name, getattr(obj, name)) except AttributeError: pass else: delattr(obj, name) obj = combine(obj, qs.shared_model) # use known objects from self, not qs as we cleared it earlier if django.VERSION >= (1, 6): for field, rel_objs in self._known_related_objects.items(): if hasattr(obj, field.get_cache_name()): continue # field was already cached pk = getattr(obj, field.get_attname()) try: rel_obj = rel_objs[pk] except KeyError: pass else: setattr(obj, field.name, rel_obj) else: kro_attname, kro_instance = (getattr(self, 'known_related_object', None) or (None, None)) if kro_instance: setattr(obj, kro_attname, kro_instance) yield obj