Source code for tof.models

from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.contrib.contenttypes.models import ContentType
from django.db import models, transaction
from django.db.models import Q, QuerySet
from django.utils.safestring import mark_safe
from django.utils.translation.trans_real import DjangoTranslation

from . import _
from .mixins import apply_mixins
from .utils import TranslatableText


[docs] class TranslationQueryset(QuerySet):
[docs] @transaction.atomic def save_translations(self, instance): vars(instance).pop('get_from_db', None) to_update = [*instance.collect('updated')] self.bulk_update((obj for obj in to_update if obj.value), ['value']) self.bulk_delete(obj.pk for obj in to_update if not obj.value) self.bulk_create(instance.collect('created'))
[docs] def bulk_delete(self, objs): return self.filter(pk__in=[*objs]).delete()
[docs] class Translation(models.Model): class Meta: verbose_name = _('Translation') verbose_name_plural = _('Translation') unique_together = ('object_id', 'field', 'lang') content_type = models.ForeignKey(ContentType, limit_choices_to=~Q(app_label='tof'), on_delete=models.DO_NOTHING, related_name='translations',) object_id = models.PositiveIntegerField(help_text=_('First set the field')) content_object = GenericForeignKey() field = models.ForeignKey('TranslatableField', related_name='translations', on_delete=models.CASCADE) lang = models.ForeignKey('Language', related_name='translations', to_field='iso', limit_choices_to=Q(is_active=True), on_delete=models.DO_NOTHING) value = models.TextField(_('Value'), help_text=_('Value field')) objects = TranslationQueryset.as_manager() def __str__(self): return f'{self.value or ""}' def __repr__(self): return f'{self.content_object}.{self.field.name}.{self.lang} = "{self}"'
[docs] def update(self, lang, value): self.value = value[lang] return self
[docs] @classmethod def create(cls, field, instance, lang, value): return cls(field=field, content_object=instance, lang_id=lang, value=value)
[docs] class TranslatableFieldQuerySet(QuerySet):
[docs] def patch_fields(self): for field in self.all(): field.patch_unpatch('patch')
[docs] class TranslatableField(models.Model): replaced_field = None translator_cls = TranslatableText class Meta: verbose_name = _('Translatable field') verbose_name_plural = _('Translatable fields') ordering = ('content_type', 'name') unique_together = ('content_type', 'name') name = models.CharField(_('Field name'), max_length=250, help_text=_('Name field')) content_type = models.ForeignKey(ContentType, limit_choices_to=~Q(app_label='tof'), on_delete=models.CASCADE, related_name='translatablefields') objects = TranslatableFieldQuerySet.as_manager() def __str__(self): return f'{self.content_type.model}|{self.name}'
[docs] def pre_save(self, instance, *__): value = vars(instance).get(self.name) return getattr(value, '_origin', None) or value
[docs] def save(self, *args, **kwargs): super().save(*args, **kwargs) self.patch_unpatch('patch')
[docs] def delete(self, *args, **kwargs): self.patch_unpatch('unpatch') super().delete(*args, **kwargs)
[docs] def is_translatable(self, value): return isinstance(value, self.translator_cls)
[docs] def get_saved(self, instance): return instance.get_from_db().get(self.pk) or {}
[docs] def collect(self, instance, action): value = vars(instance).get(self.name) if self.is_translatable(value): yield from getattr(self, f'collect_{action}')(self.get_saved(instance), value, instance)
[docs] @staticmethod def collect_updated(saved, value, *__): yield from (translation.update(lang, value) for lang, translation in saved.items() if translation.value != value[lang])
[docs] def collect_created(self, saved, value, instance): model = getattr(self, 'translations').model yield from (model.create(self, instance, lang, value) for lang, value in value.iter if not saved.get(lang))
def __get__(self, instance, instance_cls): if instance: value = vars(instance).get(self.name) if hasattr(instance, '_end_init') and not self.is_translatable(value): value = self.translator_cls().update(_origin=value, **self.get_saved(instance)) return value return vars(instance_cls).get(self.name) def __set__(self, instance, value): if hasattr(instance, '_end_init') and not self.is_translatable(value): value = getattr(instance, self.name).update_current(value) vars(instance)[self.name] = value def __delete__(self, instance): vars(instance).pop(self.name, None)
[docs] def patch_unpatch(self, patch): target_cls = getattr(self.content_type, 'model_class')() if target_cls: if not hasattr(target_cls, 'translations'): GenericRelation('tof.Translation', verbose_name=_('Translation')).contribute_to_class(target_cls, 'translations') target_cls.translations.fields = {} getattr(self, f'{patch}_field')(target_cls, target_cls.translations.fields) apply_mixins(patch, target_cls, target_cls.translations.fields)
[docs] def patch_field(self, target_cls, fields): if getattr(target_cls, self.name, None): self.replaced_field = getattr(target_cls, self.name, None) setattr(self.replaced_field.field, 'pre_save', lambda *args, **kwargs: self.pre_save(*args, *kwargs)) setattr(target_cls, self.name, self) fields[self.pk] = self.name
[docs] def unpatch_field(self, target_cls, fields): if getattr(target_cls, self.name, None): field = getattr(target_cls, self.name, None) if hasattr(field, 'replaced_field'): delattr(field.replaced_field.field, 'pre_save') setattr(target_cls, self.name, field.replaced_field) fields.pop(self.pk, None)
[docs] class LanguageQuerySet(QuerySet):
[docs] def active(self): return self.filter(is_active=True)
[docs] def get_choices_by(self, *args, **kwargs): return ((obj.iso, str(obj)) for obj in self.active())
[docs] class Language(models.Model): class Meta: verbose_name = _('Language') verbose_name_plural = _('Languages') ordering = ['iso'] iso = models.CharField(max_length=2, unique=True) is_active = models.BooleanField(_(u'Active'), default=True) objects = LanguageQuerySet.as_manager() def __str__(self): return f'{self.iso or str()}'
[docs] class ThroughLanguage(models.Model):
[docs] class Meta: abstract = True
language = models.ForeignKey(Language, on_delete=models.CASCADE, to_field='iso', limit_choices_to=Q(is_active=True))
[docs] class StaticMessageTranslation(models.Model): CACHE = {} base_translator_cls = DjangoTranslation.gettext class Meta: verbose_name = _('Static translation') verbose_name_plural = _('Static translations') indexes = [models.Index(fields=['message'])] message = models.CharField(_('Message'), max_length=1000) translation = models.CharField(_('Translation'), max_length=1000) objects = QuerySet.as_manager() def __str__(self): return f'{self.translation or self.message or ""}'
[docs] def languages(self): iterator = getattr(self.translation, 'iter', ()) return ', '.join((lang for lang, __ in iterator)) if iterator else ''
languages.admin_order_field = 'translations'
[docs] @staticmethod def gettext(translator, message): cls = StaticMessageTranslation if message not in cls.CACHE: try: messages = cls.objects.filter(message=message)[:2] if len(messages): cls.CACHE[message], *messages = messages if messages: cls.objects.exclude(pk=cls.CACHE[message].pk).filter(message=message).delete() else: cls.CACHE[message] = cls.objects.create(message=message) except Exception as error: print(message, repr(error), error, getattr(error, 'args', None), error.__context__, error.__cause__,) return str(message) cached = cls.CACHE[message] if not isinstance(cached.translation, TranslatableText): cached.translation = TranslatableText() if not cached.translation.current: translation = cls.base_translator_cls(translator, message) if translation not in vars(cached.translation).values(): cached.translation = translation cached.save() return mark_safe(cached.translation)
[docs] @classmethod def patch_djangotranslation(cls): DjangoTranslation.gettext = cls.gettext
[docs] def save(self, *args, **kwargs): try: type(self).CACHE[self.message] = self super().save(*args, **kwargs) except Exception as error: type(self).CACHE.pop(self.message, None) raise Exception from error