Commit 9e50833e authored by Loic Bistuer's avatar Loic Bistuer Committed by Tim Graham
Browse files

Fixed #20000 -- Allowed ModelForm meta overrides for label, help_text and error_messages

parent dc9c3595
Loading
Loading
Loading
Loading
+49 −7
Original line number Diff line number Diff line
@@ -138,7 +138,9 @@ 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, localized_fields=None):
def fields_for_model(model, fields=None, exclude=None, widgets=None,
                     formfield_callback=None, localized_fields=None,
                     labels=None, help_texts=None, error_messages=None):
    """
    Returns a ``SortedDict`` containing form fields for the given model.

@@ -149,7 +151,16 @@ def fields_for_model(model, fields=None, exclude=None, widgets=None, formfield_c
    fields will be excluded from the returned fields, even if they are listed
    in the ``fields`` argument.

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

    ``labels`` is a dictionary of model field names mapped to a label.

    ``help_texts`` is a dictionary of model field names mapped to a help text.

    ``error_messages`` is a dictionary of model field names mapped to a
    dictionary of error messages.

    ``formfield_callback`` is a callable that takes a model field and returns
    a form field.
@@ -170,6 +181,12 @@ def fields_for_model(model, fields=None, exclude=None, widgets=None, formfield_c
            kwargs['widget'] = widgets[f.name]
        if localized_fields == ALL_FIELDS or (localized_fields and f.name in localized_fields):
            kwargs['localize'] = True
        if labels and f.name in labels:
            kwargs['label'] = labels[f.name]
        if help_texts and f.name in help_texts:
            kwargs['help_text'] = help_texts[f.name]
        if error_messages and f.name in error_messages:
            kwargs['error_messages'] = error_messages[f.name]

        if formfield_callback is None:
            formfield = f.formfield(**kwargs)
@@ -197,6 +214,9 @@ class ModelFormOptions(object):
        self.exclude = getattr(options, 'exclude', None)
        self.widgets = getattr(options, 'widgets', None)
        self.localized_fields = getattr(options, 'localized_fields', None)
        self.labels = getattr(options, 'labels', None)
        self.help_texts = getattr(options, 'help_texts', None)
        self.error_messages = getattr(options, 'error_messages', None)


class ModelFormMetaclass(type):
@@ -248,7 +268,9 @@ class ModelFormMetaclass(type):
                opts.fields = None

            fields = fields_for_model(opts.model, opts.fields, opts.exclude,
                                      opts.widgets, formfield_callback, opts.localized_fields)
                                      opts.widgets, formfield_callback,
                                      opts.localized_fields, opts.labels,
                                      opts.help_texts, opts.error_messages)

            # make sure opts.fields doesn't specify an invalid field
            none_model_fields = [k for k, v in six.iteritems(fields) if not v]
@@ -416,7 +438,8 @@ 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):
                      formfield_callback=None, widgets=None, localized_fields=None,
                      labels=None, help_texts=None, error_messages=None):
    """
    Returns a ModelForm containing form fields for the given model.

@@ -434,6 +457,13 @@ def modelform_factory(model, form=ModelForm, fields=None, exclude=None,

    ``formfield_callback`` is a callable that takes a model field and returns
    a form field.

    ``labels`` is a dictionary of model field names mapped to a label.

    ``help_texts`` is a dictionary of model field names mapped to a help text.

    ``error_messages`` is a dictionary of model field names mapped to a
    dictionary of error messages.
    """
    # Create the inner Meta class. FIXME: ideally, we should be able to
    # construct a ModelForm without creating and passing in a temporary
@@ -449,6 +479,12 @@ def modelform_factory(model, form=ModelForm, fields=None, exclude=None,
        attrs['widgets'] = widgets
    if localized_fields is not None:
        attrs['localized_fields'] = localized_fields
    if labels is not None:
        attrs['labels'] = labels
    if help_texts is not None:
        attrs['help_texts'] = help_texts
    if error_messages is not None:
        attrs['error_messages'] = error_messages

    # If parent form class already has an inner Meta, the Meta we're
    # creating needs to inherit from the parent's inner meta.
@@ -738,7 +774,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, localized_fields=None):
                         widgets=None, validate_max=False, localized_fields=None,
                         labels=None, help_texts=None, error_messages=None):
    """
    Returns a FormSet class for the given Django model class.
    """
@@ -759,7 +796,8 @@ 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, localized_fields=localized_fields)
                             widgets=widgets, localized_fields=localized_fields,
                             labels=labels, help_texts=help_texts, error_messages=error_messages)
    FormSet = formset_factory(form, formset, extra=extra, max_num=max_num,
                              can_order=can_order, can_delete=can_delete,
                              validate_max=validate_max)
@@ -898,7 +936,8 @@ 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, localized_fields=None):
                          widgets=None, validate_max=False, localized_fields=None,
                          labels=None, help_texts=None, error_messages=None):
    """
    Returns an ``InlineFormSet`` for the given kwargs.

@@ -922,6 +961,9 @@ def inlineformset_factory(parent_model, model, form=ModelForm,
        'widgets': widgets,
        'validate_max': validate_max,
        'localized_fields': localized_fields,
        'labels': labels,
        'help_texts': help_texts,
        'error_messages': error_messages,
    }
    FormSet = modelformset_factory(model, **kwargs)
    FormSet.fk = fk
+20 −9
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, localized_fields=None)
.. function:: modelform_factory(model, form=ModelForm, fields=None, exclude=None, formfield_callback=None, widgets=None, localized_fields=None, labels=None, help_texts=None, error_messages=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,11 +20,18 @@ 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.

    ``localized_fields`` is a list of names of fields which should be localized.

    ``labels`` is a dictionary of model field names mapped to a label.

    ``help_texts`` is a dictionary of model field names mapped to a help text.

    ``error_messages`` is a dictionary of model field names mapped to a
    dictionary of error messages.

    See :ref:`modelforms-factory` for example usage.

    .. versionchanged:: 1.6
@@ -35,14 +42,16 @@ Model Form Functions
    information. Omitting any definition of the fields to use will result in all
    fields being used, but this behavior is deprecated.

    The ``localized_fields`` parameter was added.
    The ``localized_fields``, ``labels``, ``help_texts``, and
    ``error_messages`` parameters were 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)
