Fixed #24295 -- Allowed ModelForm meta to specify form field classes.

Thanks Carl Meyer and Markus Holtermann for the reviews.
This commit is contained in:
Loic Bistuer 2015-02-07 04:19:23 +07:00
parent e8cf4f8abe
commit 00a889167f
6 changed files with 92 additions and 26 deletions

View File

@ -154,7 +154,8 @@ def model_to_dict(instance, fields=None, exclude=None):
def fields_for_model(model, fields=None, exclude=None, widgets=None, def fields_for_model(model, fields=None, exclude=None, widgets=None,
formfield_callback=None, localized_fields=None, formfield_callback=None, localized_fields=None,
labels=None, help_texts=None, error_messages=None): labels=None, help_texts=None, error_messages=None,
field_classes=None):
""" """
Returns a ``OrderedDict`` containing form fields for the given model. Returns a ``OrderedDict`` containing form fields for the given model.
@ -167,6 +168,9 @@ def fields_for_model(model, fields=None, exclude=None, widgets=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.
``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. ``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. ``labels`` is a dictionary of model field names mapped to a label.
@ -176,8 +180,8 @@ def fields_for_model(model, fields=None, exclude=None, widgets=None,
``error_messages`` is a dictionary of model field names mapped to a ``error_messages`` is a dictionary of model field names mapped to a
dictionary of error messages. dictionary of error messages.
``formfield_callback`` is a callable that takes a model field and returns ``field_classes`` is a dictionary of model field names mapped to a form
a form field. field class.
""" """
field_list = [] field_list = []
ignored = [] ignored = []
@ -205,6 +209,8 @@ def fields_for_model(model, fields=None, exclude=None, widgets=None,
kwargs['help_text'] = help_texts[f.name] kwargs['help_text'] = help_texts[f.name]
if error_messages and f.name in error_messages: if error_messages and f.name in error_messages:
kwargs['error_messages'] = error_messages[f.name] kwargs['error_messages'] = error_messages[f.name]
if field_classes and f.name in field_classes:
kwargs['form_class'] = field_classes[f.name]
if formfield_callback is None: if formfield_callback is None:
formfield = f.formfield(**kwargs) formfield = f.formfield(**kwargs)
@ -236,6 +242,7 @@ class ModelFormOptions(object):
self.labels = getattr(options, 'labels', None) self.labels = getattr(options, 'labels', None)
self.help_texts = getattr(options, 'help_texts', None) self.help_texts = getattr(options, 'help_texts', None)
self.error_messages = getattr(options, 'error_messages', None) self.error_messages = getattr(options, 'error_messages', None)
self.field_classes = getattr(options, 'field_classes', None)
class ModelFormMetaclass(DeclarativeFieldsMetaclass): class ModelFormMetaclass(DeclarativeFieldsMetaclass):
@ -280,7 +287,8 @@ class ModelFormMetaclass(DeclarativeFieldsMetaclass):
fields = fields_for_model(opts.model, opts.fields, opts.exclude, fields = fields_for_model(opts.model, opts.fields, opts.exclude,
opts.widgets, formfield_callback, opts.widgets, formfield_callback,
opts.localized_fields, opts.labels, opts.localized_fields, opts.labels,
opts.help_texts, opts.error_messages) opts.help_texts, opts.error_messages,
opts.field_classes)
# 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]
@ -469,7 +477,8 @@ class ModelForm(six.with_metaclass(ModelFormMetaclass, BaseModelForm)):
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, formfield_callback=None, widgets=None, localized_fields=None,
labels=None, help_texts=None, error_messages=None): labels=None, help_texts=None, error_messages=None,
field_classes=None):
""" """
Returns a ModelForm containing form fields for the given model. Returns a ModelForm containing form fields for the given model.
@ -494,6 +503,9 @@ def modelform_factory(model, form=ModelForm, fields=None, exclude=None,
``error_messages`` is a dictionary of model field names mapped to a ``error_messages`` is a dictionary of model field names mapped to a
dictionary of error messages. dictionary of error messages.
``field_classes`` is a dictionary of model field names mapped to a form
field class.
""" """
# Create the inner Meta class. FIXME: ideally, we should be able to # Create the inner Meta class. FIXME: ideally, we should be able to
# construct a ModelForm without creating and passing in a temporary # construct a ModelForm without creating and passing in a temporary
@ -515,6 +527,8 @@ def modelform_factory(model, form=ModelForm, fields=None, exclude=None,
attrs['help_texts'] = help_texts attrs['help_texts'] = help_texts
if error_messages is not None: if error_messages is not None:
attrs['error_messages'] = error_messages attrs['error_messages'] = error_messages
if field_classes is not None:
attrs['field_classes'] = field_classes
# 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.
@ -813,7 +827,7 @@ def modelformset_factory(model, form=ModelForm, formfield_callback=None,
can_order=False, max_num=None, fields=None, exclude=None, 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, labels=None, help_texts=None, error_messages=None,
min_num=None, validate_min=False): min_num=None, validate_min=False, field_classes=None):
""" """
Returns a FormSet class for the given Django model class. Returns a FormSet class for the given Django model class.
""" """
@ -830,7 +844,8 @@ 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, localized_fields=localized_fields, widgets=widgets, localized_fields=localized_fields,
labels=labels, help_texts=help_texts, error_messages=error_messages) labels=labels, help_texts=help_texts,
error_messages=error_messages, field_classes=field_classes)
FormSet = formset_factory(form, formset, extra=extra, min_num=min_num, max_num=max_num, FormSet = formset_factory(form, formset, extra=extra, min_num=min_num, max_num=max_num,
can_order=can_order, can_delete=can_delete, can_order=can_order, can_delete=can_delete,
validate_min=validate_min, validate_max=validate_max) validate_min=validate_min, validate_max=validate_max)
@ -991,7 +1006,7 @@ def inlineformset_factory(parent_model, model, form=ModelForm,
can_delete=True, max_num=None, formfield_callback=None, 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, labels=None, help_texts=None, error_messages=None,
min_num=None, validate_min=False): min_num=None, validate_min=False, field_classes=None):
""" """
Returns an ``InlineFormSet`` for the given kwargs. Returns an ``InlineFormSet`` for the given kwargs.
@ -1020,6 +1035,7 @@ def inlineformset_factory(parent_model, model, form=ModelForm,
'labels': labels, 'labels': labels,
'help_texts': help_texts, 'help_texts': help_texts,
'error_messages': error_messages, 'error_messages': error_messages,
'field_classes': field_classes,
} }
FormSet = modelformset_factory(model, **kwargs) FormSet = modelformset_factory(model, **kwargs)
FormSet.fk = fk FormSet.fk = fk

