Commit ce823d37 authored by Marc Tamlyn's avatar Marc Tamlyn
Browse files

Merge pull request #1382 from loic/ticket19617

Fixed #19617 -- Refactored form metaclasses to support more inheritance scenarios.
parents ed8919cb b16dd1fe
Loading
Loading
Loading
Loading
+43 −10
Original line number Diff line number Diff line
@@ -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,42 @@ 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)

            # 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

        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 +429,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 +438,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"
+26 −22
Original line number Diff line number Diff line
@@ -10,11 +10,11 @@ import warnings

from django.core.exceptions import ValidationError, NON_FIELD_ERRORS, FieldError
from django.forms.fields import Field, ChoiceField
from django.forms.forms import BaseForm, get_declared_fields
from django.forms.forms import DeclarativeFieldsMetaclass, BaseForm
from django.forms.formsets import BaseFormSet, formset_factory
from django.forms.utils import ErrorList
from django.forms.widgets import (SelectMultiple, HiddenInput,
    MultipleHiddenInput, media_property, CheckboxSelectMultiple)
    MultipleHiddenInput, CheckboxSelectMultiple)
from django.utils.encoding import smart_text, force_text
from django.utils import six
from django.utils.text import get_text_list, capfirst
@@ -61,6 +61,7 @@ def construct_instance(form, instance, fields=None, exclude=None):

    return instance


