Fixed #19617 -- Refactored Form metaclasses to support more inheritance scenarios.
Thanks apollo13, funkybob and mjtamlyn for the reviews.
This commit is contained in:
parent
54cd930baf
commit
ac5ec7b8bc
|
@ -11,7 +11,7 @@ import warnings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.forms.fields import Field, FileField
|
from django.forms.fields import Field, FileField
|
||||||
from django.forms.utils import flatatt, ErrorDict, ErrorList
|
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.html import conditional_escape, format_html
|
||||||
from django.utils.encoding import smart_text, force_text, python_2_unicode_compatible
|
from django.utils.encoding import smart_text, force_text, python_2_unicode_compatible
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
@ -29,6 +29,7 @@ def pretty_name(name):
|
||||||
return ''
|
return ''
|
||||||
return name.replace('_', ' ').capitalize()
|
return name.replace('_', ' ').capitalize()
|
||||||
|
|
||||||
|
|
||||||
def get_declared_fields(bases, attrs, with_base_fields=True):
|
def get_declared_fields(bases, attrs, with_base_fields=True):
|
||||||
"""
|
"""
|
||||||
Create a list of form field instances from the passed in 'attrs', plus any
|
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.
|
used. The distinction is useful in ModelForm subclassing.
|
||||||
Also integrates any additional media definitions.
|
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 = [(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)
|
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)
|
return OrderedDict(fields)
|
||||||
|
|
||||||
class DeclarativeFieldsMetaclass(type):
|
|
||||||
|
class DeclarativeFieldsMetaclass(MediaDefiningClass):
|
||||||
"""
|
"""
|
||||||
Metaclass that converts Field attributes to a dictionary called
|
Metaclass that collects Fields declared on the base classes.
|
||||||
'base_fields', taking into account parent class 'base_fields' as well.
|
|
||||||
"""
|
"""
|
||||||
def __new__(cls, name, bases, attrs):
|
def __new__(mcs, name, bases, attrs):
|
||||||
attrs['base_fields'] = get_declared_fields(bases, attrs)
|
# Collect fields from current class.
|
||||||
new_class = super(DeclarativeFieldsMetaclass,
|
current_fields = []
|
||||||
cls).__new__(cls, name, bases, attrs)
|
for key, value in list(attrs.items()):
|
||||||
if 'media' not in attrs:
|
if isinstance(value, Field):
|
||||||
new_class.media = media_property(new_class)
|
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
|
return new_class
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class BaseForm(object):
|
class BaseForm(object):
|
||||||
# This is the main implementation of all the Form logic. Note that this
|
# 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]
|
return [field for field in self if not field.is_hidden]
|
||||||
|
|
||||||
|
|
||||||
class Form(six.with_metaclass(DeclarativeFieldsMetaclass, BaseForm)):
|
class Form(six.with_metaclass(DeclarativeFieldsMetaclass, BaseForm)):
|
||||||
"A collection of Fields, plus their associated data."
|
"A collection of Fields, plus their associated data."
|
||||||
# This is a separate class from BaseForm in order to abstract the way
|
# 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.
|
# to define a form using declarative syntax.
|
||||||
# BaseForm itself has no way of designating self.fields.
|
# BaseForm itself has no way of designating self.fields.
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class BoundField(object):
|
class BoundField(object):
|
||||||
"A Field plus data"
|
"A Field plus data"
|
||||||
|
|
|
@ -10,11 +10,11 @@ import warnings
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError, NON_FIELD_ERRORS, FieldError
|
from django.core.exceptions import ValidationError, NON_FIELD_ERRORS, FieldError
|
||||||
from django.forms.fields import Field, ChoiceField
|
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.formsets import BaseFormSet, formset_factory
|
||||||
from django.forms.utils import ErrorList
|
from django.forms.utils import ErrorList
|
||||||
from django.forms.widgets import (SelectMultiple, HiddenInput,
|
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.encoding import smart_text, force_text
|
||||||
from django.utils import six
|
from django.utils import six
|
||||||
from django.utils.text import get_text_list, capfirst
|
from django.utils.text import get_text_list, capfirst
|
||||||
|
@ -61,6 +61,7 @@ def construct_instance(form, instance, fields=None, exclude=None):
|
||||||
|
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
|
||||||
def save_instance(form, instance, fields=None, fail_message='saved',
|
def save_instance(form, instance, fields=None, fail_message='saved',
|
||||||
commit=True, exclude=None, construct=True):
|
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)
|
data[f.name] = f.value_from_object(instance)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
def fields_for_model(model, fields=None, exclude=None, widgets=None,
|
def fields_for_model(model, fields=None, exclude=None, widgets=None,
|
||||||
formfield_callback=None, localized_fields=None,
|
formfield_callback=None, localized_fields=None,
|
||||||
labels=None, help_texts=None, error_messages=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
|
return field_dict
|
||||||
|
|
||||||
|
|
||||||
class ModelFormOptions(object):
|
class ModelFormOptions(object):
|
||||||
def __init__(self, options=None):
|
def __init__(self, options=None):
|
||||||
self.model = getattr(options, 'model', None)
|
self.model = getattr(options, 'model', None)
|
||||||
|
@ -219,22 +222,16 @@ class ModelFormOptions(object):
|
||||||
self.error_messages = getattr(options, 'error_messages', None)
|
self.error_messages = getattr(options, 'error_messages', None)
|
||||||
|
|
||||||
|
|
||||||
class ModelFormMetaclass(type):
|
class ModelFormMetaclass(DeclarativeFieldsMetaclass):
|
||||||
def __new__(cls, name, bases, attrs):
|
def __new__(mcs, name, bases, attrs):
|
||||||
formfield_callback = attrs.pop('formfield_callback', None)
|
formfield_callback = attrs.pop('formfield_callback', None)
|
||||||
try:
|
|
||||||
parents = [b for b in bases if issubclass(b, ModelForm)]
|
new_class = (super(ModelFormMetaclass, mcs)
|
||||||
except NameError:
|
.__new__(mcs, name, bases, attrs))
|
||||||
# We are defining ModelForm itself.
|
|
||||||
parents = None
|
if bases == (BaseModelForm,):
|
||||||
declared_fields = get_declared_fields(bases, attrs, False)
|
|
||||||
new_class = super(ModelFormMetaclass, cls).__new__(cls, name, bases,
|
|
||||||
attrs)
|
|
||||||
if not parents:
|
|
||||||
return new_class
|
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))
|
opts = new_class._meta = ModelFormOptions(getattr(new_class, 'Meta', None))
|
||||||
|
|
||||||
# We check if a string was passed to `fields` or `exclude`,
|
# We check if a string was passed to `fields` or `exclude`,
|
||||||
|
@ -253,7 +250,6 @@ class ModelFormMetaclass(type):
|
||||||
|
|
||||||
if opts.model:
|
if opts.model:
|
||||||
# If a model is defined, extract form fields from it.
|
# If a model is defined, extract form fields from it.
|
||||||
|
|
||||||
if opts.fields is None and opts.exclude is None:
|
if opts.fields is None and opts.exclude is None:
|
||||||
# This should be some kind of assertion error once deprecation
|
# This should be some kind of assertion error once deprecation
|
||||||
# cycle is complete.
|
# cycle is complete.
|
||||||
|
@ -263,7 +259,7 @@ class ModelFormMetaclass(type):
|
||||||
DeprecationWarning, stacklevel=2)
|
DeprecationWarning, stacklevel=2)
|
||||||
|
|
||||||
if opts.fields == ALL_FIELDS:
|
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"
|
# fields from the model"
|
||||||
opts.fields = None
|
opts.fields = None
|
||||||
|
|
||||||
|
@ -274,8 +270,8 @@ class ModelFormMetaclass(type):
|
||||||
|
|
||||||
# make sure opts.fields doesn't specify an invalid field
|
# make sure opts.fields doesn't specify an invalid field
|
||||||
none_model_fields = [k for k, v in six.iteritems(fields) if not v]
|
none_model_fields = [k for k, v in six.iteritems(fields) if not v]
|
||||||
missing_fields = set(none_model_fields) - \
|
missing_fields = (set(none_model_fields) -
|
||||||
set(declared_fields.keys())
|
set(new_class.declared_fields.keys()))
|
||||||
if missing_fields:
|
if missing_fields:
|
||||||
message = 'Unknown field(s) (%s) specified for %s'
|
message = 'Unknown field(s) (%s) specified for %s'
|
||||||
message = message % (', '.join(missing_fields),
|
message = message % (', '.join(missing_fields),
|
||||||
|
@ -283,13 +279,15 @@ class ModelFormMetaclass(type):
|
||||||
raise FieldError(message)
|
raise FieldError(message)
|
||||||
# Override default model fields with any custom declared ones
|
# Override default model fields with any custom declared ones
|
||||||
# (plus, include all the other declared fields).
|
# (plus, include all the other declared fields).
|
||||||
fields.update(declared_fields)
|
fields.update(new_class.declared_fields)
|
||||||
else:
|
else:
|
||||||
fields = declared_fields
|
fields = new_class.declared_fields
|
||||||
new_class.declared_fields = declared_fields
|
|
||||||
new_class.base_fields = fields
|
new_class.base_fields = fields
|
||||||
|
|
||||||
return new_class
|
return new_class
|
||||||
|
|
||||||
|
|
||||||
class BaseModelForm(BaseForm):
|
class BaseModelForm(BaseForm):
|
||||||
def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
|
def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
|
||||||
initial=None, error_class=ErrorList, label_suffix=None,
|
initial=None, error_class=ErrorList, label_suffix=None,
|
||||||
|
@ -438,9 +436,11 @@ class BaseModelForm(BaseForm):
|
||||||
|
|
||||||
save.alters_data = True
|
save.alters_data = True
|
||||||
|
|
||||||
|
|
||||||
class ModelForm(six.with_metaclass(ModelFormMetaclass, BaseModelForm)):
|
class ModelForm(six.with_metaclass(ModelFormMetaclass, BaseModelForm)):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def modelform_factory(model, form=ModelForm, fields=None, exclude=None,
|
def modelform_factory(model, form=ModelForm, fields=None, exclude=None,
|
||||||
formfield_callback=None, widgets=None, localized_fields=None,
|
formfield_callback=None, widgets=None, localized_fields=None,
|
||||||
labels=None, help_texts=None, error_messages=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)
|
form.fields[self._pk_field.name] = ModelChoiceField(qs, initial=pk_value, required=False, widget=widget)
|
||||||
super(BaseModelFormSet, self).add_fields(form, index)
|
super(BaseModelFormSet, self).add_fields(form, index)
|
||||||
|
|
||||||
|
|
||||||
def modelformset_factory(model, form=ModelForm, formfield_callback=None,
|
def modelformset_factory(model, form=ModelForm, formfield_callback=None,
|
||||||
formset=BaseModelFormSet, extra=1, can_delete=False,
|
formset=BaseModelFormSet, extra=1, can_delete=False,
|
||||||
can_order=False, max_num=None, fields=None, exclude=None,
|
can_order=False, max_num=None, fields=None, exclude=None,
|
||||||
|
@ -1021,6 +1022,7 @@ class InlineForeignKeyField(Field):
|
||||||
def _has_changed(self, initial, data):
|
def _has_changed(self, initial, data):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
class ModelChoiceIterator(object):
|
class ModelChoiceIterator(object):
|
||||||
def __init__(self, field):
|
def __init__(self, field):
|
||||||
self.field = field
|
self.field = field
|
||||||
|
@ -1047,6 +1049,7 @@ class ModelChoiceIterator(object):
|
||||||
def choice(self, obj):
|
def choice(self, obj):
|
||||||
return (self.field.prepare_value(obj), self.field.label_from_instance(obj))
|
return (self.field.prepare_value(obj), self.field.label_from_instance(obj))
|
||||||
|
|
||||||
|
|
||||||
class ModelChoiceField(ChoiceField):
|
class ModelChoiceField(ChoiceField):
|
||||||
"""A ChoiceField whose choices are a model QuerySet."""
|
"""A ChoiceField whose choices are a model QuerySet."""
|
||||||
# This class is a subclass of ChoiceField for purity, but it doesn't
|
# 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 ''
|
data_value = data if data is not None else ''
|
||||||
return force_text(self.prepare_value(initial_value)) != force_text(data_value)
|
return force_text(self.prepare_value(initial_value)) != force_text(data_value)
|
||||||
|
|
||||||
|
|
||||||
class ModelMultipleChoiceField(ModelChoiceField):
|
class ModelMultipleChoiceField(ModelChoiceField):
|
||||||
"""A MultipleChoiceField whose choices are a model QuerySet."""
|
"""A MultipleChoiceField whose choices are a model QuerySet."""
|
||||||
widget = SelectMultiple
|
widget = SelectMultiple
|
||||||
|
|
|
@ -131,12 +131,16 @@ def media_property(cls):
|
||||||
return property(_media)
|
return property(_media)
|
||||||
|
|
||||||
class MediaDefiningClass(type):
|
class MediaDefiningClass(type):
|
||||||
"Metaclass for classes that can have media definitions"
|
"""
|
||||||
def __new__(cls, name, bases, attrs):
|
Metaclass for classes that can have media definitions.
|
||||||
new_class = super(MediaDefiningClass, cls).__new__(cls, name, bases,
|
"""
|
||||||
attrs)
|
def __new__(mcs, name, bases, attrs):
|
||||||
|
new_class = (super(MediaDefiningClass, mcs)
|
||||||
|
.__new__(mcs, name, bases, attrs))
|
||||||
|
|
||||||
if 'media' not in attrs:
|
if 'media' not in attrs:
|
||||||
new_class.media = media_property(new_class)
|
new_class.media = media_property(new_class)
|
||||||
|
|
||||||
return new_class
|
return new_class
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
|
|
|
@ -467,6 +467,8 @@ these changes.
|
||||||
* The ``use_natural_keys`` argument for ``serializers.serialize()`` will be
|
* The ``use_natural_keys`` argument for ``serializers.serialize()`` will be
|
||||||
removed. Use ``use_natural_foreign_keys`` instead.
|
removed. Use ``use_natural_foreign_keys`` instead.
|
||||||
|
|
||||||
|
* ``django.forms.get_declared_fields`` will be removed.
|
||||||
|
|
||||||
2.0
|
2.0
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
@ -288,6 +288,11 @@ Forms
|
||||||
:func:`~django.forms.formsets.formset_factory` to allow validating
|
:func:`~django.forms.formsets.formset_factory` to allow validating
|
||||||
a minimum number of submitted forms.
|
a minimum number of submitted forms.
|
||||||
|
|
||||||
|
* The metaclasses used by ``Form`` and ``ModelForm`` have been reworked to
|
||||||
|
support more inheritance scenarios. The previous limitation that prevented
|
||||||
|
inheriting from both ``Form`` and ``ModelForm`` simultaneously have been
|
||||||
|
removed as long as ``ModelForm`` appears first in the MRO.
|
||||||
|
|
||||||
Internationalization
|
Internationalization
|
||||||
^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
@ -513,6 +518,12 @@ Miscellaneous
|
||||||
still worked, even though it was not documented or officially supported. If
|
still worked, even though it was not documented or officially supported. If
|
||||||
you're still using it, please update to the current :setting:`CACHES` syntax.
|
you're still using it, please update to the current :setting:`CACHES` syntax.
|
||||||
|
|
||||||
|
* The default ordering of ``Form`` fields in case of inheritance has changed to
|
||||||
|
follow normal Python MRO. Fields are now discovered by iterating through the
|
||||||
|
MRO in reverse with the topmost class coming last. This only affects you if
|
||||||
|
you relied on the default field ordering while having fields defined on both
|
||||||
|
the current class *and* on a parent ``Form``.
|
||||||
|
|
||||||
Features deprecated in 1.7
|
Features deprecated in 1.7
|
||||||
==========================
|
==========================
|
||||||
|
|
||||||
|
|
|
@ -644,11 +644,12 @@ There are a couple of things to note, however.
|
||||||
used. This means the child's ``Meta``, if it exists, otherwise the
|
used. This means the child's ``Meta``, if it exists, otherwise the
|
||||||
``Meta`` of the first parent, etc.
|
``Meta`` of the first parent, etc.
|
||||||
|
|
||||||
* For technical reasons, a subclass cannot inherit from both a ``ModelForm``
|
.. versionchanged:: 1.7
|
||||||
and a ``Form`` simultaneously.
|
|
||||||
|
|
||||||
Chances are these notes won't affect you unless you're trying to do something
|
* It's possible to inherit from both ``Form`` and ``ModelForm`` simultaneuosly,
|
||||||
tricky with subclassing.
|
however, you must ensure that ``ModelForm`` appears first in the MRO. This is
|
||||||
|
because these classes rely on different metaclasses and a class can only have
|
||||||
|
one metaclass.
|
||||||
|
|
||||||
.. _modelforms-factory:
|
.. _modelforms-factory:
|
||||||
|
|
||||||
|
|
|
@ -1302,10 +1302,10 @@ class FormsTestCase(TestCase):
|
||||||
haircut_type = CharField()
|
haircut_type = CharField()
|
||||||
|
|
||||||
b = Beatle(auto_id=False)
|
b = Beatle(auto_id=False)
|
||||||
self.assertHTMLEqual(b.as_ul(), """<li>First name: <input type="text" name="first_name" /></li>
|
self.assertHTMLEqual(b.as_ul(), """<li>Instrument: <input type="text" name="instrument" /></li>
|
||||||
|
<li>First name: <input type="text" name="first_name" /></li>
|
||||||
<li>Last name: <input type="text" name="last_name" /></li>
|
<li>Last name: <input type="text" name="last_name" /></li>
|
||||||
<li>Birthday: <input type="text" name="birthday" /></li>
|
<li>Birthday: <input type="text" name="birthday" /></li>
|
||||||
<li>Instrument: <input type="text" name="instrument" /></li>
|
|
||||||
<li>Haircut type: <input type="text" name="haircut_type" /></li>""")
|
<li>Haircut type: <input type="text" name="haircut_type" /></li>""")
|
||||||
|
|
||||||
def test_forms_with_prefixes(self):
|
def test_forms_with_prefixes(self):
|
||||||
|
|
|
@ -1802,3 +1802,16 @@ class M2mHelpTextTest(TestCase):
|
||||||
html = form.as_p()
|
html = form.as_p()
|
||||||
self.assertInHTML('<ul id="id_status">', html)
|
self.assertInHTML('<ul id="id_status">', html)
|
||||||
self.assertInHTML(dreaded_help_text, html, count=0)
|
self.assertInHTML(dreaded_help_text, html, count=0)
|
||||||
|
|
||||||
|
|
||||||
|
class ModelFormInheritanceTests(TestCase):
|
||||||
|
def test_form_subclass_inheritance(self):
|
||||||
|
class Form(forms.Form):
|
||||||
|
age = forms.IntegerField()
|
||||||
|
|
||||||
|
class ModelForm(forms.ModelForm, Form):
|
||||||
|
class Meta:
|
||||||
|
model = Writer
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
self.assertEqual(list(ModelForm().fields.keys()), ['name', 'age'])
|
||||||
|
|
Loading…
Reference in New Issue