From ac5ec7b8bcc5623cc05d4df900c39e194f5affcf Mon Sep 17 00:00:00 2001 From: Loic Bistuer Date: Sun, 21 Jul 2013 02:25:27 +0700 Subject: Fixed #19617 -- Refactored Form metaclasses to support more inheritance scenarios. Thanks apollo13, funkybob and mjtamlyn for the reviews. --- django/forms/forms.py | 48 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 38 insertions(+), 10 deletions(-) (limited to 'django/forms/forms.py') diff --git a/django/forms/forms.py b/django/forms/forms.py index 490d9c2c3b..8d0fa23daa 100644 --- a/django/forms/forms.py +++ b/django/forms/forms.py @@ -11,7 +11,7 @@ import warnings from django.core.exceptions import ValidationError from django.forms.fields import Field, FileField from django.forms.utils import flatatt, ErrorDict, ErrorList -from django.forms.widgets import Media, media_property, TextInput, Textarea +from django.forms.widgets import Media, MediaDefiningClass, TextInput, Textarea from django.utils.html import conditional_escape, format_html from django.utils.encoding import smart_text, force_text, python_2_unicode_compatible from django.utils.safestring import mark_safe @@ -29,6 +29,7 @@ def pretty_name(name): return '' return name.replace('_', ' ').capitalize() + def get_declared_fields(bases, attrs, with_base_fields=True): """ Create a list of form field instances from the passed in 'attrs', plus any @@ -40,6 +41,13 @@ def get_declared_fields(bases, attrs, with_base_fields=True): used. The distinction is useful in ModelForm subclassing. Also integrates any additional media definitions. """ + + warnings.warn( + "get_declared_fields is deprecated and will be removed in Django 1.9.", + PendingDeprecationWarning, + stacklevel=2, + ) + fields = [(field_name, attrs.pop(field_name)) for field_name, obj in list(six.iteritems(attrs)) if isinstance(obj, Field)] fields.sort(key=lambda x: x[1].creation_counter) @@ -57,19 +65,37 @@ def get_declared_fields(bases, attrs, with_base_fields=True): return OrderedDict(fields) -class DeclarativeFieldsMetaclass(type): + +class DeclarativeFieldsMetaclass(MediaDefiningClass): """ - Metaclass that converts Field attributes to a dictionary called - 'base_fields', taking into account parent class 'base_fields' as well. + Metaclass that collects Fields declared on the base classes. """ - def __new__(cls, name, bases, attrs): - attrs['base_fields'] = get_declared_fields(bases, attrs) - new_class = super(DeclarativeFieldsMetaclass, - cls).__new__(cls, name, bases, attrs) - if 'media' not in attrs: - new_class.media = media_property(new_class) + def __new__(mcs, name, bases, attrs): + # Collect fields from current class. + current_fields = [] + for key, value in list(attrs.items()): + if isinstance(value, Field): + current_fields.append((key, value)) + attrs.pop(key) + current_fields.sort(key=lambda x: x[1].creation_counter) + attrs['declared_fields'] = OrderedDict(current_fields) + + new_class = (super(DeclarativeFieldsMetaclass, mcs) + .__new__(mcs, name, bases, attrs)) + + # Walk through the MRO. + declared_fields = OrderedDict() + for base in reversed(new_class.__mro__): + # Collect fields from base class. + if hasattr(base, 'declared_fields'): + declared_fields.update(base.declared_fields) + + new_class.base_fields = declared_fields + new_class.declared_fields = declared_fields + return new_class + @python_2_unicode_compatible class BaseForm(object): # This is the main implementation of all the Form logic. Note that this @@ -398,6 +424,7 @@ class BaseForm(object): """ return [field for field in self if not field.is_hidden] + class Form(six.with_metaclass(DeclarativeFieldsMetaclass, BaseForm)): "A collection of Fields, plus their associated data." # This is a separate class from BaseForm in order to abstract the way @@ -406,6 +433,7 @@ class Form(six.with_metaclass(DeclarativeFieldsMetaclass, BaseForm)): # to define a form using declarative syntax. # BaseForm itself has no way of designating self.fields. + @python_2_unicode_compatible class BoundField(object): "A Field plus data" -- cgit v1.3 From b16dd1fe019b38d0dcdf4a400e9377fb06b33178 Mon Sep 17 00:00:00 2001 From: Loic Bistuer Date: Mon, 14 Oct 2013 22:42:33 +0700 Subject: Fixed #8620 -- Updated the Form metaclass to support excluding fields by shadowing them. --- django/forms/forms.py | 5 +++++ docs/ref/forms/api.txt | 7 +++++++ docs/releases/1.7.txt | 3 +++ docs/topics/forms/modelforms.txt | 14 ++++++++++++++ tests/model_forms/tests.py | 23 +++++++++++++++++++++++ 5 files changed, 52 insertions(+) (limited to 'django/forms/forms.py') 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']) -- cgit v1.3