View File

@ -8,7 +8,7 @@ Model Form API reference. For introductory material about model forms, see the
.. 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, localized_fields=None, labels=None, help_texts=None, error_messages=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, field_classes=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
@ -21,11 +21,11 @@ Model Form API reference. For introductory material about model forms, see the
fields will be excluded from the returned fields, even if they are listed fields will be excluded from the returned fields, even if they are listed
in the ``fields`` argument. in the ``fields`` argument.
``widgets`` is a dictionary of model field names mapped to a widget.
``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.
``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. ``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. ``labels`` is a dictionary of model field names mapped to a label.
@ -35,6 +35,9 @@ Model Form API reference. For introductory material about model forms, see the
``error_messages`` is a dictionary of model field names mapped to a ``error_messages`` is a dictionary of model field names mapped to a
dictionary of error messages. dictionary of error messages.
``field_classes`` is a dictionary of model field names mapped to a form
field class.
See :ref:`modelforms-factory` for example usage. See :ref:`modelforms-factory` for example usage.
You must provide the list of fields explicitly, either via keyword arguments You must provide the list of fields explicitly, either via keyword arguments
@ -48,14 +51,18 @@ Model Form API reference. For introductory material about model forms, see the
Previously, omitting the list of fields was allowed and resulted in Previously, omitting the list of fields was allowed and resulted in
a form with all fields of the model. a form with all fields of the model.
.. 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, min_num=None, validate_min=False) .. versionadded:: 1.9
The ``field_classes`` keyword argument 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, labels=None, help_texts=None, error_messages=None, min_num=None, validate_min=False, field_classes=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``, ``widgets``, ``localized_fields``, ``labels``, ``formfield_callback``, ``widgets``, ``localized_fields``, ``labels``,
``help_texts``, and ``error_messages`` are all passed through to ``help_texts``, ``error_messages``, and ``field_classes`` are all passed
:func:`~django.forms.models.modelform_factory`. through to :func:`~django.forms.models.modelform_factory`.
Arguments ``formset``, ``extra``, ``max_num``, ``can_order``, Arguments ``formset``, ``extra``, ``max_num``, ``can_order``,
``can_delete`` and ``validate_max`` are passed through to ``can_delete`` and ``validate_max`` are passed through to
@ -64,7 +71,11 @@ Model Form API reference. For introductory material about model forms, see the
See :ref:`model-formsets` for example usage. See :ref:`model-formsets` for example usage.
.. 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, min_num=None, validate_min=False) .. versionadded:: 1.9
The ``field_classes`` keyword argument was 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, labels=None, help_texts=None, error_messages=None, min_num=None, validate_min=False, field_classes=None)
Returns an ``InlineFormSet`` using :func:`modelformset_factory` with Returns an ``InlineFormSet`` using :func:`modelformset_factory` with
defaults of ``formset=``:class:`~django.forms.models.BaseInlineFormSet`, defaults of ``formset=``:class:`~django.forms.models.BaseInlineFormSet`,
@ -74,3 +85,7 @@ Model Form API reference. For introductory material about model forms, see the
the ``parent_model``, you must specify a ``fk_name``. the ``parent_model``, you must specify a ``fk_name``.
See :ref:`inline-formsets` for example usage. See :ref:`inline-formsets` for example usage.
.. versionadded:: 1.9
The ``field_classes`` keyword argument was added.

