From e03cdf76e78ea992763df4d3e16217d298929301 Mon Sep 17 00:00:00 2001 From: Kamil Turek Date: Thu, 4 Aug 2022 20:39:12 +0200 Subject: [PATCH] Fixed #31721 -- Allowed ModelForm meta to specify form fields. --- django/forms/models.py | 13 +++--------- docs/releases/4.2.txt | 6 +++++- docs/topics/forms/modelforms.txt | 22 ++++++++++++++++++-- tests/model_forms/tests.py | 35 ++++++++++++++++++++++++++++++++ 4 files changed, 63 insertions(+), 13 deletions(-) diff --git a/django/forms/models.py b/django/forms/models.py index 192d9fad94..89cb000271 100644 --- a/django/forms/models.py +++ b/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( diff --git a/docs/releases/4.2.txt b/docs/releases/4.2.txt index 92ccb8e6dd..6760848976 100644 --- a/docs/releases/4.2.txt +++ b/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 ~~~~~~~~~~~~~ diff --git a/docs/topics/forms/modelforms.txt b/docs/topics/forms/modelforms.txt index 3332709089..9000ff24f4 100644 --- a/docs/topics/forms/modelforms.txt +++ b/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 ` for more information on fields and their arguments. +.. versionchanged:: 4.2 + + The ``Meta.formfield_callback`` attribute was added. Enabling localization of fields ------------------------------- diff --git a/tests/model_forms/tests.py b/tests/model_forms/tests.py index d2ce3348c5..8268032e3c 100644 --- a/tests/model_forms/tests.py +++ b/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):