def save_instance(form, instance, fields=None, fail_message='saved',
                  commit=True, exclude=None, construct=True):
    """
@@ -138,6 +139,7 @@ def model_to_dict(instance, fields=None, exclude=None):
            data[f.name] = f.value_from_object(instance)
    return data


def fields_for_model(model, fields=None, exclude=None, widgets=None,
                     formfield_callback=None, localized_fields=None,
                     labels=None, help_texts=None, error_messages=None):
@@ -207,6 +209,7 @@ def fields_for_model(model, fields=None, exclude=None, widgets=None,
        )
    return field_dict


class ModelFormOptions(object):
    def __init__(self, options=None):
        self.model = getattr(options, 'model', None)
@@ -219,22 +222,16 @@ class ModelFormOptions(object):
        self.error_messages = getattr(options, 'error_messages', None)


class ModelFormMetaclass(type):
    def __new__(cls, name, bases, attrs):
class ModelFormMetaclass(DeclarativeFieldsMetaclass):
    def __new__(mcs, name, bases, attrs):
        formfield_callback = attrs.pop('formfield_callback', None)
        try:
            parents = [b for b in bases if issubclass(b, ModelForm)]
        except NameError:
            # We are defining ModelForm itself.
            parents = None
        declared_fields = get_declared_fields(bases, attrs, False)
        new_class = super(ModelFormMetaclass, cls).__new__(cls, name, bases,
                attrs)
        if not parents:

        new_class = (super(ModelFormMetaclass, mcs)
                        .__new__(mcs, name, bases, attrs))

        if bases == (BaseModelForm,):
            return new_class

        if 'media' not in attrs:
            new_class.media = media_property(new_class)
        opts = new_class._meta = ModelFormOptions(getattr(new_class, 'Meta', None))

        # We check if a string was passed to `fields` or `exclude`,
@@ -253,7 +250,6 @@ class ModelFormMetaclass(type):

        if opts.model:
            # If a model is defined, extract form fields from it.

            if opts.fields is None and opts.exclude is None:
                # This should be some kind of assertion error once deprecation
                # cycle is complete.
@@ -263,7 +259,7 @@ class ModelFormMetaclass(type):
                              DeprecationWarning, stacklevel=2)

            if opts.fields == ALL_FIELDS:
                # sentinel for fields_for_model to indicate "get the list of
                # Sentinel for fields_for_model to indicate "get the list of
                # fields from the model"
                opts.fields = None

@@ -274,8 +270,8 @@ class ModelFormMetaclass(type):

            # make sure opts.fields doesn't specify an invalid field
            none_model_fields = [k for k, v in six.iteritems(fields) if not v]
            missing_fields = set(none_model_fields) - \
                             set(declared_fields.keys())
            missing_fields = (set(none_model_fields) -
                              set(new_class.declared_fields.keys()))
            if missing_fields:
                message = 'Unknown field(s) (%s) specified for %s'
                message = message % (', '.join(missing_fields),
@@ -283,13 +279,15 @@ class ModelFormMetaclass(type):
                raise FieldError(message)
            # Override default model fields with any custom declared ones
            # (plus, include all the other declared fields).
            fields.update(declared_fields)
            fields.update(new_class.declared_fields)
        else:
            fields = declared_fields
        new_class.declared_fields = declared_fields
            fields = new_class.declared_fields

        new_class.base_fields = fields

        return new_class


class BaseModelForm(BaseForm):
    def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
                 initial=None, error_class=ErrorList, label_suffix=None,
@@ -438,9 +436,11 @@ class BaseModelForm(BaseForm):

    save.alters_data = True


class ModelForm(six.with_metaclass(ModelFormMetaclass, BaseModelForm)):
    pass


def modelform_factory(model, form=ModelForm, fields=None, exclude=None,
                      formfield_callback=None, widgets=None, localized_fields=None,
                      labels=None, help_texts=None, error_messages=None):
@@ -780,6 +780,7 @@ class BaseModelFormSet(BaseFormSet):
            form.fields[self._pk_field.name] = ModelChoiceField(qs, initial=pk_value, required=False, widget=widget)
        super(BaseModelFormSet, self).add_fields(form, index)


def modelformset_factory(model, form=ModelForm, formfield_callback=None,
                         formset=BaseModelFormSet, extra=1, can_delete=False,
                         can_order=False, max_num=None, fields=None, exclude=None,
@@ -1021,6 +1022,7 @@ class InlineForeignKeyField(Field):
    def _has_changed(self, initial, data):
        return False


class ModelChoiceIterator(object):
    def __init__(self, field):
        self.field = field
@@ -1047,6 +1049,7 @@ class ModelChoiceIterator(object):
    def choice(self, obj):
        return (self.field.prepare_value(obj), self.field.label_from_instance(obj))


class ModelChoiceField(ChoiceField):
    """A ChoiceField whose choices are a model QuerySet."""
    # This class is a subclass of ChoiceField for purity, but it doesn't
@@ -1141,6 +1144,7 @@ class ModelChoiceField(ChoiceField):
        data_value = data if data is not None else ''
        return force_text(self.prepare_value(initial_value)) != force_text(data_value)


class ModelMultipleChoiceField(ModelChoiceField):
    """A MultipleChoiceField whose choices are a model QuerySet."""
    widget = SelectMultiple
+8 −4
Original line number Diff line number Diff line
@@ -131,12 +131,16 @@ def media_property(cls):
    return property(_media)

class MediaDefiningClass(type):
    "Metaclass for classes that can have media definitions"
    def __new__(cls, name, bases, attrs):
        new_class = super(MediaDefiningClass, cls).__new__(cls, name, bases,
                                                           attrs)
    """
    Metaclass for classes that can have media definitions.
    """
    def __new__(mcs, name, bases, attrs):
        new_class = (super(MediaDefiningClass, mcs)
            .__new__(mcs, name, bases, attrs))

        if 'media' not in attrs:
            new_class.media = media_property(new_class)

        return new_class

@python_2_unicode_compatible
+2 −0
Original line number Diff line number Diff line
@@ -467,6 +467,8 @@ these changes.
* The ``use_natural_keys`` argument for ``serializers.serialize()`` will be
  removed. Use ``use_natural_foreign_keys`` instead.

* ``django.forms.get_declared_fields`` will be removed.

2.0
---

+7 −0
Original line number Diff line number Diff line
@@ -854,6 +854,13 @@ classes::
    <li>Instrument: <input type="text" name="instrument" /></li>
    <li>Haircut type: <input type="text" name="haircut_type" /></li>

.. 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
Loading