From fcc52a08b3ee71c2b8b944685d3c7a06e7413951 Mon Sep 17 00:00:00 2001 From: baobab-ai Date: Thu, 6 Feb 2020 15:17:24 +0300 Subject: [PATCH 1/2] Using descriptor_class from Django 3.0 --- tof/descriptors.py | 26 ++++++++++++++++++++++++++ tof/fields.py | 16 +++++++++++++--- tof/mixins.py | 37 +++++++++++++++++++++++++++++++++++++ tof/models.py | 36 +++++++++++++++--------------------- tof/tests/tests.py | 3 ++- 5 files changed, 93 insertions(+), 25 deletions(-) create mode 100644 tof/descriptors.py create mode 100644 tof/mixins.py diff --git a/tof/descriptors.py b/tof/descriptors.py new file mode 100644 index 0000000..6cacffb --- /dev/null +++ b/tof/descriptors.py @@ -0,0 +1,26 @@ +from .utils import TranslatableText + + +class TranslatableFieldDescriptor: + def __init__(self, field): + self.field = field + self.name = field.name + self.id = field.id + + def __get__(self, obj, type_): + return obj.get_translation(self.name) if obj else vars(type_).get(self.name) + + def __set__(self, obj, value): + attrs = vars(obj) + if isinstance(value, TranslatableText): + attrs[self.name] = value + else: + translation = attrs[self.name] = obj.get_translation(self.name) + vars(translation)[translation.get_lang() if '_end_init' in attrs else '_origin'] = str(value) + + def __delete__(self, obj): + vars(self).pop(self.name, None) + obj._meta._field_tof['by_name'].pop( + obj._meta._field_tof['by_id'].pop(self.id, self).name, + None + ) diff --git a/tof/fields.py b/tof/fields.py index 078120a..bb6f2a6 100644 --- a/tof/fields.py +++ b/tof/fields.py @@ -4,19 +4,29 @@ # @Last Modified by: MaxST # @Last Modified time: 2019-11-29 12:04:04 from django.core.exceptions import ValidationError -from django.forms.fields import CharField, MultiValueField +from django.db import models +from django.forms import fields from django.utils.translation import get_language +from .mixins import TranslatableFieldMixin from .forms import TranslatableFieldHiddenWidget, TranslatableFieldWidget from .utils import TranslatableText -class TranslatableFieldFormField(MultiValueField): +class TranslatableCharField(TranslatableFieldMixin, models.CharField): + pass + + +class TranslatableTextField(TranslatableFieldMixin, models.TextField): + pass + + +class TranslatableFieldFormField(fields.MultiValueField): widget = TranslatableFieldWidget hidden_widget = TranslatableFieldHiddenWidget def __init__(self, field=None, *args, **kwargs): - fld = (field or CharField(*args, **kwargs)) + fld = (field or fields.CharField(*args, **kwargs)) fld.widget.attrs.update({'lang': get_language()}) widget = self.widget(fld.widget) super().__init__((fld,), widget=widget) diff --git a/tof/mixins.py b/tof/mixins.py new file mode 100644 index 0000000..e811fec --- /dev/null +++ b/tof/mixins.py @@ -0,0 +1,37 @@ +import inspect + +from django.db import models + +from .descriptors import TranslatableFieldDescriptor + + +class TranslatableFieldMixin: + descriptor_class = TranslatableFieldDescriptor + + def __init__(self, content_type, title, id, *args, **kwargs): + self.content_type = content_type + self.title = title + self.id = id + super().__init__(*args, **kwargs) + + @classmethod + def from_field(cls, content_type, title, id, field): + """Creating a copy of a field with a descriptor class. + :param content_type: Object of ContentType Model. Owner of the Field. + :param title: Name of field. + :param id: Identification of TranslatableField object. + :param field: Object of models.Field. Original field for copying. + :return: TranslatableCharField or TranslatableTextField - copy of field with TranslatableFieldMixin. + """ + init_args = inspect.signature(models.Field.__init__).parameters + data = vars(field) + + extra_data = {arg.name: data[arg.name] for arg in init_args.values() if data.get(arg.name)} + + if isinstance(field, models.CharField): + extra_data['max_length'] = data['max_length'] + + return cls(content_type, title, id, **extra_data) + + def contribute_to_class(self, cls): + setattr(cls, self.name, self.descriptor_class(self)) diff --git a/tof/models.py b/tof/models.py index 5186444..5c5be46 100644 --- a/tof/models.py +++ b/tof/models.py @@ -13,6 +13,7 @@ from django.utils.functional import cached_property from django.utils.translation import gettext_lazy as _ +from .fields import TranslatableCharField, TranslatableTextField from .managers import TranslationManager from .settings import CHANGE_DEFAULT_MANAGER from .utils import TranslatableText @@ -105,21 +106,6 @@ def delete(self, *args, **kwargs): self.remove_translation_from_class() super().delete(*args, **kwargs) - def __get__(self, instance, instance_cls): - return instance.get_translation(self.name) if instance else vars(instance_cls).get(self.name) - - def __set__(self, instance, value): - attrs = vars(instance) - if isinstance(value, TranslatableText): - attrs[self.name] = value - else: - translation = attrs[self.name] = instance.get_translation(self.name) - vars(translation)[translation.get_lang() if '_end_init' in attrs else '_origin'] = str(value) - - def __delete__(self, instance): - vars(self).pop(self.name, None) - instance._meta._field_tof['by_name'].pop(instance._meta._field_tof['by_id'].pop(self.id, self).name, None) - def save_translation(self, instance): val = instance.get_translation(self.name) if val: @@ -131,9 +117,11 @@ def save_translation(self, instance): def add_translation_to_class(self, trans_mng=None): cls = self.content_type.model_class() + if not hasattr(cls._meta, '_field_tof'): cls.__bases__ = (TranslationFieldMixin, ) + cls.__bases__ cls._meta._field_tof = {'by_name': {}, 'by_id': {}} + if CHANGE_DEFAULT_MANAGER and not isinstance(cls._default_manager, TranslationManager): origin = cls.objects new_mng_cls = type(f'TranslationManager{cls.__name__}', (TranslationManager, type(origin)), {}) @@ -144,11 +132,17 @@ def add_translation_to_class(self, trans_mng=None): del cls.objects trans_mng.contribute_to_class(cls, 'objects') origin.contribute_to_class(cls, 'objects_origin') - setattr( - cls, - cls._meta._field_tof['by_name'].setdefault(cls._meta._field_tof['by_id'].setdefault(self.id, self).name, self).name, - self, - ) + + cls._meta._field_tof = { + 'by_id': {self.id: self}, + 'by_name': {self.name: self} + } + + field = getattr(cls, self.name).field + field_class = TranslatableCharField if isinstance(field, models.CharField) else TranslatableTextField + + field_mixin = field_class.from_field(self.content_type, self.name, self.id, field) + field_mixin.contribute_to_class(cls) def remove_translation_from_class(self): cls = self.content_type.model_class() @@ -175,7 +169,7 @@ class Meta: ordering = ['iso'] iso = models.CharField(max_length=2, unique=True, primary_key=True) - is_active = models.BooleanField(_(u'Active'), default=True) + is_active = models.BooleanField(_('Active'), default=True) def __str__(self): return self.iso diff --git a/tof/tests/tests.py b/tof/tests/tests.py index e4934cc..a0e8c7a 100644 --- a/tof/tests/tests.py +++ b/tof/tests/tests.py @@ -27,6 +27,7 @@ from tof.models import ( Language, TranslatableField, Translation, TranslationFieldMixin, ) +from tof.descriptors import TranslatableFieldDescriptor from tof.settings import FALLBACK_LANGUAGES from tof.utils import TranslatableText @@ -137,7 +138,7 @@ def test_save(self): self.assertEqual(wine1.title.nl, title_nl) def test_get(self): - self.assertIsInstance(Wine.title, TranslatableField) + self.assertIsInstance(Wine.title, TranslatableFieldDescriptor) def test_prefetch(self): wine1 = Wine.objects.first() From 48ff6baf5e47440cb481d6ea33318b7f4a755704 Mon Sep 17 00:00:00 2001 From: baobab-ai Date: Fri, 7 Feb 2020 16:22:17 +0300 Subject: [PATCH 2/2] change the way of populating _field_tof --- tof/models.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tof/models.py b/tof/models.py index 5c5be46..cc1c093 100644 --- a/tof/models.py +++ b/tof/models.py @@ -133,10 +133,8 @@ def add_translation_to_class(self, trans_mng=None): trans_mng.contribute_to_class(cls, 'objects') origin.contribute_to_class(cls, 'objects_origin') - cls._meta._field_tof = { - 'by_id': {self.id: self}, - 'by_name': {self.name: self} - } + cls._meta._field_tof['by_id'].update({self.id: self}) + cls._meta._field_tof['by_name'].update({self.name: self}) field = getattr(cls, self.name).field field_class = TranslatableCharField if isinstance(field, models.CharField) else TranslatableTextField