diff --git a/tof/admin.py b/tof/admin.py index 28c235b..9ed5134 100644 --- a/tof/admin.py +++ b/tof/admin.py @@ -48,9 +48,9 @@ def has_delete_permission(self, *args, **kwargs): @admin.register(Language) class LanguageAdmin(admin.ModelAdmin): - search_fields = ('iso', ) + search_fields = ('iso',) list_display = ('iso', 'is_active') - list_editable = ('is_active', ) + list_editable = ('is_active',) def get_search_results(self, request, queryset, search_term): queryset, use_distinct = super().get_search_results(request, queryset, search_term) @@ -70,9 +70,9 @@ class TranslatableFieldAdmin(admin.ModelAdmin): 'name', 'title', ), - }), ) + }),) - autocomplete_fields = ('content_type', ) + autocomplete_fields = ('content_type',) def delete_queryset(self, request, queryset): for obj in queryset: @@ -108,7 +108,7 @@ def _changeform_view(self, request, object_id, form_url, extra_context): class TranslationAdmin(admin.ModelAdmin): form = TranslationsForm list_display = ('content_object', 'lang', 'field', 'value') - list_filter = ('content_type', ) + list_filter = ('content_type',) fieldsets = ((None, { 'fields': ( ('field', 'lang'), @@ -168,7 +168,7 @@ class TranslationInline(GenericInlineModelAdmin): @property def media(self): media = super().media - js = ('tof/js/translation_inline.js', ) + js = ('tof/js/translation_inline.js',) return media + forms.Media(js=js) @@ -186,9 +186,10 @@ class TofAdmin(admin.ModelAdmin): def get_readonly_fields(self, request, obj=None): response = list(super().get_readonly_fields(request, obj)) - field_tof = getattr(self.model._meta, '_field_tof', {}).get('by_name') + field_tof = [field for field in self.model._meta.fields if + isinstance(getattr(self.model, field.name), TranslatableField)] if field_tof and any(issubclass(c, TranslationInline) for c in self.inlines): - response.extend(field_tof.keys()) + response.extend(field_tof) return tuple(response) def get_form(self, request, obj=None, change=False, **kwargs): diff --git a/tof/decorators.py b/tof/decorators.py index 8a9c678..6c3bdc3 100644 --- a/tof/decorators.py +++ b/tof/decorators.py @@ -29,20 +29,19 @@ def wrapper(*args, **kwargs): def tof_filter(func): @wraps(func) def wrapper(self, *args, **kwargs): - tof_fields = getattr(self.model._meta, '_field_tof', {}).get('by_name') new_args, new_kwargs = args, kwargs - if tof_fields: + if hasattr(self.model, '_translations'): new_args = [] for arg in args: if isinstance(arg, Q): # modify Q objects (warning: recursion ahead) - arg = expand_q_filters(arg, tof_fields) + arg = expand_q_filters(arg, self.model) new_args.append(arg) new_kwargs = {} for key, value in list(kwargs.items()): # modify kwargs (warning: recursion ahead) - new_key, new_value, _ = expand_filter(tof_fields, key, value) + new_key, new_value, _ = expand_filter(self.model, key, value) new_kwargs.update({new_key: new_value}) return func(self, *new_args, **new_kwargs) @@ -50,28 +49,29 @@ def wrapper(self, *args, **kwargs): return wrapper -def expand_q_filters(q, tof_fields): +def expand_q_filters(q, model): new_children = [] for qi in q.children: if isinstance(qi, tuple): # this child is a leaf node: in Q this is a 2-tuple of: # (filter parameter, value) - key, value, repl = expand_filter(tof_fields, *qi) + key, value, repl = expand_filter(model, *qi) query = Q(**{key: value}) if repl: query |= Q(**{qi[0]: qi[1]}) new_children.append(query) else: # this child is another Q node: recursify! - new_children.append(expand_q_filters(qi, tof_fields)) + new_children.append(expand_q_filters(qi, model)) q.children = new_children return q -def expand_filter(tof_fields, key, value): +def expand_filter(model, key, value): field_name, sep, lookup = key.partition('__') - field = tof_fields.get(field_name) - if field: + field = getattr(model, field_name) + from .models import TranslatableField + if isinstance(field, TranslatableField): query = Q(**{f'value{sep}{lookup}': value}) if DEFAULT_FILTER_LANGUAGE == '__all__': pass diff --git a/tof/forms.py b/tof/forms.py index 9701fb1..fd69de8 100644 --- a/tof/forms.py +++ b/tof/forms.py @@ -133,9 +133,11 @@ class TranslationFieldModelForm(forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - _field_tof = getattr(self._meta.model._meta, '_field_tof', {}).get('by_name') - if _field_tof: + from .models import TranslatableField + field_tof = [field for field in self._meta.model._meta.fields if + isinstance(getattr(self._meta.model, field.name), TranslatableField)] + if field_tof: from .fields import TranslatableFieldFormField - for name in set(_field_tof.keys()) - set(self.only_current_lang): + for name in set(map(lambda x: x.name, field_tof)) - set(self.only_current_lang): if name in self.fields: self.fields[name] = TranslatableFieldFormField(self.fields[name]) diff --git a/tof/managers.py b/tof/managers.py index 67b52a9..56986a6 100644 --- a/tof/managers.py +++ b/tof/managers.py @@ -5,6 +5,8 @@ # @Last Modified time: 2019-11-19 16:40:49 from django.db import models +from django.utils.translation import get_language + from .decorators import tof_filter, tof_prefetch @@ -21,6 +23,30 @@ def exclude(self, *args, **kwargs): def get(self, *args, **kwargs): return super().get(*args, **kwargs) + def order_by(self, *args, **kwargs): + from .models import Translation, TranslatableField + new_args = [] + subqueries = {} + lang = get_language() + for arg in args: + field_name, rev = arg, '' + if arg.startswith('-'): + field_name, rev = arg[1:], arg[0] + field = getattr(self.model, field_name) + if isinstance(field, TranslatableField): + subquery = Translation.objects.filter( + object_id=models.OuterRef('pk'), + lang=lang, + field=f'{field.content_type.app_label}.{field.content_type.model}.{field_name}' + ).values('value') + new_args.append(f'{rev}_{field_name}') + subqueries[f'_{field_name}'] = subquery + else: + new_args.append(arg) + if args: + self = self.model.objects.annotate(**subqueries) + return super().order_by(*new_args, **kwargs) + class TranslationsQuerySet(DecoratedMixIn, models.QuerySet): pass diff --git a/tof/migrations/0004_auto_20200218_1229.py b/tof/migrations/0004_auto_20200218_1229.py new file mode 100644 index 0000000..3c3f0fb --- /dev/null +++ b/tof/migrations/0004_auto_20200218_1229.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.3 on 2020-02-18 09:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tof', '0003_auto_20191127_0907'), + ] + + operations = [ + migrations.AlterField( + model_name='translatablefield', + name='id', + field=models.CharField(max_length=300, primary_key=True, serialize=False), + ), + ] diff --git a/tof/models.py b/tof/models.py index 5186444..8fccf4f 100644 --- a/tof/models.py +++ b/tof/models.py @@ -59,10 +59,10 @@ def __init__(self, *args, **kwargs): @cached_property def _all_translations(self): - attrs, names_mapper = vars(self), self._meta._field_tof['by_id'] + attrs = vars(self) for trans in self._translations.all(): - name = names_mapper[trans.field_id].name - attrs[name] = trans_obj = attrs.get(name) or TranslatableText() + *remains, field_name = trans.field_id.rpartition('.') + attrs[field_name] = trans_obj = attrs.get(field_name) or TranslatableText() vars(trans_obj)[trans.lang_id] = trans.value return attrs @@ -74,8 +74,10 @@ def get_translation(self, name): def save(self, *args, **kwargs): super().save(*args, **kwargs) - for translated_field in self._meta._field_tof['by_id'].values(): - translated_field.save_translation(self) + for field in self._meta.fields: + field_tof = getattr(self._meta.model, field.name) + if isinstance(field_tof, TranslatableField): + field_tof.save_translation(self) class TranslatableField(models.Model): @@ -93,11 +95,13 @@ class Meta: on_delete=models.CASCADE, related_name='translatablefields', ) + id = models.CharField(max_length=300, primary_key=True) def __str__(self): return f'{self.content_type.model}|{self.title}' def save(self, *args, **kwargs): + self.id = f'{self.content_type.app_label}.{self.content_type.model}.{self.name}' super().save(*args, **kwargs) self.add_translation_to_class() @@ -118,7 +122,6 @@ def __set__(self, instance, 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) @@ -131,9 +134,10 @@ 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'): + if not issubclass(cls, TranslationFieldMixin): cls.__bases__ = (TranslationFieldMixin, ) + cls.__bases__ - cls._meta._field_tof = {'by_name': {}, 'by_id': {}} + field = TranslationFieldMixin._meta.get_field('_translations') + field.contribute_to_class(cls, '_translations') 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,20 +148,31 @@ 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') + field = cls._meta.get_field(self.name) + field.old_descriptor = field.descriptor_class + field.descriptor_class = TranslatableField setattr( cls, - cls._meta._field_tof['by_name'].setdefault(cls._meta._field_tof['by_id'].setdefault(self.id, self).name, self).name, + self.name, self, ) def remove_translation_from_class(self): cls = self.content_type.model_class() - cls._meta._field_tof['by_name'].pop(cls._meta._field_tof['by_id'].pop(self.id, self).name, None) delattr(cls, self.name) field = cls._meta.get_field(self.name) - field.contribute_to_class(cls, self.name) - if not cls._meta._field_tof['by_id']: - del cls._meta._field_tof + field.descriptor_class = field.old_descriptor + delattr(field, 'old_descriptor') + setattr( + cls, + self.name, + field.descriptor_class(field), + ) + has_tof = False + for field in cls._meta.fields: + if isinstance(getattr(cls, field.name), TranslatableField): + has_tof = True + if not has_tof: cls.__bases__ = tuple(base for base in cls.__bases__ if base != TranslationFieldMixin) # important! if CHANGE_DEFAULT_MANAGER and isinstance(cls._default_manager, TranslationManager): delattr(cls, cls._default_manager.default_name) diff --git a/tof/tests/tests.py b/tof/tests/tests.py index e4934cc..5c1a81f 100644 --- a/tof/tests/tests.py +++ b/tof/tests/tests.py @@ -48,8 +48,10 @@ def create_field(name='title', cls=None): def clean_model(cls, attr='title'): if issubclass(cls, TranslationFieldMixin): - for fld in {**cls._meta._field_tof['by_id']}.values(): - fld.remove_translation_from_class() + for field in cls._meta.fields: + field_tof = getattr(cls, field.name) + if isinstance(field_tof, TranslatableField): + field_tof.remove_translation_from_class() class TranslatableFieldTestCase(TestCase): @@ -64,13 +66,9 @@ def test_save(self): log = LogEntry.objects.first() self.assertNotIsInstance(wine1, TranslationFieldMixin) self.assertNotIsInstance(log, TranslationFieldMixin) - self.assertIsNone(vars(LogEntry._meta).get('_field_tof')) create_field() self.assertIsInstance(wine1, TranslationFieldMixin) - self.assertIsNotNone(vars(Wine._meta).get('_field_tof')) - self.assertIsNone(vars(LogEntry._meta).get('_field_tof')) create_field('change_message', LogEntry) - self.assertIsNotNone(vars(LogEntry._meta).get('_field_tof')) def test_delete(self): create_field()