Browse Source

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

Erik Romijn 12 years ago
parent
commit
756b81dbd1

+ 23 - 14
django/forms/models.py

@@ -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]}
-        else:
-            kwargs = {}
+            kwargs['widget'] = widgets[f.name]
+        if localized_fields == ALL_FIELDS or (localized_fields and f.name in localized_fields):
+            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,
-                                      opts.exclude, opts.widgets, formfield_callback)
+            fields = fields_for_model(opts.model, opts.fields, opts.exclude,
+                                      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,
-                         exclude=None, widgets=None, validate_max=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 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,
-                          extra=3, can_order=False, can_delete=True, max_num=None,
-                          formfield_callback=None, widgets=None, validate_max=False):
+                          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`` 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

+ 10 - 6
docs/ref/forms/models.txt

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

+ 4 - 0
docs/releases/1.6.txt

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

+ 36 - 0
docs/topics/forms/modelforms.txt

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

+ 35 - 0
tests/model_forms_regress/tests.py

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

+ 6 - 1
tests/model_formsets_regress/tests.py

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