Browse Source

Fixed #31721 -- Allowed ModelForm meta to specify form fields.

Kamil Turek 2 years ago
parent
commit
e03cdf76e7
4 changed files with 63 additions and 13 deletions
  1. 3 10
      django/forms/models.py
  2. 5 1
      docs/releases/4.2.txt
  3. 20 2
      docs/topics/forms/modelforms.txt
  4. 35 0
      tests/model_forms/tests.py

+ 3 - 10
django/forms/models.py

@@ -253,18 +253,11 @@ class ModelFormOptions:
         self.help_texts = getattr(options, "help_texts", None)
         self.error_messages = getattr(options, "error_messages", None)
         self.field_classes = getattr(options, "field_classes", None)
+        self.formfield_callback = getattr(options, "formfield_callback", None)
 
 
 class ModelFormMetaclass(DeclarativeFieldsMetaclass):
     def __new__(mcs, name, bases, attrs):
-        base_formfield_callback = None
-        for b in bases:
-            if hasattr(b, "Meta") and hasattr(b.Meta, "formfield_callback"):
-                base_formfield_callback = b.Meta.formfield_callback
-                break
-
-        formfield_callback = attrs.pop("formfield_callback", base_formfield_callback)
-
         new_class = super().__new__(mcs, name, bases, attrs)
 
         if bases == (BaseModelForm,):
@@ -308,7 +301,7 @@ class ModelFormMetaclass(DeclarativeFieldsMetaclass):
                 opts.fields,
                 opts.exclude,
                 opts.widgets,
-                formfield_callback,
+                opts.formfield_callback,
                 opts.localized_fields,
                 opts.labels,
                 opts.help_texts,
@@ -636,7 +629,7 @@ def modelform_factory(
     class_name = model.__name__ + "Form"
 
     # Class attributes for the new form class.
-    form_class_attrs = {"Meta": Meta, "formfield_callback": formfield_callback}
+    form_class_attrs = {"Meta": Meta}
 
     if getattr(Meta, "fields", None) is None and getattr(Meta, "exclude", None) is None:
         raise ImproperlyConfigured(

+ 5 - 1
docs/releases/4.2.txt

@@ -151,7 +151,11 @@ File Uploads
 Forms
 ~~~~~
 
-* ...
+* :class:`~django.forms.ModelForm` now accepts the new ``Meta`` option
+  ``formfield_callback`` to customize form fields.
+
+* :func:`~django.forms.models.modelform_factory` now respects the
+  ``formfield_callback`` attribute of the ``form``’s ``Meta``.
 
 Generic Views
 ~~~~~~~~~~~~~

+ 20 - 2
docs/topics/forms/modelforms.txt

@@ -548,8 +548,8 @@ the ``name`` field::
                 },
             }
 
-You can also specify ``field_classes`` to customize the type of fields
-instantiated by the form.
+You can also specify ``field_classes`` or ``formfield_callback`` 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::
@@ -565,6 +565,21 @@ field, you could do the following::
                 'slug': MySlugFormField,
             }
 
+or::
+
+    from django.forms import ModelForm
+    from myapp.models import Article
+
+    def formfield_for_dbfield(db_field, **kwargs):
+        if db_field.name == "slug":
+            return MySlugFormField()
+        return db_field.formfield(**kwargs)
+
+    class ArticleForm(ModelForm):
+        class Meta:
+            model = Article
+            fields = ["pub_date", "headline", "content", "reporter", "slug"]
+            formfield_callback = formfield_for_dbfield
 
 Finally, if you want complete control over of a field -- including its type,
 validators, required, etc. -- you can do this by declaratively specifying
@@ -638,6 +653,9 @@ the field declaratively and setting its ``validators`` parameter::
     See the :doc:`form field documentation </ref/forms/fields>` for more information
     on fields and their arguments.
 
+.. versionchanged:: 4.2
+
+    The ``Meta.formfield_callback`` attribute was added.
 
 Enabling localization of fields
 -------------------------------

+ 35 - 0
tests/model_forms/tests.py

@@ -3496,6 +3496,41 @@ class FormFieldCallbackTests(SimpleTestCase):
                 type(NewForm.base_fields[name].widget),
             )
 
+    def test_custom_callback_in_meta(self):
+        def callback(db_field, **kwargs):
+            return forms.CharField(widget=forms.Textarea)
+
+        class NewForm(forms.ModelForm):
+            class Meta:
+                model = Person
+                fields = ["id", "name"]
+                formfield_callback = callback
+
+        for field in NewForm.base_fields.values():
+            self.assertEqual(type(field.widget), forms.Textarea)
+
+    def test_custom_callback_from_base_form_meta(self):
+        def callback(db_field, **kwargs):
+            return forms.CharField(widget=forms.Textarea)
+
+        class BaseForm(forms.ModelForm):
+            class Meta:
+                model = Person
+                fields = "__all__"
+                formfield_callback = callback
+
+        NewForm = modelform_factory(model=Person, form=BaseForm)
+
+        class InheritedForm(NewForm):
+            pass
+
+        for name, field in NewForm.base_fields.items():
+            self.assertEqual(type(field.widget), forms.Textarea)
+            self.assertEqual(
+                type(field.widget),
+                type(InheritedForm.base_fields[name].widget),
+            )
+
 
 class LocalizedModelFormTest(TestCase):
     def test_model_form_applies_localize_to_some_fields(self):