.. 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, labels=None, help_texts=None, error_messages=None)

    Returns a ``FormSet`` class for the given ``model`` class.

    Arguments ``model``, ``form``, ``fields``, ``exclude``,
    ``formfield_callback``, ``widgets`` and ``localized_fields`` are all passed through to
    ``formfield_callback``, ``widgets``, ``localized_fields``, ``labels``,
    ``help_texts``, and ``error_messages`` are all passed through to
    :func:`~django.forms.models.modelform_factory`.

    Arguments ``formset``, ``extra``, ``max_num``, ``can_order``,
@@ -54,9 +63,10 @@ Model Form Functions

    .. versionchanged:: 1.6

        The ``widgets``, ``validate_max`` and ``localized_fields`` parameters were added.
        The ``widgets``, ``validate_max``, ``localized_fields``, ``labels``,
        ``help_texts``, and ``error_messages`` 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, localized_fields=None)
.. 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, labels=None, help_texts=None, error_messages=None)

    Returns an ``InlineFormSet`` using :func:`modelformset_factory` with
    defaults of ``formset=BaseInlineFormSet``, ``can_delete=True``, and
@@ -69,4 +79,5 @@ Model Form Functions

    .. versionchanged:: 1.6

        The ``widgets``, ``validate_max`` and ``localized_fields`` parameters were added.
        The ``widgets``, ``validate_max`` and ``localized_fields``, ``labels``,
        ``help_texts``, and ``error_messages`` parameters were added.
+8 −3
Original line number Diff line number Diff line
@@ -236,9 +236,14 @@ 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
* :class:`~django.forms.ModelForm` accepts several new ``Meta``
  options.

  * Fields included in the ``localized_fields`` list will be localized
    (by setting ``localize`` on the form field).
  * The  ``labels``, ``help_texts`` and ``error_messages`` options may be used
    to customize the default fields, see
    :ref:`modelforms-overriding-default-fields` for details.

* The ``choices`` argument to model fields now accepts an iterable of iterables
  instead of requiring an iterable of lists or tuples.
+44 −15
Original line number Diff line number Diff line
@@ -141,7 +141,7 @@ In addition, each generated form field has attributes set as follows:
  ``default`` value will be initially selected instead).

Finally, note that you can override the form field used for a given model
field. See `Overriding the default field types or widgets`_ below.
field. See `Overriding the default fields`_ below.

A full example
--------------
@@ -388,8 +388,10 @@ include that field.

.. _section on saving forms: `The save() method`_

Overriding the default field types or widgets
---------------------------------------------
.. _modelforms-overriding-default-fields:

Overriding the default fields
-----------------------------

The default field types, as described in the `Field types`_ table above, are
sensible defaults. If you have a ``DateField`` in your model, chances are you'd
@@ -420,38 +422,65 @@ widget::
The ``widgets`` dictionary accepts either widget instances (e.g.,
``Textarea(...)``) or classes (e.g., ``Textarea``).

If you want to further customize a field -- including its type, label, etc. --
you can do this by declaratively specifying fields like you would in a regular
``Form``. Declared fields will override the default ones generated by using the
``model`` attribute.
.. versionadded:: 1.6

    The ``labels``, ``help_texts`` and ``error_messages`` options were added.

Similarly, you can specify the ``labels``, ``help_texts`` and ``error_messages``
attributes of the inner ``Meta`` class if you want to further customize a field.

For example, if you wanted to use ``MyDateFormField`` for the ``pub_date``
For example if you wanted to customize the wording of all user facing strings for
the ``name`` field::

    class AuthorForm(ModelForm):
        class Meta:
            model = Author
            fields = ('name', 'title', 'birth_date')
            labels = {
                'name': _('Writer'),
            }
            help_texts = {
                'name': _('Some useful help text.'),
            }
            error_messages = {
                'name': {
                    'max_length': _("This writer's name is too long."),
                },
            }

