فهرست منبع

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

Thanks Carl Meyer and Markus Holtermann for the reviews.
Loic Bistuer 10 سال پیش
والد
کامیت
00a889167f
6فایلهای تغییر یافته به همراه92 افزوده شده و 26 حذف شده
  1. 24 8
      django/forms/models.py
  2. 22 7
      docs/ref/forms/models.txt
  3. 3 1
      docs/releases/1.9.txt
  4. 15 8
      docs/topics/forms/modelforms.txt
  5. 10 2
      tests/model_forms/tests.py
  6. 18 0
      tests/model_formsets/tests.py

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