diff --git a/django/forms/forms.py b/django/forms/forms.py index 8d0fa23daa..15c3e2c3f8 100644 --- a/django/forms/forms.py +++ b/django/forms/forms.py @@ -90,6 +90,11 @@ class DeclarativeFieldsMetaclass(MediaDefiningClass): if hasattr(base, 'declared_fields'): declared_fields.update(base.declared_fields) + # Field shadowing. + for attr in base.__dict__.keys(): + if attr in declared_fields: + declared_fields.pop(attr) + new_class.base_fields = declared_fields new_class.declared_fields = declared_fields diff --git a/docs/ref/forms/api.txt b/docs/ref/forms/api.txt index 14092512dc..c15f748308 100644 --- a/docs/ref/forms/api.txt +++ b/docs/ref/forms/api.txt @@ -854,6 +854,13 @@ classes::
  • Instrument:
  • Haircut type:
  • +.. versionadded:: 1.7 + +* It's possible to opt-out from a ``Field`` inherited from a parent class by + shadowing it. While any non-``Field`` value works for this purpose, it's + recommended to use ``None`` to make it explicit that a field is being + nullified. + .. _form-prefix: Prefixes for forms diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt index 87bf8481a0..5c208e22ac 100644 --- a/docs/releases/1.7.txt +++ b/docs/releases/1.7.txt @@ -293,6 +293,9 @@ Forms inheriting from both ``Form`` and ``ModelForm`` simultaneously have been removed as long as ``ModelForm`` appears first in the MRO. +* It's now possible to opt-out from a ``Form`` field declared in a parent class + by shadowing it with a non-``Field`` value. + Internationalization ^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/topics/forms/modelforms.txt b/docs/topics/forms/modelforms.txt index edf9de17dd..2a48aa75dc 100644 --- a/docs/topics/forms/modelforms.txt +++ b/docs/topics/forms/modelforms.txt @@ -651,6 +651,18 @@ There are a couple of things to note, however. because these classes rely on different metaclasses and a class can only have one metaclass. +.. versionadded:: 1.7 + +* It's possible to opt-out from a ``Field`` inherited from a parent class by + shadowing it. While any non-``Field`` value works for this purpose, it's + recommended to use ``None`` to make it explicit that a field is being + nullified. + + You can only use this technique to opt out from a field defined declaratively + by a parent class; it won't prevent the ``ModelForm`` metaclass from generating + a default field. To opt-out from default fields, see + :ref:`controlling-fields-with-fields-and-exclude`. + .. _modelforms-factory: ModelForm factory function @@ -749,6 +761,8 @@ instances of the model, you can specify an empty QuerySet:: >>> AuthorFormSet(queryset=Author.objects.none()) +.. _controlling-fields-with-fields-and-exclude: + Controlling which fields are used with ``fields`` and ``exclude`` ----------------------------------------------------------------- diff --git a/tests/model_forms/tests.py b/tests/model_forms/tests.py index 67aedd06d0..2f94f64128 100644 --- a/tests/model_forms/tests.py +++ b/tests/model_forms/tests.py @@ -1815,3 +1815,26 @@ class ModelFormInheritanceTests(TestCase): fields = '__all__' self.assertEqual(list(ModelForm().fields.keys()), ['name', 'age']) + + def test_field_shadowing(self): + class ModelForm(forms.ModelForm): + class Meta: + model = Writer + fields = '__all__' + + class Mixin(object): + age = None + + class Form(forms.Form): + age = forms.IntegerField() + + class Form2(forms.Form): + foo = forms.IntegerField() + + self.assertEqual(list(ModelForm().fields.keys()), ['name']) + self.assertEqual(list(type(str('NewForm'), (Mixin, Form), {})().fields.keys()), []) + self.assertEqual(list(type(str('NewForm'), (Form2, Mixin, Form), {})().fields.keys()), ['foo']) + self.assertEqual(list(type(str('NewForm'), (Mixin, ModelForm, Form), {})().fields.keys()), ['name']) + self.assertEqual(list(type(str('NewForm'), (ModelForm, Mixin, Form), {})().fields.keys()), ['name']) + self.assertEqual(list(type(str('NewForm'), (ModelForm, Form, Mixin), {})().fields.keys()), ['name', 'age']) + self.assertEqual(list(type(str('NewForm'), (ModelForm, Form), {'age': None})().fields.keys()), ['name'])