Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions tof/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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:
Expand Down Expand Up @@ -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'),
Expand Down Expand Up @@ -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)


Expand All @@ -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):
Expand Down
20 changes: 10 additions & 10 deletions tof/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,49 +29,49 @@ 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)

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
Expand Down
8 changes: 5 additions & 3 deletions tof/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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])
26 changes: 26 additions & 0 deletions tof/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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
Expand Down
18 changes: 18 additions & 0 deletions tof/migrations/0004_auto_20200218_1229.py
Original file line number Diff line number Diff line change
@@ -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),
),
]
41 changes: 28 additions & 13 deletions tof/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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):
Expand All @@ -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()

Expand All @@ -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)
Expand All @@ -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)), {})
Expand All @@ -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)
Expand Down
10 changes: 4 additions & 6 deletions tof/tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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()
Expand Down