Skip to content

Commit

Permalink
Fixed #20000 -- Allowed ModelForm meta overrides for label, help_text…
Browse files Browse the repository at this point in the history
… and error_messages
  • Loading branch information
loic authored and timgraham committed Jun 13, 2013
1 parent dc9c359 commit 9e50833
Show file tree
Hide file tree
Showing 6 changed files with 231 additions and 45 deletions.
56 changes: 49 additions & 7 deletions django/forms/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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)
Expand Down Expand Up @@ -197,6 +214,9 @@ def __init__(self, options=None):
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):
Expand Down Expand Up @@ -248,7 +268,9 @@ def __new__(cls, name, bases, attrs):
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]
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -738,7 +774,8 @@ def pk_is_not_editable(pk):
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.
"""
Expand All @@ -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)
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down
29 changes: 20 additions & 9 deletions docs/ref/forms/models.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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``,
Expand All @@ -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
Expand All @@ -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.
11 changes: 8 additions & 3 deletions docs/releases/1.6.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
(by setting ``localize`` on the form field).
* :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.
Expand Down
59 changes: 44 additions & 15 deletions docs/topics/forms/modelforms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
--------------
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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::

Expand Down Expand Up @@ -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
Expand Down
Loading

1 comment on commit 9e50833

@jacobvalenta
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you so much, just ran into the need to do this.

Please sign in to comment.