Fixed #13546 -- Easier handling of localize field options in ModelForm

This commit is contained in:
Erik Romijn 2013-05-18 14:13:00 +02:00
parent be826aafac
commit 756b81dbd1
6 changed files with 114 additions and 21 deletions

View File

@ -136,7 +136,7 @@ def model_to_dict(instance, fields=None, exclude=None):
data[f.name] = f.value_from_object(instance) data[f.name] = f.value_from_object(instance)
return data return data
def fields_for_model(model, fields=None, exclude=None, widgets=None, formfield_callback=None): def fields_for_model(model, fields=None, exclude=None, widgets=None, localized_fields=None, formfield_callback=None):
""" """
Returns a ``SortedDict`` containing form fields for the given model. Returns a ``SortedDict`` containing form fields for the given model.
@ -162,10 +162,12 @@ def fields_for_model(model, fields=None, exclude=None, widgets=None, formfield_c
continue continue
if exclude and f.name in exclude: if exclude and f.name in exclude:
continue continue
kwargs = {}
if widgets and f.name in widgets: if widgets and f.name in widgets:
kwargs = {'widget': widgets[f.name]} kwargs['widget'] = widgets[f.name]
else: if localized_fields == ALL_FIELDS or (localized_fields and f.name in localized_fields):
kwargs = {} kwargs['localize'] = True
if formfield_callback is None: if formfield_callback is None:
formfield = f.formfield(**kwargs) formfield = f.formfield(**kwargs)
@ -192,6 +194,7 @@ class ModelFormOptions(object):
self.fields = getattr(options, 'fields', None) self.fields = getattr(options, 'fields', None)
self.exclude = getattr(options, 'exclude', None) self.exclude = getattr(options, 'exclude', None)
self.widgets = getattr(options, 'widgets', None) self.widgets = getattr(options, 'widgets', None)
self.localized_fields = getattr(options, 'localized_fields', None)
class ModelFormMetaclass(type): class ModelFormMetaclass(type):
@ -215,7 +218,7 @@ class ModelFormMetaclass(type):
# We check if a string was passed to `fields` or `exclude`, # We check if a string was passed to `fields` or `exclude`,
# which is likely to be a mistake where the user typed ('foo') instead # which is likely to be a mistake where the user typed ('foo') instead
# of ('foo',) # of ('foo',)
for opt in ['fields', 'exclude']: for opt in ['fields', 'exclude', 'localized_fields']:
value = getattr(opts, opt) value = getattr(opts, opt)
if isinstance(value, six.string_types) and value != ALL_FIELDS: if isinstance(value, six.string_types) and value != ALL_FIELDS:
msg = ("%(model)s.Meta.%(opt)s cannot be a string. " msg = ("%(model)s.Meta.%(opt)s cannot be a string. "
@ -242,8 +245,9 @@ class ModelFormMetaclass(type):
# fields from the model" # fields from the model"
opts.fields = None opts.fields = None
fields = fields_for_model(opts.model, opts.fields, fields = fields_for_model(opts.model, opts.fields, opts.exclude,
opts.exclude, opts.widgets, formfield_callback) opts.widgets, opts.localized_fields, formfield_callback)
# make sure opts.fields doesn't specify an invalid field # make sure opts.fields doesn't specify an invalid field
none_model_fields = [k for k, v in six.iteritems(fields) if not v] none_model_fields = [k for k, v in six.iteritems(fields) if not v]
missing_fields = set(none_model_fields) - \ missing_fields = set(none_model_fields) - \
@ -409,7 +413,7 @@ class ModelForm(six.with_metaclass(ModelFormMetaclass, BaseModelForm)):
pass pass
def modelform_factory(model, form=ModelForm, fields=None, exclude=None, def modelform_factory(model, form=ModelForm, fields=None, exclude=None,
formfield_callback=None, widgets=None): localized_fields=None, widgets=None, formfield_callback=None):
""" """
Returns a ModelForm containing form fields for the given model. Returns a ModelForm containing form fields for the given model.
@ -423,6 +427,8 @@ def modelform_factory(model, form=ModelForm, fields=None, exclude=None,
``widgets`` is a dictionary of model field names mapped to a widget. ``widgets`` is a dictionary of model field names mapped to a widget.
``localized_fields`` is a list of names of fields which should be localized.
``formfield_callback`` is a callable that takes a model field and returns ``formfield_callback`` is a callable that takes a model field and returns
a form field. a form field.
""" """
@ -438,6 +444,8 @@ def modelform_factory(model, form=ModelForm, fields=None, exclude=None,
attrs['exclude'] = exclude attrs['exclude'] = exclude
if widgets is not None: if widgets is not None:
attrs['widgets'] = widgets attrs['widgets'] = widgets
if localized_fields is not None:
attrs['localized_fields'] = localized_fields
# If parent form class already has an inner Meta, the Meta we're # If parent form class already has an inner Meta, the Meta we're
# creating needs to inherit from the parent's inner meta. # creating needs to inherit from the parent's inner meta.
@ -726,8 +734,8 @@ class BaseModelFormSet(BaseFormSet):
def modelformset_factory(model, form=ModelForm, formfield_callback=None, def modelformset_factory(model, form=ModelForm, formfield_callback=None,
formset=BaseModelFormSet, extra=1, can_delete=False, formset=BaseModelFormSet, extra=1, can_delete=False,
can_order=False, max_num=None, fields=None, can_order=False, max_num=None, fields=None, exclude=None,
exclude=None, widgets=None, validate_max=False): widgets=None, validate_max=False, localized_fields=None):
""" """
Returns a FormSet class for the given Django model class. Returns a FormSet class for the given Django model class.
""" """
@ -748,7 +756,7 @@ def modelformset_factory(model, form=ModelForm, formfield_callback=None,
form = modelform_factory(model, form=form, fields=fields, exclude=exclude, form = modelform_factory(model, form=form, fields=fields, exclude=exclude,
formfield_callback=formfield_callback, formfield_callback=formfield_callback,
widgets=widgets) widgets=widgets, localized_fields=localized_fields)
FormSet = formset_factory(form, formset, extra=extra, max_num=max_num, FormSet = formset_factory(form, formset, extra=extra, max_num=max_num,
can_order=can_order, can_delete=can_delete, can_order=can_order, can_delete=can_delete,
validate_max=validate_max) validate_max=validate_max)
@ -885,9 +893,9 @@ def _get_foreign_key(parent_model, model, fk_name=None, can_fail=False):
def inlineformset_factory(parent_model, model, form=ModelForm, def inlineformset_factory(parent_model, model, form=ModelForm,
formset=BaseInlineFormSet, fk_name=None, formset=BaseInlineFormSet, fk_name=None,
fields=None, exclude=None, fields=None, exclude=None, extra=3, can_order=False,
extra=3, can_order=False, can_delete=True, max_num=None, can_delete=True, max_num=None, formfield_callback=None,
formfield_callback=None, widgets=None, validate_max=False): widgets=None, validate_max=False, localized_fields=None):
""" """
Returns an ``InlineFormSet`` for the given kwargs. Returns an ``InlineFormSet`` for the given kwargs.
@ -910,6 +918,7 @@ def inlineformset_factory(parent_model, model, form=ModelForm,
'max_num': max_num, 'max_num': max_num,
'widgets': widgets, 'widgets': widgets,
'validate_max': validate_max, 'validate_max': validate_max,
'localized_fields': localized_fields,
} }
FormSet = modelformset_factory(model, **kwargs) FormSet = modelformset_factory(model, **kwargs)
FormSet.fk = fk FormSet.fk = fk

View File

@ -5,7 +5,7 @@ Model Form Functions
.. module:: django.forms.models .. module:: django.forms.models
:synopsis: Django's functions for building model forms and formsets. :synopsis: Django's functions for building model forms and formsets.
.. function:: modelform_factory(model, form=ModelForm, fields=None, exclude=None, formfield_callback=None, widgets=None) .. function:: modelform_factory(model, form=ModelForm, fields=None, exclude=None, formfield_callback=None, widgets=None, localized_fields=None)
Returns a :class:`~django.forms.ModelForm` class for the given ``model``. Returns a :class:`~django.forms.ModelForm` class for the given ``model``.
You can optionally pass a ``form`` argument to use as a starting point for You can optionally pass a ``form`` argument to use as a starting point for
@ -20,6 +20,8 @@ Model Form Functions
``widgets`` is a dictionary of model field names mapped to a widget. ``widgets`` is a dictionary of model field names mapped to a widget.
``localized_fields`` is a list of names of fields which should be localized.
``formfield_callback`` is a callable that takes a model field and returns ``formfield_callback`` is a callable that takes a model field and returns
a form field. a form field.
@ -33,12 +35,14 @@ Model Form Functions
information. Omitting any definition of the fields to use will result in all information. Omitting any definition of the fields to use will result in all
fields being used, but this behaviour is deprecated. fields being used, but this behaviour is deprecated.
.. function:: modelformset_factory(model, form=ModelForm, formfield_callback=None, formset=BaseModelFormSet, extra=1, can_delete=False, can_order=False, max_num=None, fields=None, exclude=None, widgets=None, validate_max=False) The ``localized_fields`` parameter was added.
.. function:: modelformset_factory(model, form=ModelForm, formfield_callback=None, formset=BaseModelFormSet, extra=1, can_delete=False, can_order=False, max_num=None, fields=None, exclude=None, widgets=None, validate_max=False, localized_fields=None)
Returns a ``FormSet`` class for the given ``model`` class. Returns a ``FormSet`` class for the given ``model`` class.
Arguments ``model``, ``form``, ``fields``, ``exclude``, Arguments ``model``, ``form``, ``fields``, ``exclude``,
``formfield_callback`` and ``widgets`` are all passed through to ``formfield_callback``, ``widgets`` and ``localized_fields`` are all passed through to
:func:`~django.forms.models.modelform_factory`. :func:`~django.forms.models.modelform_factory`.
Arguments ``formset``, ``extra``, ``max_num``, ``can_order``, Arguments ``formset``, ``extra``, ``max_num``, ``can_order``,
@ -50,9 +54,9 @@ Model Form Functions
.. versionchanged:: 1.6 .. versionchanged:: 1.6
The ``widgets`` and the ``validate_max`` parameters were added. The ``widgets``, ``validate_max`` and ``localized_fields`` parameters were added.
.. function:: inlineformset_factory(parent_model, model, form=ModelForm, formset=BaseInlineFormSet, fk_name=None, fields=None, exclude=None, extra=3, can_order=False, can_delete=True, max_num=None, formfield_callback=None, widgets=None, validate_max=False) .. function:: inlineformset_factory(parent_model, model, form=ModelForm, formset=BaseInlineFormSet, fk_name=None, fields=None, exclude=None, extra=3, can_order=False, can_delete=True, max_num=None, formfield_callback=None, widgets=None, validate_max=False, localized_fields=None)
Returns an ``InlineFormSet`` using :func:`modelformset_factory` with Returns an ``InlineFormSet`` using :func:`modelformset_factory` with
defaults of ``formset=BaseInlineFormSet``, ``can_delete=True``, and defaults of ``formset=BaseInlineFormSet``, ``can_delete=True``, and
@ -65,4 +69,4 @@ Model Form Functions
.. versionchanged:: 1.6 .. versionchanged:: 1.6
The ``widgets`` and the ``validate_max`` parameters were added. The ``widgets``, ``validate_max`` and ``localized_fields`` parameters were added.

View File

@ -234,6 +234,10 @@ Minor features
.. _`Pillow`: https://pypi.python.org/pypi/Pillow .. _`Pillow`: https://pypi.python.org/pypi/Pillow
.. _`PIL`: https://pypi.python.org/pypi/PIL .. _`PIL`: https://pypi.python.org/pypi/PIL
* :doc:`ModelForm </topics/forms/modelforms/>` accepts a new
Meta option: ``localized_fields``. Fields included in this list will be localized
(by setting ``localize`` on the form field).
Backwards incompatible changes in 1.6 Backwards incompatible changes in 1.6
===================================== =====================================

View File

@ -474,6 +474,24 @@ parameter when declaring the form field::
See the :doc:`form field documentation </ref/forms/fields>` for more information See the :doc:`form field documentation </ref/forms/fields>` for more information
on fields and their arguments. on fields and their arguments.
Enabling localization of fields
-------------------------------
.. versionadded:: 1.6
By default, the fields in a ``ModelForm`` will not localize their data. To
enable localization for fields, you can use the ``localized_fields``
attribute on the ``Meta`` class.
>>> class AuthorForm(ModelForm):
... class Meta:
... model = Author
... localized_fields = ('birth_date',)
If ``localized_fields`` is set to the special value ``'__all__'``, all fields
will be localized.
.. _overriding-modelform-clean-method: .. _overriding-modelform-clean-method:
Overriding the clean() method Overriding the clean() method
@ -570,6 +588,10 @@ keyword arguments, or the corresponding attributes on the ``ModelForm`` inner
``Meta`` class. Please see the ``ModelForm`` :ref:`modelforms-selecting-fields` ``Meta`` class. Please see the ``ModelForm`` :ref:`modelforms-selecting-fields`
documentation. documentation.
... or enable localization for specific fields::
>>> Form = modelform_factory(Author, form=AuthorForm, localized_fields=("birth_date",))
.. _model-formsets: .. _model-formsets:
Model formsets Model formsets
@ -663,6 +685,20 @@ class of a ``ModelForm`` works::
>>> AuthorFormSet = modelformset_factory( >>> AuthorFormSet = modelformset_factory(
... Author, widgets={'name': Textarea(attrs={'cols': 80, 'rows': 20}) ... Author, widgets={'name': Textarea(attrs={'cols': 80, 'rows': 20})
Enabling localization for fields with ``localized_fields``
----------------------------------------------------------
.. versionadded:: 1.6
Using the ``localized_fields`` parameter, you can enable localization for
fields in the form.
>>> AuthorFormSet = modelformset_factory(
... Author, localized_fields=('value',))
If ``localized_fields`` is set to the special value ``'__all__'``, all fields
will be localized.
Providing initial values Providing initial values
------------------------ ------------------------

View File

@ -92,6 +92,41 @@ class OverrideCleanTests(TestCase):
self.assertEqual(form.instance.left, 1) self.assertEqual(form.instance.left, 1)
class PartiallyLocalizedTripleForm(forms.ModelForm):
class Meta:
model = Triple
localized_fields = ('left', 'right',)
class FullyLocalizedTripleForm(forms.ModelForm):
class Meta:
model = Triple
localized_fields = "__all__"
class LocalizedModelFormTest(TestCase):
def test_model_form_applies_localize_to_some_fields(self):
f = PartiallyLocalizedTripleForm({'left': 10, 'middle': 10, 'right': 10})
self.assertTrue(f.is_valid())
self.assertTrue(f.fields['left'].localize)
self.assertFalse(f.fields['middle'].localize)
self.assertTrue(f.fields['right'].localize)
def test_model_form_applies_localize_to_all_fields(self):
f = FullyLocalizedTripleForm({'left': 10, 'middle': 10, 'right': 10})
self.assertTrue(f.is_valid())
self.assertTrue(f.fields['left'].localize)
self.assertTrue(f.fields['middle'].localize)
self.assertTrue(f.fields['right'].localize)
def test_model_form_refuses_arbitrary_string(self):
with self.assertRaises(TypeError):
class BrokenLocalizedTripleForm(forms.ModelForm):
class Meta:
model = Triple
localized_fields = "foo"
# Regression test for #12960. # Regression test for #12960.
# Make sure the cleaned_data returned from ModelForm.clean() is applied to the # Make sure the cleaned_data returned from ModelForm.clean() is applied to the
# model instance. # model instance.

View File

@ -273,6 +273,7 @@ class UserSiteForm(forms.ModelForm):
'id': CustomWidget, 'id': CustomWidget,
'data': CustomWidget, 'data': CustomWidget,
} }
localized_fields = ('data',)
class Callback(object): class Callback(object):
@ -297,19 +298,23 @@ class FormfieldCallbackTests(TestCase):
form = Formset().forms[0] form = Formset().forms[0]
self.assertTrue(isinstance(form['id'].field.widget, CustomWidget)) self.assertTrue(isinstance(form['id'].field.widget, CustomWidget))
self.assertTrue(isinstance(form['data'].field.widget, CustomWidget)) self.assertTrue(isinstance(form['data'].field.widget, CustomWidget))
self.assertFalse(form.fields['id'].localize)
self.assertTrue(form.fields['data'].localize)
def test_modelformset_factory_default(self): def test_modelformset_factory_default(self):
Formset = modelformset_factory(UserSite, form=UserSiteForm) Formset = modelformset_factory(UserSite, form=UserSiteForm)
form = Formset().forms[0] form = Formset().forms[0]
self.assertTrue(isinstance(form['id'].field.widget, CustomWidget)) self.assertTrue(isinstance(form['id'].field.widget, CustomWidget))
self.assertTrue(isinstance(form['data'].field.widget, CustomWidget)) self.assertTrue(isinstance(form['data'].field.widget, CustomWidget))
self.assertFalse(form.fields['id'].localize)
self.assertTrue(form.fields['data'].localize)
def assertCallbackCalled(self, callback): def assertCallbackCalled(self, callback):
id_field, user_field, data_field = UserSite._meta.fields id_field, user_field, data_field = UserSite._meta.fields
expected_log = [ expected_log = [
(id_field, {'widget': CustomWidget}), (id_field, {'widget': CustomWidget}),
(user_field, {}), (user_field, {}),
(data_field, {'widget': CustomWidget}), (data_field, {'widget': CustomWidget, 'localize': True}),
] ]
self.assertEqual(callback.log, expected_log) self.assertEqual(callback.log, expected_log)