Finally, if you want complete control over of a field -- including its type,
validators, etc. -- you can do this by declaratively specifying fields like you
would in a regular ``Form``. Declared fields will override the default ones
generated by using the ``model`` attribute. Fields declared like this will
ignore any customizations in the ``widgets``, ``labels``, ``help_texts``, and
``error_messages`` options declared on ``Meta``.

For example, if you wanted to use ``MySlugFormField`` for the ``slug``
field, you could do the following::

    from django.forms import ModelForm
    from myapp.models import Article

    class ArticleForm(ModelForm):
        pub_date = MyDateFormField()
        slug = MySlugFormField()

        class Meta:
            model = Article
            fields = ['pub_date', 'headline', 'content', 'reporter']


If you want to override a field's default label, then specify the ``label``
parameter when declaring the form field::
If you want to override a field's default validators, then specify the
``validators`` parameter when declaring the form field::

    from django.forms import ModelForm, DateField
    from myapp.models import Article

    class ArticleForm(ModelForm):
        pub_date = DateField(label='Publication date')
        slug = CharField(validators=[validate_slug])

        class Meta:
            model = Article
            fields = ['pub_date', 'headline', 'content', 'reporter']

            fields = ['pub_date', 'headline', 'content', 'reporter', 'slug']

.. note::

@@ -597,7 +626,7 @@ example by specifying the widgets to be used for a given field::

    >>> from django.forms import Textarea
    >>> Form = modelform_factory(Book, form=BookForm,
                                 widgets={"title": Textarea()})
    ...                          widgets={"title": Textarea()})

The fields to include can be specified using the ``fields`` and ``exclude``
keyword arguments, or the corresponding attributes on the ``ModelForm`` inner
+59 −10
Original line number Diff line number Diff line
@@ -490,7 +490,7 @@ class ModelFormBaseTest(TestCase):
                         ['slug', 'name'])


class TestWidgetForm(forms.ModelForm):
class FieldOverridesTroughFormMetaForm(forms.ModelForm):
    class Meta:
        model = Category
        fields = ['name', 'url', 'slug']
@@ -498,25 +498,74 @@ class TestWidgetForm(forms.ModelForm):
            'name': forms.Textarea,
            'url': forms.TextInput(attrs={'class': 'url'})
        }
        labels = {
            'name': 'Title',
        }
        help_texts = {
            'slug': 'Watch out! Letters, numbers, underscores and hyphens only.',
        }
        error_messages = {
            'slug': {
                'invalid': (
                    "Didn't you read the help text? "
                    "We said letters, numbers, underscores and hyphens only!"
                )
            }
        }


class TestFieldOverridesTroughFormMeta(TestCase):
    def test_widget_overrides(self):
        form = FieldOverridesTroughFormMetaForm()
        self.assertHTMLEqual(
            str(form['name']),
            '<textarea id="id_name" rows="10" cols="40" name="name"></textarea>',
        )
        self.assertHTMLEqual(
            str(form['url']),
            '<input id="id_url" type="text" class="url" name="url" maxlength="40" />',
        )
        self.assertHTMLEqual(
            str(form['slug']),
            '<input id="id_slug" type="text" name="slug" maxlength="20" />',
        )

class TestWidgets(TestCase):
    def test_base_widgets(self):
        frm = TestWidgetForm()
    def test_label_overrides(self):
        form = FieldOverridesTroughFormMetaForm()
        self.assertHTMLEqual(
            str(frm['name']),
            '<textarea id="id_name" rows="10" cols="40" name="name"></textarea>'
            str(form['name'].label_tag()),
            '<label for="id_name">Title:</label>',
        )
        self.assertHTMLEqual(
            str(frm['url']),
            '<input id="id_url" type="text" class="url" name="url" maxlength="40" />'
            str(form['url'].label_tag()),
            '<label for="id_url">The URL:</label>',
        )
        self.assertHTMLEqual(
            str(frm['slug']),
            '<input id="id_slug" type="text" name="slug" maxlength="20" />'
            str(form['slug'].label_tag()),
            '<label for="id_slug">Slug:</label>',
        )

    def test_help_text_overrides(self):
        form = FieldOverridesTroughFormMetaForm()
        self.assertEqual(
            form['slug'].help_text,
            'Watch out! Letters, numbers, underscores and hyphens only.',
        )

    def test_error_messages_overrides(self):
        form = FieldOverridesTroughFormMetaForm(data={
            'name': 'Category',
            'url': '/category/',
            'slug': '!%#*@',
        })
        form.full_clean()

        error = [
            "Didn't you read the help text? "
            "We said letters, numbers, underscores and hyphens only!",
        ]
        self.assertEqual(form.errors, {'slug': error})


class IncompleteCategoryFormWithFields(forms.ModelForm):
    """
Loading