Browse Source

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

Thanks Carl Meyer and Markus Holtermann for the reviews.
Loic Bistuer 10 years ago
parent
commit
00a889167f

+ 24 - 8
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

+ 22 - 7
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.

+ 3 - 1
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
 ^^^^^^^^^^^^^

+ 15 - 8
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

+ 10 - 2
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):
     """

+ 18 - 0
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)