mirror of
https://github.com/Hopiu/django-modeltranslation.git
synced 2026-05-09 22:04:48 +00:00
Add 3 new settings, without prefix that deal with external resources. This allows software with elevated security restrictions to function properly.
388 lines
17 KiB
Python
Executable file
388 lines
17 KiB
Python
Executable file
# -*- coding: utf-8 -*-
|
|
from copy import deepcopy
|
|
|
|
from django.contrib import admin
|
|
from django.contrib.admin.options import BaseModelAdmin, flatten_fieldsets, InlineModelAdmin
|
|
from django import forms
|
|
|
|
from modeltranslation import settings as mt_settings
|
|
from modeltranslation.translator import translator
|
|
from modeltranslation.utils import (
|
|
get_translation_fields, build_css_class, build_localized_fieldname, get_language,
|
|
get_language_bidi, unique)
|
|
from modeltranslation.widgets import ClearableWidgetWrapper
|
|
|
|
from django.contrib.contenttypes.admin import GenericTabularInline
|
|
from django.contrib.contenttypes.admin import GenericStackedInline
|
|
|
|
|
|
class TranslationBaseModelAdmin(BaseModelAdmin):
|
|
_orig_was_required = {}
|
|
both_empty_values_fields = ()
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(TranslationBaseModelAdmin, self).__init__(*args, **kwargs)
|
|
self.trans_opts = translator.get_options_for_model(self.model)
|
|
self._patch_prepopulated_fields()
|
|
|
|
def _declared_fieldsets(self):
|
|
# Take custom modelform fields option into account
|
|
if not self.fields and hasattr(self.form, '_meta') and self.form._meta.fields:
|
|
self.fields = self.form._meta.fields
|
|
if self.fieldsets:
|
|
return self._patch_fieldsets(self.fieldsets)
|
|
elif self.fields:
|
|
return [(None, {'fields': self.replace_orig_field(self.fields)})]
|
|
return None
|
|
declared_fieldsets = property(_declared_fieldsets)
|
|
|
|
def formfield_for_dbfield(self, db_field, **kwargs):
|
|
field = super(TranslationBaseModelAdmin, self).formfield_for_dbfield(db_field, **kwargs)
|
|
self.patch_translation_field(db_field, field, **kwargs)
|
|
return field
|
|
|
|
def patch_translation_field(self, db_field, field, **kwargs):
|
|
if db_field.name in self.trans_opts.fields:
|
|
if field.required:
|
|
field.required = False
|
|
field.blank = True
|
|
self._orig_was_required['%s.%s' % (db_field.model._meta, db_field.name)] = True
|
|
|
|
# For every localized field copy the widget from the original field
|
|
# and add a css class to identify a modeltranslation widget.
|
|
try:
|
|
orig_field = db_field.translated_field
|
|
except AttributeError:
|
|
pass
|
|
else:
|
|
orig_formfield = self.formfield_for_dbfield(orig_field, **kwargs)
|
|
field.widget = deepcopy(orig_formfield.widget)
|
|
# if any widget attrs are defined on the form they should be copied
|
|
try:
|
|
field.widget = deepcopy(self.form._meta.widgets[orig_field.name])
|
|
except (AttributeError, TypeError, KeyError):
|
|
pass
|
|
# field.widget = deepcopy(orig_formfield.widget)
|
|
if orig_field.name in self.both_empty_values_fields:
|
|
from modeltranslation.forms import NullableField, NullCharField
|
|
form_class = field.__class__
|
|
if issubclass(form_class, NullCharField):
|
|
# NullableField don't work with NullCharField
|
|
form_class.__bases__ = tuple(
|
|
b for b in form_class.__bases__ if b != NullCharField)
|
|
field.__class__ = type(
|
|
'Nullable%s' % form_class.__name__, (NullableField, form_class), {})
|
|
if (
|
|
(
|
|
db_field.empty_value == 'both' or
|
|
orig_field.name in self.both_empty_values_fields
|
|
) and isinstance(field.widget, (forms.TextInput, forms.Textarea))
|
|
):
|
|
field.widget = ClearableWidgetWrapper(field.widget)
|
|
css_classes = field.widget.attrs.get('class', '').split(' ')
|
|
css_classes.append('mt')
|
|
# Add localized fieldname css class
|
|
css_classes.append(build_css_class(db_field.name, 'mt-field'))
|
|
# Add mt-bidi css class if language is bidirectional
|
|
if(get_language_bidi(db_field.language)):
|
|
css_classes.append('mt-bidi')
|
|
if db_field.language == mt_settings.DEFAULT_LANGUAGE:
|
|
# Add another css class to identify a default modeltranslation widget
|
|
css_classes.append('mt-default')
|
|
if (orig_formfield.required or self._orig_was_required.get(
|
|
'%s.%s' % (orig_field.model._meta, orig_field.name))):
|
|
# In case the original form field was required, make the
|
|
# default translation field required instead.
|
|
orig_formfield.required = False
|
|
orig_formfield.blank = True
|
|
field.required = True
|
|
field.blank = False
|
|
# Hide clearable widget for required fields
|
|
if isinstance(field.widget, ClearableWidgetWrapper):
|
|
field.widget = field.widget.widget
|
|
field.widget.attrs['class'] = ' '.join(css_classes)
|
|
|
|
def _exclude_original_fields(self, exclude=None):
|
|
if exclude is None:
|
|
exclude = tuple()
|
|
if exclude:
|
|
exclude_new = tuple(exclude)
|
|
return exclude_new + tuple(self.trans_opts.fields.keys())
|
|
return tuple(self.trans_opts.fields.keys())
|
|
|
|
def replace_orig_field(self, option):
|
|
"""
|
|
Replaces each original field in `option` that is registered for
|
|
translation by its translation fields.
|
|
|
|
Returns a new list with replaced fields. If `option` contains no
|
|
registered fields, it is returned unmodified.
|
|
|
|
>>> self = TranslationAdmin() # PyFlakes
|
|
>>> print(self.trans_opts.fields.keys())
|
|
['title',]
|
|
>>> get_translation_fields(self.trans_opts.fields.keys()[0])
|
|
['title_de', 'title_en']
|
|
>>> self.replace_orig_field(['title', 'url'])
|
|
['title_de', 'title_en', 'url']
|
|
|
|
Note that grouped fields are flattened. We do this because:
|
|
|
|
1. They are hard to handle in the jquery-ui tabs implementation
|
|
2. They don't scale well with more than a few languages
|
|
3. It's better than not handling them at all (okay that's weak)
|
|
|
|
>>> self.replace_orig_field((('title', 'url'), 'email', 'text'))
|
|
['title_de', 'title_en', 'url_de', 'url_en', 'email_de', 'email_en', 'text']
|
|
"""
|
|
if option:
|
|
option_new = list(option)
|
|
for opt in option:
|
|
if opt in self.trans_opts.fields:
|
|
index = option_new.index(opt)
|
|
option_new[index:index + 1] = get_translation_fields(opt)
|
|
elif isinstance(opt, (tuple, list)) and (
|
|
[o for o in opt if o in self.trans_opts.fields]):
|
|
index = option_new.index(opt)
|
|
option_new[index:index + 1] = self.replace_orig_field(opt)
|
|
option = option_new
|
|
return option
|
|
|
|
def _patch_fieldsets(self, fieldsets):
|
|
if fieldsets:
|
|
fieldsets_new = list(fieldsets)
|
|
for (name, dct) in fieldsets:
|
|
if 'fields' in dct:
|
|
dct['fields'] = self.replace_orig_field(dct['fields'])
|
|
fieldsets = fieldsets_new
|
|
return fieldsets
|
|
|
|
def _patch_prepopulated_fields(self):
|
|
def localize(sources, lang):
|
|
"Append lang suffix (if applicable) to field list"
|
|
def append_lang(source):
|
|
if source in self.trans_opts.fields:
|
|
return build_localized_fieldname(source, lang)
|
|
return source
|
|
return tuple(map(append_lang, sources))
|
|
|
|
prepopulated_fields = {}
|
|
for dest, sources in self.prepopulated_fields.items():
|
|
if dest in self.trans_opts.fields:
|
|
for lang in mt_settings.AVAILABLE_LANGUAGES:
|
|
key = build_localized_fieldname(dest, lang)
|
|
prepopulated_fields[key] = localize(sources, lang)
|
|
else:
|
|
lang = mt_settings.PREPOPULATE_LANGUAGE or get_language()
|
|
prepopulated_fields[dest] = localize(sources, lang)
|
|
self.prepopulated_fields = prepopulated_fields
|
|
|
|
def _get_form_or_formset(self, request, obj, **kwargs):
|
|
"""
|
|
Generic code shared by get_form and get_formset.
|
|
"""
|
|
if self.exclude is None:
|
|
exclude = []
|
|
else:
|
|
exclude = list(self.exclude)
|
|
exclude.extend(self.get_readonly_fields(request, obj))
|
|
if not self.exclude and hasattr(self.form, '_meta') and self.form._meta.exclude:
|
|
# Take the custom ModelForm's Meta.exclude into account only if the
|
|
# ModelAdmin doesn't define its own.
|
|
exclude.extend(self.form._meta.exclude)
|
|
# If exclude is an empty list we pass None to be consistant with the
|
|
# default on modelform_factory
|
|
exclude = self.replace_orig_field(exclude) or None
|
|
exclude = self._exclude_original_fields(exclude)
|
|
kwargs.update({'exclude': exclude})
|
|
|
|
return kwargs
|
|
|
|
def _get_fieldsets_pre_form_or_formset(self):
|
|
"""
|
|
Generic get_fieldsets code, shared by
|
|
TranslationAdmin and TranslationInlineModelAdmin.
|
|
"""
|
|
return self._declared_fieldsets()
|
|
|
|
def _get_fieldsets_post_form_or_formset(self, request, form, obj=None):
|
|
"""
|
|
Generic get_fieldsets code, shared by
|
|
TranslationAdmin and TranslationInlineModelAdmin.
|
|
"""
|
|
base_fields = self.replace_orig_field(form.base_fields.keys())
|
|
fields = base_fields + list(self.get_readonly_fields(request, obj))
|
|
return [(None, {'fields': self.replace_orig_field(fields)})]
|
|
|
|
def get_translation_field_excludes(self, exclude_languages=None):
|
|
"""
|
|
Returns a tuple of translation field names to exclude based on
|
|
`exclude_languages` arg.
|
|
TODO: Currently unused?
|
|
"""
|
|
if exclude_languages is None:
|
|
exclude_languages = []
|
|
excl_languages = []
|
|
if exclude_languages:
|
|
excl_languages = exclude_languages
|
|
exclude = []
|
|
for orig_fieldname, translation_fields in self.trans_opts.fields.items():
|
|
for tfield in translation_fields:
|
|
if tfield.language in excl_languages and tfield not in exclude:
|
|
exclude.append(tfield)
|
|
return tuple(exclude)
|
|
|
|
def get_readonly_fields(self, request, obj=None):
|
|
"""
|
|
Hook to specify custom readonly fields.
|
|
"""
|
|
return self.replace_orig_field(self.readonly_fields)
|
|
|
|
|
|
class TranslationAdmin(TranslationBaseModelAdmin, admin.ModelAdmin):
|
|
# TODO: Consider addition of a setting which allows to override the fallback to True
|
|
group_fieldsets = False
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(TranslationAdmin, self).__init__(*args, **kwargs)
|
|
self._patch_list_editable()
|
|
|
|
def _patch_list_editable(self):
|
|
if self.list_editable:
|
|
editable_new = list(self.list_editable)
|
|
display_new = list(self.list_display)
|
|
for field in self.list_editable:
|
|
if field in self.trans_opts.fields:
|
|
index = editable_new.index(field)
|
|
display_index = display_new.index(field)
|
|
translation_fields = get_translation_fields(field)
|
|
editable_new[index:index + 1] = translation_fields
|
|
display_new[display_index:display_index + 1] = translation_fields
|
|
self.list_editable = editable_new
|
|
self.list_display = display_new
|
|
|
|
def _group_fieldsets(self, fieldsets):
|
|
# Fieldsets are not grouped by default. The function is activated by
|
|
# setting TranslationAdmin.group_fieldsets to True. If the admin class
|
|
# already defines a fieldset, we leave it alone and assume the author
|
|
# has done whatever grouping for translated fields they desire.
|
|
if not self.declared_fieldsets and self.group_fieldsets is True:
|
|
flattened_fieldsets = flatten_fieldsets(fieldsets)
|
|
|
|
# Create a fieldset to group each translated field's localized fields
|
|
untranslated_fields = [
|
|
f.name for f in self.opts.fields if (
|
|
# Exclude the primary key field
|
|
f is not self.opts.auto_field and
|
|
# Exclude non-editable fields
|
|
f.editable and
|
|
# Exclude the translation fields
|
|
not hasattr(f, 'translated_field') and
|
|
# Honour field arguments. We rely on the fact that the
|
|
# passed fieldsets argument is already fully filtered
|
|
# and takes options like exclude into account.
|
|
f.name in flattened_fieldsets
|
|
)
|
|
]
|
|
# TODO: Allow setting a label
|
|
fieldsets = [('', {'fields': untranslated_fields},)] if untranslated_fields else []
|
|
|
|
temp_fieldsets = {}
|
|
for orig_field, trans_fields in self.trans_opts.fields.items():
|
|
trans_fieldnames = [f.name for f in sorted(trans_fields, key=lambda x: x.name)]
|
|
if any(f in trans_fieldnames for f in flattened_fieldsets):
|
|
# Extract the original field's verbose_name for use as this
|
|
# fieldset's label - using ugettext_lazy in your model
|
|
# declaration can make that translatable.
|
|
label = self.model._meta.get_field(orig_field).verbose_name.capitalize()
|
|
temp_fieldsets[orig_field] = (label, {
|
|
'fields': trans_fieldnames,
|
|
'classes': ('mt-fieldset',)
|
|
})
|
|
|
|
fields_order = unique(f.translated_field.name for f in self.opts.fields if
|
|
hasattr(f, 'translated_field') and f.name in flattened_fieldsets)
|
|
for field_name in fields_order:
|
|
fieldsets.append(temp_fieldsets.pop(field_name))
|
|
assert not temp_fieldsets # cleaned
|
|
|
|
return fieldsets
|
|
|
|
def get_form(self, request, obj=None, **kwargs):
|
|
kwargs = self._get_form_or_formset(request, obj, **kwargs)
|
|
return super(TranslationAdmin, self).get_form(request, obj, **kwargs)
|
|
|
|
def get_fieldsets(self, request, obj=None):
|
|
if self.declared_fieldsets:
|
|
return self._get_fieldsets_pre_form_or_formset()
|
|
return self._group_fieldsets(
|
|
self._get_fieldsets_post_form_or_formset(
|
|
request, self.get_form(request, obj, fields=None), obj))
|
|
|
|
|
|
class TranslationInlineModelAdmin(TranslationBaseModelAdmin, InlineModelAdmin):
|
|
def get_formset(self, request, obj=None, **kwargs):
|
|
kwargs = self._get_form_or_formset(request, obj, **kwargs)
|
|
return super(TranslationInlineModelAdmin, self).get_formset(request, obj, **kwargs)
|
|
|
|
def get_fieldsets(self, request, obj=None):
|
|
# FIXME: If fieldsets are declared on an inline some kind of ghost
|
|
# fieldset line with just the original model verbose_name of the model
|
|
# is displayed above the new fieldsets.
|
|
if self.declared_fieldsets:
|
|
return self._get_fieldsets_pre_form_or_formset()
|
|
form = self.get_formset(request, obj, fields=None).form
|
|
return self._get_fieldsets_post_form_or_formset(request, form, obj)
|
|
|
|
|
|
class TranslationTabularInline(TranslationInlineModelAdmin, admin.TabularInline):
|
|
pass
|
|
|
|
|
|
class TranslationStackedInline(TranslationInlineModelAdmin, admin.StackedInline):
|
|
pass
|
|
|
|
|
|
class TranslationGenericTabularInline(TranslationInlineModelAdmin, GenericTabularInline):
|
|
pass
|
|
|
|
|
|
class TranslationGenericStackedInline(TranslationInlineModelAdmin, GenericStackedInline):
|
|
pass
|
|
|
|
|
|
class TabbedDjangoJqueryTranslationAdmin(TranslationAdmin):
|
|
"""
|
|
Convenience class which includes the necessary media files for tabbed
|
|
translation fields. Reuses Django's internal jquery version.
|
|
"""
|
|
class Media:
|
|
js = (
|
|
'modeltranslation/js/force_jquery.js',
|
|
mt_settings.JQUERY_UI_URL,
|
|
mt_settings.JQUERY_MB_BROWSER_URL,
|
|
'modeltranslation/js/tabbed_translation_fields.js',
|
|
)
|
|
css = {
|
|
'all': ('modeltranslation/css/tabbed_translation_fields.css',),
|
|
}
|
|
|
|
|
|
class TabbedExternalJqueryTranslationAdmin(TranslationAdmin):
|
|
"""
|
|
Convenience class which includes the necessary media files for tabbed
|
|
translation fields. Loads recent jquery version from a cdn.
|
|
"""
|
|
class Media:
|
|
js = (
|
|
mt_settings.JQUERY_URL,
|
|
mt_settings.JQUERY_UI_URL,
|
|
mt_settings.JQUERY_MB_BROWSER_URL,
|
|
'modeltranslation/js/tabbed_translation_fields.js',
|
|
)
|
|
css = {
|
|
'screen': ('modeltranslation/css/tabbed_translation_fields.css',),
|
|
}
|
|
|
|
|
|
TabbedTranslationAdmin = TabbedDjangoJqueryTranslationAdmin
|