View File

@ -106,7 +106,9 @@ File Uploads
Forms Forms
^^^^^ ^^^^^
* ... * :class:`~django.forms.ModelForm` accepts the new ``Meta`` option
``field_classes`` to customize the type of the fields. See
:ref:`modelforms-overriding-default-fields` for details.
Generic Views Generic Views
^^^^^^^^^^^^^ ^^^^^^^^^^^^^

View File

@ -475,9 +475,8 @@ Overriding the default fields
The default field types, as described in the `Field types`_ table above, are 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 sensible defaults. If you have a ``DateField`` in your model, chances are you'd
want that to be represented as a ``DateField`` in your form. But want that to be represented as a ``DateField`` in your form. But ``ModelForm``
``ModelForm`` gives you the flexibility of changing the form field type and gives you the flexibility of changing the form field for a given model.
widget for a given model field.
To specify a custom widget for a field, use the ``widgets`` attribute of the To specify a custom widget for a field, use the ``widgets`` attribute of the
inner ``Meta`` class. This should be a dictionary mapping field names to widget inner ``Meta`` class. This should be a dictionary mapping field names to widget
@ -525,9 +524,8 @@ the ``name`` field::
}, },
} }
Finally, if you want complete control over of a field -- including its type, You can also specify ``field_classes`` to customize the type of fields
validators, etc. -- you can do this by declaratively specifying fields like you instantiated by the form.
would in a regular ``Form``.
For example, if you wanted to use ``MySlugFormField`` for the ``slug`` For example, if you wanted to use ``MySlugFormField`` for the ``slug``
field, you could do the following:: field, you could do the following::
@ -536,13 +534,18 @@ field, you could do the following::
from myapp.models import Article from myapp.models import Article
class ArticleForm(ModelForm): class ArticleForm(ModelForm):
slug = MySlugFormField()
class Meta: class Meta:
model = Article model = Article
fields = ['pub_date', 'headline', 'content', 'reporter', 'slug'] fields = ['pub_date', 'headline', 'content', 'reporter', 'slug']
field_classes = {
'slug': MySlugFormField,
}
Finally, if you want complete control over of a field -- including its type,
validators, required, etc. -- you can do this by declaratively specifying
fields like you would in a regular ``Form``.
If you want to specify a field's validators, you can do so by defining If you want to specify a field's validators, you can do so by defining
the field declaratively and setting its ``validators`` parameter:: the field declaratively and setting its ``validators`` parameter::
@ -556,6 +559,10 @@ the field declaratively and setting its ``validators`` parameter::
model = Article model = Article
fields = ['pub_date', 'headline', 'content', 'reporter', 'slug'] fields = ['pub_date', 'headline', 'content', 'reporter', 'slug']
.. versionadded:: 1.9
The ``Meta.field_classes`` attribute was added.
.. note:: .. note::
When you explicitly instantiate a form field like this, it is important to When you explicitly instantiate a form field like this, it is important to

