Explorar o código

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

Erik Romijn %!s(int64=12) %!d(string=hai) anos
pai
achega
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)
     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.
 
@@ -162,10 +162,12 @@ def fields_for_model(model, fields=None, exclude=None, widgets=None, formfield_c
             continue
         if exclude and f.name in exclude:
             continue
+
+        kwargs = {}
         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:
             formfield = f.formfield(**kwargs)
@@ -192,6 +194,7 @@ class ModelFormOptions(object):
         self.fields = getattr(options, 'fields', None)
         self.exclude = getattr(options, 'exclude', None)
         self.widgets = getattr(options, 'widgets', None)
+        self.localized_fields = getattr(options, 'localized_fields', None)
 
 
 class ModelFormMetaclass(type):
@@ -215,7 +218,7 @@ class ModelFormMetaclass(type):
         # We check if a string was passed to `fields` or `exclude`,
         # which is likely to be a mistake where the user typed ('foo') instead
         # of ('foo',)
-        for opt in ['fields', 'exclude']:
+        for opt in ['fields', 'exclude', 'localized_fields']:
             value = getattr(opts, opt)
             if isinstance(value, six.string_types) and value != ALL_FIELDS:
                 msg = ("%(model)s.Meta.%(opt)s cannot be a string. "
@@ -242,8 +245,9 @@ class ModelFormMetaclass(type):
                 # fields from the model"
                 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
             none_model_fields = [k for k, v in six.iteritems(fields) if not v]
             missing_fields = set(none_model_fields) - \
@@ -409,7 +413,7 @@ class ModelForm(six.with_metaclass(ModelFormMetaclass, BaseModelForm)):
     pass
 
 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.
 
@@ -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.
 
+    ``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
     a form field.
     """
@@ -438,6 +444,8 @@ def modelform_factory(model, form=ModelForm, fields=None, exclude=None,
         attrs['exclude'] = exclude
     if widgets is not None:
         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
     # 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,
                          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.
     """
@@ -748,7 +756,7 @@ def modelformset_factory(model, form=ModelForm, formfield_callback=None,
 
     form = modelform_factory(model, form=form, fields=fields, exclude=exclude,
                              formfield_callback=formfield_callback,
-                             widgets=widgets)
+                             widgets=widgets, localized_fields=localized_fields)
     FormSet = formset_factory(form, formset, extra=extra, max_num=max_num,
                               can_order=can_order, can_delete=can_delete,
                               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,
                           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.
 
@@ -910,6 +918,7 @@ def inlineformset_factory(parent_model, model, form=ModelForm,
         'max_num': max_num,
         'widgets': widgets,
         'validate_max': validate_max,
+        'localized_fields': localized_fields,
     }
     FormSet = modelformset_factory(model, **kwargs)
     FormSet.fk = fk

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

@@ -5,7 +5,7 @@ Model Form Functions
 .. module:: django.forms.models
    :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``.
     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.
 
+    ``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
     a form field.
 
@@ -33,12 +35,14 @@ Model Form Functions
     information. Omitting any definition of the fields to use will result in all
     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.
 
     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`.
 
     Arguments ``formset``, ``extra``, ``max_num``, ``can_order``,
@@ -50,9 +54,9 @@ Model Form Functions
 
     .. 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
     defaults of ``formset=BaseInlineFormSet``, ``can_delete=True``, and
@@ -65,4 +69,4 @@ Model Form Functions
 
     .. 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
 .. _`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
 =====================================
 

+ 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
     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 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`
 documentation.
 
+... or enable localization for specific fields::
+
+    >>> Form = modelform_factory(Author, form=AuthorForm, localized_fields=("birth_date",))
+
 .. _model-formsets:
 
 Model formsets
@@ -663,6 +685,20 @@ class of a ``ModelForm`` works::
     >>> AuthorFormSet = modelformset_factory(
     ...     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
 ------------------------
 

+ 35 - 0
tests/model_forms_regress/tests.py

@@ -92,6 +92,41 @@ class OverrideCleanTests(TestCase):
         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.
 # Make sure the cleaned_data returned from ModelForm.clean() is applied to the
 # model instance.

+ 6 - 1
tests/model_formsets_regress/tests.py

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