Commit 756b81db authored by Erik Romijn's avatar Erik Romijn
Browse files

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

parent be826aaf
Loading
Loading
Loading
Loading
+23 −14
Original line number Diff line number Diff line
@@ -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
        if widgets and f.name in widgets:
            kwargs = {'widget': widgets[f.name]}
        else:

        kwargs = {}
        if widgets and f.name in widgets:
            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
Original line number Diff line number Diff line
@@ -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
Original line number Diff line number Diff line
@@ -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
Original line number Diff line number Diff line
@@ -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
Original line number Diff line number Diff line
@@ -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.
Loading