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

This commit is contained in:
Kamil Turek 2022-08-04 20:39:12 +02:00 committed by Mariusz Felisiak
parent 88e67a54b7
commit e03cdf76e7
4 changed files with 63 additions and 13 deletions

View File

@ -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(

View File

@ -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
~~~~~~~~~~~~~

View File

@ -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
-------------------------------

View File

@ -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):