diff --git a/django/forms/models.py b/django/forms/models.py index c861eed3216..98f84a0a440 100644 --- a/django/forms/models.py +++ b/django/forms/models.py @@ -154,7 +154,8 @@ def model_to_dict(instance, fields=None, exclude=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): + labels=None, help_texts=None, error_messages=None, + field_classes=None): """ 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. + ``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. @@ -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 dictionary of error messages. - ``formfield_callback`` is a callable that takes a model field and returns - a form field. + ``field_classes`` is a dictionary of model field names mapped to a form + field class. """ field_list = [] ignored = [] @@ -205,6 +209,8 @@ def fields_for_model(model, fields=None, exclude=None, widgets=None, kwargs['help_text'] = help_texts[f.name] if error_messages and f.name in error_messages: 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: formfield = f.formfield(**kwargs) @@ -236,6 +242,7 @@ class ModelFormOptions(object): self.labels = getattr(options, 'labels', None) self.help_texts = getattr(options, 'help_texts', None) self.error_messages = getattr(options, 'error_messages', None) + self.field_classes = getattr(options, 'field_classes', None) class ModelFormMetaclass(DeclarativeFieldsMetaclass): @@ -280,7 +287,8 @@ class ModelFormMetaclass(DeclarativeFieldsMetaclass): fields = fields_for_model(opts.model, opts.fields, opts.exclude, opts.widgets, formfield_callback, 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 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, 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. @@ -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 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 # 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 if error_messages is not None: 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 # 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, widgets=None, validate_max=False, localized_fields=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. """ @@ -830,7 +844,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, - 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, can_order=can_order, can_delete=can_delete, 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, widgets=None, validate_max=False, localized_fields=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. @@ -1020,6 +1035,7 @@ def inlineformset_factory(parent_model, model, form=ModelForm, 'labels': labels, 'help_texts': help_texts, 'error_messages': error_messages, + 'field_classes': field_classes, } FormSet = modelformset_factory(model, **kwargs) FormSet.fk = fk diff --git a/docs/ref/forms/models.txt b/docs/ref/forms/models.txt index 259dce79f4e..bdf6ba1d892 100644 --- a/docs/ref/forms/models.txt +++ b/docs/ref/forms/models.txt @@ -8,7 +8,7 @@ Model Form API reference. For introductory material about model forms, see the .. 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, 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``. 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 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 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. ``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 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. 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 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. Arguments ``model``, ``form``, ``fields``, ``exclude``, ``formfield_callback``, ``widgets``, ``localized_fields``, ``labels``, - ``help_texts``, and ``error_messages`` are all passed through to - :func:`~django.forms.models.modelform_factory`. + ``help_texts``, ``error_messages``, and ``field_classes`` are all passed + through to :func:`~django.forms.models.modelform_factory`. Arguments ``formset``, ``extra``, ``max_num``, ``can_order``, ``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. -.. 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 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``. See :ref:`inline-formsets` for example usage. + + .. versionadded:: 1.9 + + The ``field_classes`` keyword argument was added. diff --git a/docs/releases/1.9.txt b/docs/releases/1.9.txt index d00bfef096c..9264adf81fe 100644 --- a/docs/releases/1.9.txt +++ b/docs/releases/1.9.txt @@ -106,7 +106,9 @@ File Uploads 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 ^^^^^^^^^^^^^ diff --git a/docs/topics/forms/modelforms.txt b/docs/topics/forms/modelforms.txt index a56a47f8484..49dc4f4c90b 100644 --- a/docs/topics/forms/modelforms.txt +++ b/docs/topics/forms/modelforms.txt @@ -475,9 +475,8 @@ 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 -want that to be represented as a ``DateField`` in your form. But -``ModelForm`` gives you the flexibility of changing the form field type and -widget for a given model field. +want that to be represented as a ``DateField`` in your form. But ``ModelForm`` +gives you the flexibility of changing the form field for a given model. 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 @@ -525,9 +524,8 @@ the ``name`` field:: }, } -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``. +You can also specify ``field_classes`` to customize the type of fields +instantiated by the form. For example, if you wanted to use ``MySlugFormField`` for the ``slug`` field, you could do the following:: @@ -536,13 +534,18 @@ field, you could do the following:: from myapp.models import Article class ArticleForm(ModelForm): - slug = MySlugFormField() - class Meta: model = Article 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 the field declaratively and setting its ``validators`` parameter:: @@ -556,6 +559,10 @@ the field declaratively and setting its ``validators`` parameter:: model = Article fields = ['pub_date', 'headline', 'content', 'reporter', 'slug'] +.. versionadded:: 1.9 + + The ``Meta.field_classes`` attribute was added. + .. note:: When you explicitly instantiate a form field like this, it is important to diff --git a/tests/model_forms/tests.py b/tests/model_forms/tests.py index f1cf77a6a6b..fcc9d269ecf 100644 --- a/tests/model_forms/tests.py +++ b/tests/model_forms/tests.py @@ -11,7 +11,7 @@ from django.core.exceptions import ( ) from django.core.files.uploadedfile import SimpleUploadedFile 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.forms.models import ( 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): @@ -588,7 +591,7 @@ class TestFieldOverridesByFormMeta(TestCase): def test_error_messages_overrides(self): form = FieldOverridesByFormMetaForm(data={ 'name': 'Category', - 'url': '/category/', + 'url': 'http://www.example.com/category/', 'slug': '!%#*@', }) form.full_clean() @@ -599,6 +602,11 @@ class TestFieldOverridesByFormMeta(TestCase): ] 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): """ diff --git a/tests/model_formsets/tests.py b/tests/model_formsets/tests.py index 2d0380195b6..217a1a10495 100644 --- a/tests/model_formsets/tests.py +++ b/tests/model_formsets/tests.py @@ -1431,3 +1431,21 @@ class TestModelFormsetOverridesTroughFormMeta(TestCase): form = BookFormSet.form(data={'title': 'Foo ' * 30, 'author': author.id}) form.full_clean() 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)