mirror of
https://github.com/Hopiu/django-modeltranslation.git
synced 2026-04-21 05:30:59 +00:00
Merge branch 'ultimate-null-formfield-solution'
This commit is contained in:
commit
23d014e395
6 changed files with 76 additions and 27 deletions
|
|
@ -19,6 +19,7 @@ from modeltranslation.widgets import ClearableWidgetWrapper
|
|||
|
||||
class TranslationBaseModelAdmin(BaseModelAdmin):
|
||||
_orig_was_required = {}
|
||||
both_empty_values_fields = ()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(TranslationBaseModelAdmin, self).__init__(*args, **kwargs)
|
||||
|
|
@ -57,7 +58,17 @@ class TranslationBaseModelAdmin(BaseModelAdmin):
|
|||
else:
|
||||
orig_formfield = self.formfield_for_dbfield(orig_field, **kwargs)
|
||||
field.widget = deepcopy(orig_formfield.widget)
|
||||
if db_field.null and isinstance(field.widget, (forms.TextInput, forms.Textarea)):
|
||||
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')
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ class NONE:
|
|||
pass
|
||||
|
||||
|
||||
def create_translation_field(model, field_name, lang):
|
||||
def create_translation_field(model, field_name, lang, empty_value):
|
||||
"""
|
||||
Translation field factory. Returns a ``TranslationField`` based on a
|
||||
fieldname and a language.
|
||||
|
|
@ -58,13 +58,15 @@ def create_translation_field(model, field_name, lang):
|
|||
If the class is neither a subclass of fields in ``SUPPORTED_FIELDS``, nor
|
||||
in ``CUSTOM_FIELDS`` an ``ImproperlyConfigured`` exception will be raised.
|
||||
"""
|
||||
if empty_value not in ('', 'both', None, NONE):
|
||||
raise ImproperlyConfigured('%s is not a valid empty_value.' % empty_value)
|
||||
field = model._meta.get_field(field_name)
|
||||
cls_name = field.__class__.__name__
|
||||
if not (isinstance(field, SUPPORTED_FIELDS) or cls_name in mt_settings.CUSTOM_FIELDS):
|
||||
raise ImproperlyConfigured(
|
||||
'%s is not supported by modeltranslation.' % cls_name)
|
||||
translation_class = field_factory(field.__class__)
|
||||
return translation_class(translated_field=field, language=lang)
|
||||
return translation_class(translated_field=field, language=lang, empty_value=empty_value)
|
||||
|
||||
|
||||
def field_factory(baseclass):
|
||||
|
|
@ -95,7 +97,7 @@ class TranslationField(object):
|
|||
The translation field needs to know which language it contains therefore
|
||||
that needs to be specified when the field is created.
|
||||
"""
|
||||
def __init__(self, translated_field, language, *args, **kwargs):
|
||||
def __init__(self, translated_field, language, empty_value, *args, **kwargs):
|
||||
# Update the dict of this field with the content of the original one
|
||||
# This might be a bit radical?! Seems to work though...
|
||||
self.__dict__.update(translated_field.__dict__)
|
||||
|
|
@ -103,6 +105,9 @@ class TranslationField(object):
|
|||
# Store the originally wrapped field for later
|
||||
self.translated_field = translated_field
|
||||
self.language = language
|
||||
self.empty_value = empty_value
|
||||
if empty_value is NONE:
|
||||
self.empty_value = None if translated_field.null else ''
|
||||
|
||||
# Translation are always optional (for now - maybe add some parameters
|
||||
# to the translation options for configuring this)
|
||||
|
|
@ -166,27 +171,43 @@ class TranslationField(object):
|
|||
|
||||
def formfield(self, *args, **kwargs):
|
||||
"""
|
||||
If the original field is nullable and uses ``forms.CharField`` subclass
|
||||
as its form input, we patch the form field, so it doesn't cast ``None``
|
||||
to anything.
|
||||
Returns proper formfield, according to empty_values setting
|
||||
(only for ``forms.CharField`` subclasses).
|
||||
|
||||
The ``forms.CharField`` somewhat surprising behaviour is documented as a
|
||||
"won't fix": https://code.djangoproject.com/ticket/9590.
|
||||
There are 3 different formfields:
|
||||
- CharField that stores all empty values as empty strings;
|
||||
- NullCharField that stores all empty values as None (Null);
|
||||
- NullableField that can store both None and empty_string.
|
||||
|
||||
By default, if no empty_values was specified in model's translation options,
|
||||
NullCharField would be used if the original field is nullable, CharField otherwise.
|
||||
|
||||
This can be overridden by setting empty_values to '' or None.
|
||||
|
||||
Setting 'both' will result in NullableField being used.
|
||||
Textual widgets (subclassing ``TextInput`` or ``Textarea``) used for
|
||||
nullable fields are enriched with a clear checkbox, allowing ``None``
|
||||
values to be preserved rather than saved as empty strings.
|
||||
|
||||
The ``forms.CharField`` somewhat surprising behaviour is documented as a
|
||||
"won't fix": https://code.djangoproject.com/ticket/9590.
|
||||
"""
|
||||
formfield = super(TranslationField, self).formfield(*args, **kwargs)
|
||||
if self.null:
|
||||
if isinstance(formfield, forms.CharField):
|
||||
if isinstance(formfield, forms.CharField):
|
||||
if self.empty_value is None:
|
||||
from modeltranslation.forms import NullCharField
|
||||
form_class = formfield.__class__
|
||||
kwargs['form_class'] = type(
|
||||
'Null%s' % form_class.__name__, (NullCharField, form_class), {})
|
||||
formfield = super(TranslationField, self).formfield(*args, **kwargs)
|
||||
elif self.empty_value == 'both':
|
||||
from modeltranslation.forms import NullableField
|
||||
form_class = formfield.__class__
|
||||
kwargs['form_class'] = type(
|
||||
'Nullable%s' % form_class.__name__, (NullableField, form_class), {})
|
||||
formfield = super(TranslationField, self).formfield(*args, **kwargs)
|
||||
if isinstance(formfield.widget, (forms.TextInput, forms.Textarea)):
|
||||
formfield.widget = ClearableWidgetWrapper(formfield.widget)
|
||||
if isinstance(formfield.widget, (forms.TextInput, forms.Textarea)):
|
||||
formfield.widget = ClearableWidgetWrapper(formfield.widget)
|
||||
return formfield
|
||||
|
||||
def save_form_data(self, instance, data):
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from django import forms
|
||||
from django.core import validators
|
||||
|
||||
from modeltranslation.fields import TranslationField
|
||||
|
||||
|
|
@ -12,7 +13,17 @@ class TranslationModelForm(forms.ModelForm):
|
|||
del self.fields[f.name]
|
||||
|
||||
|
||||
class NullableField(object):
|
||||
class NullCharField(forms.CharField):
|
||||
"""
|
||||
CharField subclass that returns ``None`` when ``CharField`` would return empty string.
|
||||
"""
|
||||
def to_python(self, value):
|
||||
if value in validators.EMPTY_VALUES:
|
||||
return None
|
||||
return super(NullCharField, self).to_python(value)
|
||||
|
||||
|
||||
class NullableField(forms.Field):
|
||||
"""
|
||||
Form field mixin that ensures that ``None`` is not cast to anything (like
|
||||
the empty string with ``CharField`` and its derivatives).
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from modeltranslation.tests.models import (
|
|||
|
||||
class TestTranslationOptions(TranslationOptions):
|
||||
fields = ('title', 'text', 'url', 'email',)
|
||||
empty_values = ''
|
||||
translator.register(TestModel, TestTranslationOptions)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from django.conf import settings
|
||||
from django.utils.six import with_metaclass
|
||||
from django.db.models import Manager, ForeignKey, OneToOneField
|
||||
from django.db.models.base import ModelBase
|
||||
|
|
@ -11,7 +10,7 @@ from modeltranslation.fields import (NONE, create_translation_field, Translation
|
|||
TranslatedRelationIdDescriptor,
|
||||
LanguageCacheSingleObjectDescriptor)
|
||||
from modeltranslation.manager import MultilingualManager, rewrite_lookup_key
|
||||
from modeltranslation.utils import build_localized_fieldname
|
||||
from modeltranslation.utils import build_localized_fieldname, parse_field
|
||||
|
||||
|
||||
class AlreadyRegistered(Exception):
|
||||
|
|
@ -105,13 +104,15 @@ def add_translation_fields(model, opts):
|
|||
|
||||
Adds newly created translation fields to the given translation options.
|
||||
"""
|
||||
model_empty_values = getattr(opts, 'empty_values', NONE)
|
||||
for field_name in opts.local_fields.keys():
|
||||
for l in settings.LANGUAGES:
|
||||
field_empty_value = parse_field(model_empty_values, field_name, NONE)
|
||||
for l in mt_settings.AVAILABLE_LANGUAGES:
|
||||
# Create a dynamic translation field
|
||||
translation_field = create_translation_field(
|
||||
model=model, field_name=field_name, lang=l[0])
|
||||
model=model, field_name=field_name, lang=l, empty_value=field_empty_value)
|
||||
# Construct the name for the localized field
|
||||
localized_field_name = build_localized_fieldname(field_name, l[0])
|
||||
localized_field_name = build_localized_fieldname(field_name, l)
|
||||
# Check if the model already has a field by that name
|
||||
if hasattr(model, localized_field_name):
|
||||
raise ValueError(
|
||||
|
|
@ -356,14 +357,8 @@ class Translator(object):
|
|||
model_fallback_undefined = getattr(opts, 'fallback_undefined', NONE)
|
||||
for field_name in opts.local_fields.keys():
|
||||
field = model._meta.get_field(field_name)
|
||||
if isinstance(model_fallback_values, dict):
|
||||
field_fallback_value = model_fallback_values.get(field_name, NONE)
|
||||
else:
|
||||
field_fallback_value = model_fallback_values
|
||||
if isinstance(model_fallback_undefined, dict):
|
||||
field_fallback_undefined = model_fallback_undefined.get(field_name, NONE)
|
||||
else:
|
||||
field_fallback_undefined = model_fallback_undefined
|
||||
field_fallback_value = parse_field(model_fallback_values, field_name, NONE)
|
||||
field_fallback_undefined = parse_field(model_fallback_undefined, field_name, NONE)
|
||||
descriptor = TranslationFieldDescriptor(
|
||||
field,
|
||||
fallback_languages=model_fallback_languages,
|
||||
|
|
|
|||
|
|
@ -148,3 +148,13 @@ def fallbacks(enable=True):
|
|||
yield
|
||||
finally:
|
||||
settings.ENABLE_FALLBACKS = current_enable_fallbacks
|
||||
|
||||
|
||||
def parse_field(setting, field_name, default):
|
||||
"""
|
||||
Extract result from single-value or dict-type setting like fallback_values.
|
||||
"""
|
||||
if isinstance(setting, dict):
|
||||
return setting.get(field_name, default)
|
||||
else:
|
||||
return setting
|
||||
|
|
|
|||
Loading…
Reference in a new issue