Merge branch 'ultimate-null-formfield-solution'

This commit is contained in:
deschler 2013-11-07 23:20:55 +01:00
commit 23d014e395
6 changed files with 76 additions and 27 deletions

View file

@ -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')

View file

@ -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):

View file

@ -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).

View file

@ -12,6 +12,7 @@ from modeltranslation.tests.models import (
class TestTranslationOptions(TranslationOptions):
fields = ('title', 'text', 'url', 'email',)
empty_values = ''
translator.register(TestModel, TestTranslationOptions)

View file

@ -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,

View file

@ -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