View File

@ -11,7 +11,7 @@ from django.core.exceptions import (
) )
from django.core.files.uploadedfile import SimpleUploadedFile from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.validators import ValidationError from django.core.validators import ValidationError
from django.db import connection from django.db import connection, models
from django.db.models.query import EmptyQuerySet from django.db.models.query import EmptyQuerySet
from django.forms.models import ( from django.forms.models import (
ModelFormMetaclass, construct_instance, fields_for_model, model_to_dict, ModelFormMetaclass, construct_instance, fields_for_model, model_to_dict,
@ -545,6 +545,9 @@ class FieldOverridesByFormMetaForm(forms.ModelForm):
) )
} }
} }
field_classes = {
'url': forms.URLField,
}
class TestFieldOverridesByFormMeta(TestCase): class TestFieldOverridesByFormMeta(TestCase):
@ -588,7 +591,7 @@ class TestFieldOverridesByFormMeta(TestCase):
def test_error_messages_overrides(self): def test_error_messages_overrides(self):
form = FieldOverridesByFormMetaForm(data={ form = FieldOverridesByFormMetaForm(data={
'name': 'Category', 'name': 'Category',
'url': '/category/', 'url': 'http://www.example.com/category/',
'slug': '!%#*@', 'slug': '!%#*@',
}) })
form.full_clean() form.full_clean()
@ -599,6 +602,11 @@ class TestFieldOverridesByFormMeta(TestCase):
] ]
self.assertEqual(form.errors, {'slug': error}) self.assertEqual(form.errors, {'slug': error})
def test_field_type_overrides(self):
form = FieldOverridesByFormMetaForm()
self.assertIs(Category._meta.get_field('url').__class__, models.CharField)
self.assertIsInstance(form.fields['url'], forms.URLField)
class IncompleteCategoryFormWithFields(forms.ModelForm): class IncompleteCategoryFormWithFields(forms.ModelForm):
""" """

View File

@ -1431,3 +1431,21 @@ class TestModelFormsetOverridesTroughFormMeta(TestCase):
form = BookFormSet.form(data={'title': 'Foo ' * 30, 'author': author.id}) form = BookFormSet.form(data={'title': 'Foo ' * 30, 'author': author.id})
form.full_clean() form.full_clean()
self.assertEqual(form.errors, {'title': ['Title too long!!']}) self.assertEqual(form.errors, {'title': ['Title too long!!']})
def test_modelformset_factory_field_class_overrides(self):
author = Author.objects.create(pk=1, name='Charles Baudelaire')
BookFormSet = modelformset_factory(Book, fields="__all__", field_classes={
'title': forms.SlugField,
})
form = BookFormSet.form(data={'title': 'Foo ' * 30, 'author': author.id})
self.assertIs(Book._meta.get_field('title').__class__, models.CharField)
self.assertIsInstance(form.fields['title'], forms.SlugField)
def test_inlineformset_factory_field_class_overrides(self):
author = Author.objects.create(pk=1, name='Charles Baudelaire')
BookFormSet = inlineformset_factory(Author, Book, fields="__all__", field_classes={
'title': forms.SlugField,
})
form = BookFormSet.form(data={'title': 'Foo ' * 30, 'author': author.id})
self.assertIs(Book._meta.get_field('title').__class__, models.CharField)
self.assertIsInstance(form.fields['title'], forms.SlugField)