Fixed #31026 -- Switched form rendering to template engine.
Thanks Carlton Gibson, Keryn Knight, Mariusz Felisiak, and Nick Pope for reviews. Co-authored-by: Johannes Hoppe <info@johanneshoppe.com>
This commit is contained in:
parent
5353e7c250
commit
456466d932
|
@ -1,11 +1,10 @@
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.forms.utils import flatatt, pretty_name
|
from django.forms.utils import pretty_name
|
||||||
from django.forms.widgets import MultiWidget, Textarea, TextInput
|
from django.forms.widgets import MultiWidget, Textarea, TextInput
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.html import conditional_escape, format_html, html_safe
|
from django.utils.html import format_html, html_safe
|
||||||
from django.utils.safestring import mark_safe
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
__all__ = ('BoundField',)
|
__all__ = ('BoundField',)
|
||||||
|
@ -75,7 +74,7 @@ class BoundField:
|
||||||
"""
|
"""
|
||||||
Return an ErrorList (empty if there are no errors) for this field.
|
Return an ErrorList (empty if there are no errors) for this field.
|
||||||
"""
|
"""
|
||||||
return self.form.errors.get(self.name, self.form.error_class())
|
return self.form.errors.get(self.name, self.form.error_class(renderer=self.form.renderer))
|
||||||
|
|
||||||
def as_widget(self, widget=None, attrs=None, only_initial=False):
|
def as_widget(self, widget=None, attrs=None, only_initial=False):
|
||||||
"""
|
"""
|
||||||
|
@ -177,11 +176,14 @@ class BoundField:
|
||||||
attrs['class'] += ' ' + self.form.required_css_class
|
attrs['class'] += ' ' + self.form.required_css_class
|
||||||
else:
|
else:
|
||||||
attrs['class'] = self.form.required_css_class
|
attrs['class'] = self.form.required_css_class
|
||||||
attrs = flatatt(attrs) if attrs else ''
|
context = {
|
||||||
contents = format_html('<label{}>{}</label>', attrs, contents)
|
'form': self.form,
|
||||||
else:
|
'field': self,
|
||||||
contents = conditional_escape(contents)
|
'label': contents,
|
||||||
return mark_safe(contents)
|
'attrs': attrs,
|
||||||
|
'use_tag': bool(id_),
|
||||||
|
}
|
||||||
|
return self.form.render(self.form.template_name_label, context)
|
||||||
|
|
||||||
def css_classes(self, extra_classes=None):
|
def css_classes(self, extra_classes=None):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -4,15 +4,17 @@ Form classes
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
import datetime
|
import datetime
|
||||||
|
import warnings
|
||||||
|
|
||||||
from django.core.exceptions import NON_FIELD_ERRORS, ValidationError
|
from django.core.exceptions import NON_FIELD_ERRORS, ValidationError
|
||||||
from django.forms.fields import Field, FileField
|
from django.forms.fields import Field, FileField
|
||||||
from django.forms.utils import ErrorDict, ErrorList
|
from django.forms.utils import ErrorDict, ErrorList, RenderableFormMixin
|
||||||
from django.forms.widgets import Media, MediaDefiningClass
|
from django.forms.widgets import Media, MediaDefiningClass
|
||||||
from django.utils.datastructures import MultiValueDict
|
from django.utils.datastructures import MultiValueDict
|
||||||
|
from django.utils.deprecation import RemovedInDjango50Warning
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.html import conditional_escape, html_safe
|
from django.utils.html import conditional_escape
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import SafeString, mark_safe
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from .renderers import get_default_renderer
|
from .renderers import get_default_renderer
|
||||||
|
@ -49,8 +51,7 @@ class DeclarativeFieldsMetaclass(MediaDefiningClass):
|
||||||
return new_class
|
return new_class
|
||||||
|
|
||||||
|
|
||||||
@html_safe
|
class BaseForm(RenderableFormMixin):
|
||||||
class BaseForm:
|
|
||||||
"""
|
"""
|
||||||
The main implementation of all the Form logic. Note that this class is
|
The main implementation of all the Form logic. Note that this class is
|
||||||
different than Form. See the comments by the Form class for more info. Any
|
different than Form. See the comments by the Form class for more info. Any
|
||||||
|
@ -62,6 +63,12 @@ class BaseForm:
|
||||||
prefix = None
|
prefix = None
|
||||||
use_required_attribute = True
|
use_required_attribute = True
|
||||||
|
|
||||||
|
template_name = 'django/forms/default.html'
|
||||||
|
template_name_p = 'django/forms/p.html'
|
||||||
|
template_name_table = 'django/forms/table.html'
|
||||||
|
template_name_ul = 'django/forms/ul.html'
|
||||||
|
template_name_label = 'django/forms/label.html'
|
||||||
|
|
||||||
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,
|
||||||
empty_permitted=False, field_order=None, use_required_attribute=None, renderer=None):
|
empty_permitted=False, field_order=None, use_required_attribute=None, renderer=None):
|
||||||
|
@ -129,9 +136,6 @@ class BaseForm:
|
||||||
fields.update(self.fields) # add remaining fields in original order
|
fields.update(self.fields) # add remaining fields in original order
|
||||||
self.fields = fields
|
self.fields = fields
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.as_table()
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
if self._errors is None:
|
if self._errors is None:
|
||||||
is_valid = "Unknown"
|
is_valid = "Unknown"
|
||||||
|
@ -206,6 +210,12 @@ class BaseForm:
|
||||||
|
|
||||||
def _html_output(self, normal_row, error_row, row_ender, help_text_html, errors_on_separate_row):
|
def _html_output(self, normal_row, error_row, row_ender, help_text_html, errors_on_separate_row):
|
||||||
"Output HTML. Used by as_table(), as_ul(), as_p()."
|
"Output HTML. Used by as_table(), as_ul(), as_p()."
|
||||||
|
warnings.warn(
|
||||||
|
'django.forms.BaseForm._html_output() is deprecated. '
|
||||||
|
'Please use .render() and .get_context() instead.',
|
||||||
|
RemovedInDjango50Warning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
# Errors that should be displayed above all fields.
|
# Errors that should be displayed above all fields.
|
||||||
top_errors = self.non_field_errors().copy()
|
top_errors = self.non_field_errors().copy()
|
||||||
output, hidden_fields = [], []
|
output, hidden_fields = [], []
|
||||||
|
@ -282,35 +292,37 @@ class BaseForm:
|
||||||
output.append(str_hidden)
|
output.append(str_hidden)
|
||||||
return mark_safe('\n'.join(output))
|
return mark_safe('\n'.join(output))
|
||||||
|
|
||||||
def as_table(self):
|
def get_context(self):
|
||||||
"Return this form rendered as HTML <tr>s -- excluding the <table></table>."
|
fields = []
|
||||||
return self._html_output(
|
hidden_fields = []
|
||||||
normal_row='<tr%(html_class_attr)s><th>%(label)s</th><td>%(errors)s%(field)s%(help_text)s</td></tr>',
|
top_errors = self.non_field_errors().copy()
|
||||||
error_row='<tr><td colspan="2">%s</td></tr>',
|
for name, bf in self._bound_items():
|
||||||
row_ender='</td></tr>',
|
bf_errors = self.error_class(bf.errors, renderer=self.renderer)
|
||||||
help_text_html='<br><span class="helptext">%s</span>',
|
if bf.is_hidden:
|
||||||
errors_on_separate_row=False,
|
if bf_errors:
|
||||||
)
|
top_errors += [
|
||||||
|
_('(Hidden field %(name)s) %(error)s') % {'name': name, 'error': str(e)}
|
||||||
def as_ul(self):
|
for e in bf_errors
|
||||||
"Return this form rendered as HTML <li>s -- excluding the <ul></ul>."
|
]
|
||||||
return self._html_output(
|
hidden_fields.append(bf)
|
||||||
normal_row='<li%(html_class_attr)s>%(errors)s%(label)s %(field)s%(help_text)s</li>',
|
else:
|
||||||
error_row='<li>%s</li>',
|
errors_str = str(bf_errors)
|
||||||
row_ender='</li>',
|
# RemovedInDjango50Warning.
|
||||||
help_text_html=' <span class="helptext">%s</span>',
|
if not isinstance(errors_str, SafeString):
|
||||||
errors_on_separate_row=False,
|
warnings.warn(
|
||||||
)
|
f'Returning a plain string from '
|
||||||
|
f'{self.error_class.__name__} is deprecated. Please '
|
||||||
def as_p(self):
|
f'customize via the template system instead.',
|
||||||
"Return this form rendered as HTML <p>s."
|
RemovedInDjango50Warning,
|
||||||
return self._html_output(
|
|
||||||
normal_row='<p%(html_class_attr)s>%(label)s %(field)s%(help_text)s</p>',
|
|
||||||
error_row='%s',
|
|
||||||
row_ender='</p>',
|
|
||||||
help_text_html=' <span class="helptext">%s</span>',
|
|
||||||
errors_on_separate_row=True,
|
|
||||||
)
|
)
|
||||||
|
errors_str = mark_safe(errors_str)
|
||||||
|
fields.append((bf, errors_str))
|
||||||
|
return {
|
||||||
|
'form': self,
|
||||||
|
'fields': fields,
|
||||||
|
'hidden_fields': hidden_fields,
|
||||||
|
'errors': top_errors,
|
||||||
|
}
|
||||||
|
|
||||||
def non_field_errors(self):
|
def non_field_errors(self):
|
||||||
"""
|
"""
|
||||||
|
@ -318,7 +330,10 @@ class BaseForm:
|
||||||
field -- i.e., from Form.clean(). Return an empty ErrorList if there
|
field -- i.e., from Form.clean(). Return an empty ErrorList if there
|
||||||
are none.
|
are none.
|
||||||
"""
|
"""
|
||||||
return self.errors.get(NON_FIELD_ERRORS, self.error_class(error_class='nonfield'))
|
return self.errors.get(
|
||||||
|
NON_FIELD_ERRORS,
|
||||||
|
self.error_class(error_class='nonfield', renderer=self.renderer),
|
||||||
|
)
|
||||||
|
|
||||||
def add_error(self, field, error):
|
def add_error(self, field, error):
|
||||||
"""
|
"""
|
||||||
|
@ -360,9 +375,9 @@ class BaseForm:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"'%s' has no field named '%s'." % (self.__class__.__name__, field))
|
"'%s' has no field named '%s'." % (self.__class__.__name__, field))
|
||||||
if field == NON_FIELD_ERRORS:
|
if field == NON_FIELD_ERRORS:
|
||||||
self._errors[field] = self.error_class(error_class='nonfield')
|
self._errors[field] = self.error_class(error_class='nonfield', renderer=self.renderer)
|
||||||
else:
|
else:
|
||||||
self._errors[field] = self.error_class()
|
self._errors[field] = self.error_class(renderer=self.renderer)
|
||||||
self._errors[field].extend(error_list)
|
self._errors[field].extend(error_list)
|
||||||
if field in self.cleaned_data:
|
if field in self.cleaned_data:
|
||||||
del self.cleaned_data[field]
|
del self.cleaned_data[field]
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.forms import Form
|
from django.forms import Form
|
||||||
from django.forms.fields import BooleanField, IntegerField
|
from django.forms.fields import BooleanField, IntegerField
|
||||||
from django.forms.utils import ErrorList
|
from django.forms.renderers import get_default_renderer
|
||||||
|
from django.forms.utils import ErrorList, RenderableFormMixin
|
||||||
from django.forms.widgets import CheckboxInput, HiddenInput, NumberInput
|
from django.forms.widgets import CheckboxInput, HiddenInput, NumberInput
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.html import html_safe
|
|
||||||
from django.utils.safestring import mark_safe
|
|
||||||
from django.utils.translation import gettext_lazy as _, ngettext
|
from django.utils.translation import gettext_lazy as _, ngettext
|
||||||
|
|
||||||
__all__ = ('BaseFormSet', 'formset_factory', 'all_valid')
|
__all__ = ('BaseFormSet', 'formset_factory', 'all_valid')
|
||||||
|
@ -50,8 +49,7 @@ class ManagementForm(Form):
|
||||||
return cleaned_data
|
return cleaned_data
|
||||||
|
|
||||||
|
|
||||||
@html_safe
|
class BaseFormSet(RenderableFormMixin):
|
||||||
class BaseFormSet:
|
|
||||||
"""
|
"""
|
||||||
A collection of instances of the same Form class.
|
A collection of instances of the same Form class.
|
||||||
"""
|
"""
|
||||||
|
@ -63,6 +61,10 @@ class BaseFormSet:
|
||||||
'%(field_names)s. You may need to file a bug report if the issue persists.'
|
'%(field_names)s. You may need to file a bug report if the issue persists.'
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
template_name = 'django/forms/formsets/default.html'
|
||||||
|
template_name_p = 'django/forms/formsets/p.html'
|
||||||
|
template_name_table = 'django/forms/formsets/table.html'
|
||||||
|
template_name_ul = 'django/forms/formsets/ul.html'
|
||||||
|
|
||||||
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, form_kwargs=None,
|
initial=None, error_class=ErrorList, form_kwargs=None,
|
||||||
|
@ -85,9 +87,6 @@ class BaseFormSet:
|
||||||
messages.update(error_messages)
|
messages.update(error_messages)
|
||||||
self.error_messages = messages
|
self.error_messages = messages
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.as_table()
|
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
"""Yield the forms in the order they should be rendered."""
|
"""Yield the forms in the order they should be rendered."""
|
||||||
return iter(self.forms)
|
return iter(self.forms)
|
||||||
|
@ -110,15 +109,20 @@ class BaseFormSet:
|
||||||
def management_form(self):
|
def management_form(self):
|
||||||
"""Return the ManagementForm instance for this FormSet."""
|
"""Return the ManagementForm instance for this FormSet."""
|
||||||
if self.is_bound:
|
if self.is_bound:
|
||||||
form = ManagementForm(self.data, auto_id=self.auto_id, prefix=self.prefix)
|
form = ManagementForm(self.data, auto_id=self.auto_id, prefix=self.prefix, renderer=self.renderer)
|
||||||
form.full_clean()
|
form.full_clean()
|
||||||
else:
|
else:
|
||||||
form = ManagementForm(auto_id=self.auto_id, prefix=self.prefix, initial={
|
form = ManagementForm(
|
||||||
|
auto_id=self.auto_id,
|
||||||
|
prefix=self.prefix,
|
||||||
|
initial={
|
||||||
TOTAL_FORM_COUNT: self.total_form_count(),
|
TOTAL_FORM_COUNT: self.total_form_count(),
|
||||||
INITIAL_FORM_COUNT: self.initial_form_count(),
|
INITIAL_FORM_COUNT: self.initial_form_count(),
|
||||||
MIN_NUM_FORM_COUNT: self.min_num,
|
MIN_NUM_FORM_COUNT: self.min_num,
|
||||||
MAX_NUM_FORM_COUNT: self.max_num
|
MAX_NUM_FORM_COUNT: self.max_num,
|
||||||
})
|
},
|
||||||
|
renderer=self.renderer,
|
||||||
|
)
|
||||||
return form
|
return form
|
||||||
|
|
||||||
def total_form_count(self):
|
def total_form_count(self):
|
||||||
|
@ -177,6 +181,7 @@ class BaseFormSet:
|
||||||
# incorrect validation for extra, optional, and deleted
|
# incorrect validation for extra, optional, and deleted
|
||||||
# forms in the formset.
|
# forms in the formset.
|
||||||
'use_required_attribute': False,
|
'use_required_attribute': False,
|
||||||
|
'renderer': self.renderer,
|
||||||
}
|
}
|
||||||
if self.is_bound:
|
if self.is_bound:
|
||||||
defaults['data'] = self.data
|
defaults['data'] = self.data
|
||||||
|
@ -212,7 +217,8 @@ class BaseFormSet:
|
||||||
prefix=self.add_prefix('__prefix__'),
|
prefix=self.add_prefix('__prefix__'),
|
||||||
empty_permitted=True,
|
empty_permitted=True,
|
||||||
use_required_attribute=False,
|
use_required_attribute=False,
|
||||||
**self.get_form_kwargs(None)
|
**self.get_form_kwargs(None),
|
||||||
|
renderer=self.renderer,
|
||||||
)
|
)
|
||||||
self.add_fields(form, None)
|
self.add_fields(form, None)
|
||||||
return form
|
return form
|
||||||
|
@ -338,7 +344,7 @@ class BaseFormSet:
|
||||||
self._non_form_errors.
|
self._non_form_errors.
|
||||||
"""
|
"""
|
||||||
self._errors = []
|
self._errors = []
|
||||||
self._non_form_errors = self.error_class(error_class='nonform')
|
self._non_form_errors = self.error_class(error_class='nonform', renderer=self.renderer)
|
||||||
empty_forms_count = 0
|
empty_forms_count = 0
|
||||||
|
|
||||||
if not self.is_bound: # Stop further processing.
|
if not self.is_bound: # Stop further processing.
|
||||||
|
@ -387,7 +393,8 @@ class BaseFormSet:
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
self._non_form_errors = self.error_class(
|
self._non_form_errors = self.error_class(
|
||||||
e.error_list,
|
e.error_list,
|
||||||
error_class='nonform'
|
error_class='nonform',
|
||||||
|
renderer=self.renderer,
|
||||||
)
|
)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
@ -450,29 +457,14 @@ class BaseFormSet:
|
||||||
else:
|
else:
|
||||||
return self.empty_form.media
|
return self.empty_form.media
|
||||||
|
|
||||||
def as_table(self):
|
def get_context(self):
|
||||||
"Return this formset rendered as HTML <tr>s -- excluding the <table></table>."
|
return {'formset': self}
|
||||||
# XXX: there is no semantic division between forms here, there
|
|
||||||
# probably should be. It might make sense to render each form as a
|
|
||||||
# table row with each field as a td.
|
|
||||||
forms = ' '.join(form.as_table() for form in self)
|
|
||||||
return mark_safe(str(self.management_form) + '\n' + forms)
|
|
||||||
|
|
||||||
def as_p(self):
|
|
||||||
"Return this formset rendered as HTML <p>s."
|
|
||||||
forms = ' '.join(form.as_p() for form in self)
|
|
||||||
return mark_safe(str(self.management_form) + '\n' + forms)
|
|
||||||
|
|
||||||
def as_ul(self):
|
|
||||||
"Return this formset rendered as HTML <li>s."
|
|
||||||
forms = ' '.join(form.as_ul() for form in self)
|
|
||||||
return mark_safe(str(self.management_form) + '\n' + forms)
|
|
||||||
|
|
||||||
|
|
||||||
def formset_factory(form, formset=BaseFormSet, extra=1, can_order=False,
|
def formset_factory(form, formset=BaseFormSet, extra=1, can_order=False,
|
||||||
can_delete=False, max_num=None, validate_max=False,
|
can_delete=False, max_num=None, validate_max=False,
|
||||||
min_num=None, validate_min=False, absolute_max=None,
|
min_num=None, validate_min=False, absolute_max=None,
|
||||||
can_delete_extra=True):
|
can_delete_extra=True, renderer=None):
|
||||||
"""Return a FormSet for the given form class."""
|
"""Return a FormSet for the given form class."""
|
||||||
if min_num is None:
|
if min_num is None:
|
||||||
min_num = DEFAULT_MIN_NUM
|
min_num = DEFAULT_MIN_NUM
|
||||||
|
@ -498,6 +490,7 @@ def formset_factory(form, formset=BaseFormSet, extra=1, can_order=False,
|
||||||
'absolute_max': absolute_max,
|
'absolute_max': absolute_max,
|
||||||
'validate_min': validate_min,
|
'validate_min': validate_min,
|
||||||
'validate_max': validate_max,
|
'validate_max': validate_max,
|
||||||
|
'renderer': renderer or get_default_renderer(),
|
||||||
}
|
}
|
||||||
return type(form.__name__ + 'FormSet', (formset,), attrs)
|
return type(form.__name__ + 'FormSet', (formset,), attrs)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
{% for name, value in attrs.items() %}{% if value is not sameas False %} {{ name }}{% if value is not sameas True %}="{{ value }}"{% endif %}{% endif %}{% endfor %}
|
|
@ -0,0 +1 @@
|
||||||
|
{% include "django/forms/table.html" %}
|
|
@ -0,0 +1 @@
|
||||||
|
{% include "django/forms/errors/dict/ul.html" %}
|
|
@ -0,0 +1,3 @@
|
||||||
|
{% for field, errors in errors %}* {{ field }}
|
||||||
|
{% for error in errors %} * {{ error }}
|
||||||
|
{% endfor %}{% endfor %}
|
|
@ -0,0 +1 @@
|
||||||
|
{% if errors %}<ul class="{{ error_class }}">{% for field, error in errors %}<li>{{ field }}{{ error }}</li>{% endfor %}</ul>{% endif %}
|
|
@ -0,0 +1 @@
|
||||||
|
{% include "django/forms/errors/list/ul.html" %}
|
|
@ -0,0 +1,2 @@
|
||||||
|
{% for error in errors %}* {{ error }}
|
||||||
|
{% endfor %}
|
|
@ -0,0 +1 @@
|
||||||
|
{% if errors %}<ul class="{{ error_class }}">{% for error in errors %}<li>{{ error }}</li>{% endfor %}</ul>{% endif %}
|
|
@ -0,0 +1 @@
|
||||||
|
{{ formset.management_form }}{% for form in formset %}{{ form }}{% endfor %}
|
|
@ -0,0 +1 @@
|
||||||
|
{{ formset.management_form }}{% for form in formset %}{{ form.as_p() }}{% endfor %}
|
|
@ -0,0 +1 @@
|
||||||
|
{{ formset.management_form }}{% for form in formset %}{{ form.as_table() }}{% endfor %}
|
|
@ -0,0 +1 @@
|
||||||
|
{{ formset.management_form }}{% for form in formset %}{{ form.as_ul() }}{% endfor %}
|
|
@ -0,0 +1 @@
|
||||||
|
{% if use_tag %}<label{% if attrs %}{% include 'django/forms/attrs.html' %}{% endif %}>{{ label }}</label>{% else %}{{ label }}{% endif %}
|
|
@ -0,0 +1,20 @@
|
||||||
|
{{ errors }}
|
||||||
|
{% if errors and not fields %}
|
||||||
|
<p>{% for field in hidden_fields %}{{ field }}{% endfor %}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% for field, errors in fields %}
|
||||||
|
{{ errors }}
|
||||||
|
<p{% set classes = field.css_classes() %}{% if classes %} class="{{ classes }}"{% endif %}>
|
||||||
|
{% if field.label %}{{ field.label_tag() }}{% endif %}
|
||||||
|
{{ field }}
|
||||||
|
{% if field.help_text %}
|
||||||
|
<span class="helptext">{{ field.help_text }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if loop.last %}
|
||||||
|
{% for field in hidden_fields %}{{ field }}{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
{% endfor %}
|
||||||
|
{% if not fields and not errors %}
|
||||||
|
{% for field in hidden_fields %}{{ field }}{% endfor %}
|
||||||
|
{% endif %}
|
|
@ -0,0 +1,29 @@
|
||||||
|
{% if errors %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="2">
|
||||||
|
{{ errors }}
|
||||||
|
{% if not fields %}
|
||||||
|
{% for field in hidden_fields %}{{ field }}{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
{% for field, errors in fields %}
|
||||||
|
<tr{% set classes = field.css_classes() %}{% if classes %} class="{{ classes }}"{% endif %}>
|
||||||
|
<th>{% if field.label %}{{ field.label_tag() }}{% endif %}</th>
|
||||||
|
<td>
|
||||||
|
{{ errors }}
|
||||||
|
{{ field }}
|
||||||
|
{% if field.help_text %}
|
||||||
|
<br>
|
||||||
|
<span class="helptext">{{ field.help_text }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if loop.last %}
|
||||||
|
{% for field in hidden_fields %}{{ field }}{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{% if not fields and not errors %}
|
||||||
|
{% for field in hidden_fields %}{{ field }}{% endfor %}
|
||||||
|
{% endif %}
|
|
@ -0,0 +1,24 @@
|
||||||
|
{% if errors %}
|
||||||
|
<li>
|
||||||
|
{{ errors }}
|
||||||
|
{% if not fields %}
|
||||||
|
{% for field in hidden_fields %}{{ field }}{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% for field, errors in fields %}
|
||||||
|
<li{% set classes = field.css_classes() %}{% if classes %} class="{{ classes }}"{% endif %}>
|
||||||
|
{{ errors }}
|
||||||
|
{% if field.label %}{{ field.label_tag() }}{% endif %}
|
||||||
|
{{ field }}
|
||||||
|
{% if field.help_text %}
|
||||||
|
<span class="helptext">{{ field.help_text }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if loop.last %}
|
||||||
|
{% for field in hidden_fields %}{{ field }}{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
{% if not fields and not errors %}
|
||||||
|
{% for field in hidden_fields %}{{ field }}{% endfor %}
|
||||||
|
{% endif %}
|
|
@ -718,7 +718,10 @@ class BaseModelFormSet(BaseFormSet):
|
||||||
# poke error messages into the right places and mark
|
# poke error messages into the right places and mark
|
||||||
# the form as invalid
|
# the form as invalid
|
||||||
errors.append(self.get_unique_error_message(unique_check))
|
errors.append(self.get_unique_error_message(unique_check))
|
||||||
form._errors[NON_FIELD_ERRORS] = self.error_class([self.get_form_error()])
|
form._errors[NON_FIELD_ERRORS] = self.error_class(
|
||||||
|
[self.get_form_error()],
|
||||||
|
renderer=self.renderer,
|
||||||
|
)
|
||||||
# remove the data from the cleaned_data dict since it was invalid
|
# remove the data from the cleaned_data dict since it was invalid
|
||||||
for field in unique_check:
|
for field in unique_check:
|
||||||
if field in form.cleaned_data:
|
if field in form.cleaned_data:
|
||||||
|
@ -747,7 +750,10 @@ class BaseModelFormSet(BaseFormSet):
|
||||||
# poke error messages into the right places and mark
|
# poke error messages into the right places and mark
|
||||||
# the form as invalid
|
# the form as invalid
|
||||||
errors.append(self.get_date_error_message(date_check))
|
errors.append(self.get_date_error_message(date_check))
|
||||||
form._errors[NON_FIELD_ERRORS] = self.error_class([self.get_form_error()])
|
form._errors[NON_FIELD_ERRORS] = self.error_class(
|
||||||
|
[self.get_form_error()],
|
||||||
|
renderer=self.renderer,
|
||||||
|
)
|
||||||
# remove the data from the cleaned_data dict since it was invalid
|
# remove the data from the cleaned_data dict since it was invalid
|
||||||
del form.cleaned_data[field]
|
del form.cleaned_data[field]
|
||||||
# mark the data as seen
|
# mark the data as seen
|
||||||
|
@ -869,7 +875,7 @@ def modelformset_factory(model, form=ModelForm, formfield_callback=None,
|
||||||
widgets=None, validate_max=False, localized_fields=None,
|
widgets=None, validate_max=False, localized_fields=None,
|
||||||
labels=None, help_texts=None, error_messages=None,
|
labels=None, help_texts=None, error_messages=None,
|
||||||
min_num=None, validate_min=False, field_classes=None,
|
min_num=None, validate_min=False, field_classes=None,
|
||||||
absolute_max=None, can_delete_extra=True):
|
absolute_max=None, can_delete_extra=True, renderer=None):
|
||||||
"""Return a FormSet class for the given Django model class."""
|
"""Return a FormSet class for the given Django model class."""
|
||||||
meta = getattr(form, 'Meta', None)
|
meta = getattr(form, 'Meta', None)
|
||||||
if (getattr(meta, 'fields', fields) is None and
|
if (getattr(meta, 'fields', fields) is None and
|
||||||
|
@ -887,7 +893,8 @@ def modelformset_factory(model, form=ModelForm, formfield_callback=None,
|
||||||
FormSet = formset_factory(form, formset, extra=extra, min_num=min_num, max_num=max_num,
|
FormSet = formset_factory(form, formset, extra=extra, min_num=min_num, max_num=max_num,
|
||||||
can_order=can_order, can_delete=can_delete,
|
can_order=can_order, can_delete=can_delete,
|
||||||
validate_min=validate_min, validate_max=validate_max,
|
validate_min=validate_min, validate_max=validate_max,
|
||||||
absolute_max=absolute_max, can_delete_extra=can_delete_extra)
|
absolute_max=absolute_max, can_delete_extra=can_delete_extra,
|
||||||
|
renderer=renderer)
|
||||||
FormSet.model = model
|
FormSet.model = model
|
||||||
return FormSet
|
return FormSet
|
||||||
|
|
||||||
|
@ -1069,7 +1076,7 @@ def inlineformset_factory(parent_model, model, form=ModelForm,
|
||||||
widgets=None, validate_max=False, localized_fields=None,
|
widgets=None, validate_max=False, localized_fields=None,
|
||||||
labels=None, help_texts=None, error_messages=None,
|
labels=None, help_texts=None, error_messages=None,
|
||||||
min_num=None, validate_min=False, field_classes=None,
|
min_num=None, validate_min=False, field_classes=None,
|
||||||
absolute_max=None, can_delete_extra=True):
|
absolute_max=None, can_delete_extra=True, renderer=None):
|
||||||
"""
|
"""
|
||||||
Return an ``InlineFormSet`` for the given kwargs.
|
Return an ``InlineFormSet`` for the given kwargs.
|
||||||
|
|
||||||
|
@ -1101,6 +1108,7 @@ def inlineformset_factory(parent_model, model, form=ModelForm,
|
||||||
'field_classes': field_classes,
|
'field_classes': field_classes,
|
||||||
'absolute_max': absolute_max,
|
'absolute_max': absolute_max,
|
||||||
'can_delete_extra': can_delete_extra,
|
'can_delete_extra': can_delete_extra,
|
||||||
|
'renderer': renderer,
|
||||||
}
|
}
|
||||||
FormSet = modelformset_factory(model, **kwargs)
|
FormSet = modelformset_factory(model, **kwargs)
|
||||||
FormSet.fk = fk
|
FormSet.fk = fk
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
{% for name, value in attrs.items %}{% if value is not False %} {{ name }}{% if value is not True %}="{{ value|stringformat:'s' }}"{% endif %}{% endif %}{% endfor %}
|
|
@ -0,0 +1 @@
|
||||||
|
{% include "django/forms/table.html" %}
|
|
@ -0,0 +1 @@
|
||||||
|
{% include "django/forms/errors/dict/ul.html" %}
|
|
@ -0,0 +1,3 @@
|
||||||
|
{% for field, errors in errors %}* {{ field }}
|
||||||
|
{% for error in errors %} * {{ error }}
|
||||||
|
{% endfor %}{% endfor %}
|
|
@ -0,0 +1 @@
|
||||||
|
{% if errors %}<ul class="{{ error_class }}">{% for field, error in errors %}<li>{{ field }}{{ error }}</li>{% endfor %}</ul>{% endif %}
|
|
@ -0,0 +1 @@
|
||||||
|
{% include "django/forms/errors/list/ul.html" %}
|
|
@ -0,0 +1,2 @@
|
||||||
|
{% for error in errors %}* {{ error }}
|
||||||
|
{% endfor %}
|
|
@ -0,0 +1 @@
|
||||||
|
{% if errors %}<ul class="{{ error_class }}">{% for error in errors %}<li>{{ error }}</li>{% endfor %}</ul>{% endif %}
|
|
@ -0,0 +1 @@
|
||||||
|
{{ formset.management_form }}{% for form in formset %}{{ form }}{% endfor %}
|
|
@ -0,0 +1 @@
|
||||||
|
{{ formset.management_form }}{% for form in formset %}{{ form.as_p }}{% endfor %}
|
|
@ -0,0 +1 @@
|
||||||
|
{{ formset.management_form }}{% for form in formset %}{{ form.as_table }}{% endfor %}
|
|
@ -0,0 +1 @@
|
||||||
|
{{ formset.management_form }}{% for form in formset %}{{ form.as_ul }}{% endfor %}
|
|
@ -0,0 +1 @@
|
||||||
|
{% if use_tag %}<label{% include 'django/forms/attrs.html' %}>{{ label }}</label>{% else %}{{ label }}{% endif %}
|
|
@ -0,0 +1,20 @@
|
||||||
|
{{ errors }}
|
||||||
|
{% if errors and not fields %}
|
||||||
|
<p>{% for field in hidden_fields %}{{ field }}{% endfor %}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% for field, errors in fields %}
|
||||||
|
{{ errors }}
|
||||||
|
<p{% with classes=field.css_classes %}{% if classes %} class="{{ classes }}"{% endif %}{% endwith %}>
|
||||||
|
{% if field.label %}{{ field.label_tag }}{% endif %}
|
||||||
|
{{ field }}
|
||||||
|
{% if field.help_text %}
|
||||||
|
<span class="helptext">{{ field.help_text }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if forloop.last %}
|
||||||
|
{% for field in hidden_fields %}{{ field }}{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
{% endfor %}
|
||||||
|
{% if not fields and not errors %}
|
||||||
|
{% for field in hidden_fields %}{{ field }}{% endfor %}
|
||||||
|
{% endif %}
|
|
@ -0,0 +1,29 @@
|
||||||
|
{% if errors %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="2">
|
||||||
|
{{ errors }}
|
||||||
|
{% if not fields %}
|
||||||
|
{% for field in hidden_fields %}{{ field }}{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
{% for field, errors in fields %}
|
||||||
|
<tr{% with classes=field.css_classes %}{% if classes %} class="{{ classes }}"{% endif %}{% endwith %}>
|
||||||
|
<th>{% if field.label %}{{ field.label_tag }}{% endif %}</th>
|
||||||
|
<td>
|
||||||
|
{{ errors }}
|
||||||
|
{{ field }}
|
||||||
|
{% if field.help_text %}
|
||||||
|
<br>
|
||||||
|
<span class="helptext">{{ field.help_text }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if forloop.last %}
|
||||||
|
{% for field in hidden_fields %}{{ field }}{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{% if not fields and not errors %}
|
||||||
|
{% for field in hidden_fields %}{{ field }}{% endfor %}
|
||||||
|
{% endif %}
|
|
@ -0,0 +1,24 @@
|
||||||
|
{% if errors %}
|
||||||
|
<li>
|
||||||
|
{{ errors }}
|
||||||
|
{% if not fields %}
|
||||||
|
{% for field in hidden_fields %}{{ field }}{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% for field, errors in fields %}
|
||||||
|
<li{% with classes=field.css_classes %}{% if classes %} class="{{ classes }}"{% endif %}{% endwith %}>
|
||||||
|
{{ errors }}
|
||||||
|
{% if field.label %}{{ field.label_tag }}{% endif %}
|
||||||
|
{{ field }}
|
||||||
|
{% if field.help_text %}
|
||||||
|
<span class="helptext">{{ field.help_text }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if forloop.last %}
|
||||||
|
{% for field in hidden_fields %}{{ field }}{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
{% if not fields and not errors %}
|
||||||
|
{% for field in hidden_fields %}{{ field }}{% endfor %}
|
||||||
|
{% endif %}
|
|
@ -1,10 +1,12 @@
|
||||||
import json
|
import json
|
||||||
from collections import UserList
|
from collections import UserDict, UserList
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.forms.renderers import get_default_renderer
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.html import escape, format_html, format_html_join, html_safe
|
from django.utils.html import escape, format_html_join
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
@ -41,53 +43,90 @@ def flatatt(attrs):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@html_safe
|
class RenderableMixin:
|
||||||
class ErrorDict(dict):
|
def get_context(self):
|
||||||
|
raise NotImplementedError(
|
||||||
|
'Subclasses of RenderableMixin must provide a get_context() method.'
|
||||||
|
)
|
||||||
|
|
||||||
|
def render(self, template_name=None, context=None, renderer=None):
|
||||||
|
return mark_safe((renderer or self.renderer).render(
|
||||||
|
template_name or self.template_name,
|
||||||
|
context or self.get_context(),
|
||||||
|
))
|
||||||
|
|
||||||
|
__str__ = render
|
||||||
|
__html__ = render
|
||||||
|
|
||||||
|
|
||||||
|
class RenderableFormMixin(RenderableMixin):
|
||||||
|
def as_p(self):
|
||||||
|
"""Render as <p> elements."""
|
||||||
|
return self.render(self.template_name_p)
|
||||||
|
|
||||||
|
def as_table(self):
|
||||||
|
"""Render as <tr> elements excluding the surrounding <table> tag."""
|
||||||
|
return self.render(self.template_name_table)
|
||||||
|
|
||||||
|
def as_ul(self):
|
||||||
|
"""Render as <li> elements excluding the surrounding <ul> tag."""
|
||||||
|
return self.render(self.template_name_ul)
|
||||||
|
|
||||||
|
|
||||||
|
class RenderableErrorMixin(RenderableMixin):
|
||||||
|
def as_json(self, escape_html=False):
|
||||||
|
return json.dumps(self.get_json_data(escape_html))
|
||||||
|
|
||||||
|
def as_text(self):
|
||||||
|
return self.render(self.template_name_text)
|
||||||
|
|
||||||
|
def as_ul(self):
|
||||||
|
return self.render(self.template_name_ul)
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorDict(UserDict, RenderableErrorMixin):
|
||||||
"""
|
"""
|
||||||
A collection of errors that knows how to display itself in various formats.
|
A collection of errors that knows how to display itself in various formats.
|
||||||
|
|
||||||
The dictionary keys are the field names, and the values are the errors.
|
The dictionary keys are the field names, and the values are the errors.
|
||||||
"""
|
"""
|
||||||
|
template_name = 'django/forms/errors/dict/default.html'
|
||||||
|
template_name_text = 'django/forms/errors/dict/text.txt'
|
||||||
|
template_name_ul = 'django/forms/errors/dict/ul.html'
|
||||||
|
|
||||||
|
def __init__(self, data=None, renderer=None):
|
||||||
|
super().__init__(data)
|
||||||
|
self.renderer = renderer or get_default_renderer()
|
||||||
|
|
||||||
def as_data(self):
|
def as_data(self):
|
||||||
return {f: e.as_data() for f, e in self.items()}
|
return {f: e.as_data() for f, e in self.items()}
|
||||||
|
|
||||||
def get_json_data(self, escape_html=False):
|
def get_json_data(self, escape_html=False):
|
||||||
return {f: e.get_json_data(escape_html) for f, e in self.items()}
|
return {f: e.get_json_data(escape_html) for f, e in self.items()}
|
||||||
|
|
||||||
def as_json(self, escape_html=False):
|
def get_context(self):
|
||||||
return json.dumps(self.get_json_data(escape_html))
|
return {
|
||||||
|
'errors': self.items(),
|
||||||
def as_ul(self):
|
'error_class': 'errorlist',
|
||||||
if not self:
|
}
|
||||||
return ''
|
|
||||||
return format_html(
|
|
||||||
'<ul class="errorlist">{}</ul>',
|
|
||||||
format_html_join('', '<li>{}{}</li>', self.items())
|
|
||||||
)
|
|
||||||
|
|
||||||
def as_text(self):
|
|
||||||
output = []
|
|
||||||
for field, errors in self.items():
|
|
||||||
output.append('* %s' % field)
|
|
||||||
output.append('\n'.join(' * %s' % e for e in errors))
|
|
||||||
return '\n'.join(output)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.as_ul()
|
|
||||||
|
|
||||||
|
|
||||||
@html_safe
|
class ErrorList(UserList, list, RenderableErrorMixin):
|
||||||
class ErrorList(UserList, list):
|
|
||||||
"""
|
"""
|
||||||
A collection of errors that knows how to display itself in various formats.
|
A collection of errors that knows how to display itself in various formats.
|
||||||
"""
|
"""
|
||||||
def __init__(self, initlist=None, error_class=None):
|
template_name = 'django/forms/errors/list/default.html'
|
||||||
|
template_name_text = 'django/forms/errors/list/text.txt'
|
||||||
|
template_name_ul = 'django/forms/errors/list/ul.html'
|
||||||
|
|
||||||
|
def __init__(self, initlist=None, error_class=None, renderer=None):
|
||||||
super().__init__(initlist)
|
super().__init__(initlist)
|
||||||
|
|
||||||
if error_class is None:
|
if error_class is None:
|
||||||
self.error_class = 'errorlist'
|
self.error_class = 'errorlist'
|
||||||
else:
|
else:
|
||||||
self.error_class = 'errorlist {}'.format(error_class)
|
self.error_class = 'errorlist {}'.format(error_class)
|
||||||
|
self.renderer = renderer or get_default_renderer()
|
||||||
|
|
||||||
def as_data(self):
|
def as_data(self):
|
||||||
return ValidationError(self.data).error_list
|
return ValidationError(self.data).error_list
|
||||||
|
@ -107,24 +146,11 @@ class ErrorList(UserList, list):
|
||||||
})
|
})
|
||||||
return errors
|
return errors
|
||||||
|
|
||||||
def as_json(self, escape_html=False):
|
def get_context(self):
|
||||||
return json.dumps(self.get_json_data(escape_html))
|
return {
|
||||||
|
'errors': self,
|
||||||
def as_ul(self):
|
'error_class': self.error_class,
|
||||||
if not self.data:
|
}
|
||||||
return ''
|
|
||||||
|
|
||||||
return format_html(
|
|
||||||
'<ul class="{}">{}</ul>',
|
|
||||||
self.error_class,
|
|
||||||
format_html_join('', '<li>{}</li>', ((e,) for e in self))
|
|
||||||
)
|
|
||||||
|
|
||||||
def as_text(self):
|
|
||||||
return '\n'.join('* %s' % e for e in self)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.as_ul()
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return repr(list(self))
|
return repr(list(self))
|
||||||
|
|
|
@ -57,6 +57,11 @@ details on these changes.
|
||||||
* The ``django.contrib.gis.admin.GeoModelAdmin`` and ``OSMGeoAdmin`` classes
|
* The ``django.contrib.gis.admin.GeoModelAdmin`` and ``OSMGeoAdmin`` classes
|
||||||
will be removed.
|
will be removed.
|
||||||
|
|
||||||
|
* The undocumented ``BaseForm._html_output()`` method will be removed.
|
||||||
|
|
||||||
|
* The ability to return a ``str``, rather than a ``SafeString``, when rendering
|
||||||
|
an ``ErrorDict`` and ``ErrorList`` will be removed.
|
||||||
|
|
||||||
.. _deprecation-removed-in-4.1:
|
.. _deprecation-removed-in-4.1:
|
||||||
|
|
||||||
4.1
|
4.1
|
||||||
|
|
|
@ -520,13 +520,41 @@ Although ``<table>`` output is the default output style when you ``print`` a
|
||||||
form, other output styles are available. Each style is available as a method on
|
form, other output styles are available. Each style is available as a method on
|
||||||
a form object, and each rendering method returns a string.
|
a form object, and each rendering method returns a string.
|
||||||
|
|
||||||
|
``template_name``
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
.. versionadded:: 4.0
|
||||||
|
|
||||||
|
.. attribute:: Form.template_name
|
||||||
|
|
||||||
|
The name of a template that is going to be rendered if the form is cast into a
|
||||||
|
string, e.g. via ``print(form)`` or in a template via ``{{ form }}``. By
|
||||||
|
default this template is ``'django/forms/default.html'``, which is a proxy for
|
||||||
|
``'django/forms/table.html'``. The template can be changed per form by
|
||||||
|
overriding the ``template_name`` attribute or more generally by overriding the
|
||||||
|
default template, see also :ref:`overriding-built-in-form-templates`.
|
||||||
|
|
||||||
|
``template_name_label``
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
.. versionadded:: 4.0
|
||||||
|
|
||||||
|
.. attribute:: Form.template_name_label
|
||||||
|
|
||||||
|
The template used to render a field's ``<label>``, used when calling
|
||||||
|
:meth:`BoundField.label_tag`. Can be changed per form by overriding this
|
||||||
|
attribute or more generally by overriding the default template, see also
|
||||||
|
:ref:`overriding-built-in-form-templates`.
|
||||||
|
|
||||||
``as_p()``
|
``as_p()``
|
||||||
----------
|
----------
|
||||||
|
|
||||||
.. method:: Form.as_p()
|
.. method:: Form.as_p()
|
||||||
|
|
||||||
``as_p()`` renders the form as a series of ``<p>`` tags, with each ``<p>``
|
``as_p()`` renders the form using the template assigned to the forms
|
||||||
containing one field::
|
``template_name_p`` attribute, by default this template is
|
||||||
|
``'django/forms/p.html'``. This template renders the form as a series of
|
||||||
|
``<p>`` tags, with each ``<p>`` containing one field::
|
||||||
|
|
||||||
>>> f = ContactForm()
|
>>> f = ContactForm()
|
||||||
>>> f.as_p()
|
>>> f.as_p()
|
||||||
|
@ -542,10 +570,12 @@ containing one field::
|
||||||
|
|
||||||
.. method:: Form.as_ul()
|
.. method:: Form.as_ul()
|
||||||
|
|
||||||
``as_ul()`` renders the form as a series of ``<li>`` tags, with each
|
``as_ul()`` renders the form using the template assigned to the forms
|
||||||
``<li>`` containing one field. It does *not* include the ``<ul>`` or
|
``template_name_ul`` attribute, by default this template is
|
||||||
``</ul>``, so that you can specify any HTML attributes on the ``<ul>`` for
|
``'django/forms/ul.html'``. This template renders the form as a series of
|
||||||
flexibility::
|
``<li>`` tags, with each ``<li>`` containing one field. It does *not* include
|
||||||
|
the ``<ul>`` or ``</ul>``, so that you can specify any HTML attributes on the
|
||||||
|
``<ul>`` for flexibility::
|
||||||
|
|
||||||
>>> f = ContactForm()
|
>>> f = ContactForm()
|
||||||
>>> f.as_ul()
|
>>> f.as_ul()
|
||||||
|
@ -561,9 +591,10 @@ flexibility::
|
||||||
|
|
||||||
.. method:: Form.as_table()
|
.. method:: Form.as_table()
|
||||||
|
|
||||||
Finally, ``as_table()`` outputs the form as an HTML ``<table>``. This is
|
Finally, ``as_table()`` renders the form using the template assigned to the
|
||||||
exactly the same as ``print``. In fact, when you ``print`` a form object,
|
forms ``template_name_table`` attribute, by default this template is
|
||||||
it calls its ``as_table()`` method behind the scenes::
|
``'django/forms/table.html'``. This template outputs the form as an HTML
|
||||||
|
``<table>``::
|
||||||
|
|
||||||
>>> f = ContactForm()
|
>>> f = ContactForm()
|
||||||
>>> f.as_table()
|
>>> f.as_table()
|
||||||
|
@ -574,6 +605,37 @@ it calls its ``as_table()`` method behind the scenes::
|
||||||
<tr><th><label for="id_sender">Sender:</label></th><td><input type="email" name="sender" id="id_sender" required></td></tr>
|
<tr><th><label for="id_sender">Sender:</label></th><td><input type="email" name="sender" id="id_sender" required></td></tr>
|
||||||
<tr><th><label for="id_cc_myself">Cc myself:</label></th><td><input type="checkbox" name="cc_myself" id="id_cc_myself"></td></tr>
|
<tr><th><label for="id_cc_myself">Cc myself:</label></th><td><input type="checkbox" name="cc_myself" id="id_cc_myself"></td></tr>
|
||||||
|
|
||||||
|
``get_context()``
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
.. versionadded:: 4.0
|
||||||
|
|
||||||
|
.. method:: Form.get_context()
|
||||||
|
|
||||||
|
Return context for form rendering in a template.
|
||||||
|
|
||||||
|
The available context is:
|
||||||
|
|
||||||
|
* ``form``: The bound form.
|
||||||
|
* ``fields``: All bound fields, except the hidden fields.
|
||||||
|
* ``hidden_fields``: All hidden bound fields.
|
||||||
|
* ``errors``: All non field related or hidden field related form errors.
|
||||||
|
|
||||||
|
``render()``
|
||||||
|
------------
|
||||||
|
|
||||||
|
.. versionadded:: 4.0
|
||||||
|
|
||||||
|
.. method:: Form.render(template_name=None, context=None, renderer=None)
|
||||||
|
|
||||||
|
The render method is called by ``__str__`` as well as the
|
||||||
|
:meth:`.Form.as_table`, :meth:`.Form.as_p`, and :meth:`.Form.as_ul` methods.
|
||||||
|
All arguments are optional and default to:
|
||||||
|
|
||||||
|
* ``template_name``: :attr:`.Form.template_name`
|
||||||
|
* ``context``: Value returned by :meth:`.Form.get_context`
|
||||||
|
* ``renderer``: Value returned by :attr:`.Form.default_renderer`
|
||||||
|
|
||||||
.. _ref-forms-api-styling-form-rows:
|
.. _ref-forms-api-styling-form-rows:
|
||||||
|
|
||||||
Styling required or erroneous form rows
|
Styling required or erroneous form rows
|
||||||
|
@ -834,25 +896,99 @@ method you're using::
|
||||||
Customizing the error list format
|
Customizing the error list format
|
||||||
---------------------------------
|
---------------------------------
|
||||||
|
|
||||||
By default, forms use ``django.forms.utils.ErrorList`` to format validation
|
.. class:: ErrorList(initlist=None, error_class=None, renderer=None)
|
||||||
errors. If you'd like to use an alternate class for displaying errors, you can
|
|
||||||
pass that in at construction time::
|
|
||||||
|
|
||||||
>>> from django.forms.utils import ErrorList
|
By default, forms use ``django.forms.utils.ErrorList`` to format validation
|
||||||
>>> class DivErrorList(ErrorList):
|
errors. ``ErrorList`` is a list like object where ``initlist`` is the
|
||||||
... def __str__(self):
|
list of errors. In addition this class has the following attributes and
|
||||||
... return self.as_divs()
|
methods.
|
||||||
... def as_divs(self):
|
|
||||||
... if not self: return ''
|
.. attribute:: error_class
|
||||||
... return '<div class="errorlist">%s</div>' % ''.join(['<div class="error">%s</div>' % e for e in self])
|
|
||||||
>>> f = ContactForm(data, auto_id=False, error_class=DivErrorList)
|
The CSS classes to be used when rendering the error list. Any provided
|
||||||
>>> f.as_p()
|
classes are added to the default ``errorlist`` class.
|
||||||
<div class="errorlist"><div class="error">This field is required.</div></div>
|
|
||||||
<p>Subject: <input type="text" name="subject" maxlength="100" required></p>
|
.. attribute:: renderer
|
||||||
<p>Message: <input type="text" name="message" value="Hi there" required></p>
|
|
||||||
<div class="errorlist"><div class="error">Enter a valid email address.</div></div>
|
.. versionadded:: 4.0
|
||||||
<p>Sender: <input type="email" name="sender" value="invalid email address" required></p>
|
|
||||||
<p>Cc myself: <input checked type="checkbox" name="cc_myself"></p>
|
Specifies the :doc:`renderer <renderers>` to use for ``ErrorList``.
|
||||||
|
Defaults to ``None`` which means to use the default renderer
|
||||||
|
specified by the :setting:`FORM_RENDERER` setting.
|
||||||
|
|
||||||
|
.. attribute:: template_name
|
||||||
|
|
||||||
|
.. versionadded:: 4.0
|
||||||
|
|
||||||
|
The name of the template used when calling ``__str__`` or
|
||||||
|
:meth:`render`. By default this is
|
||||||
|
``'django/forms/errors/list/default.html'`` which is a proxy for the
|
||||||
|
``'ul.html'`` template.
|
||||||
|
|
||||||
|
.. attribute:: template_name_text
|
||||||
|
|
||||||
|
.. versionadded:: 4.0
|
||||||
|
|
||||||
|
The name of the template used when calling :meth:`.as_text`. By default
|
||||||
|
this is ``'django/forms/errors/list/text.html'``. This template renders
|
||||||
|
the errors as a list of bullet points.
|
||||||
|
|
||||||
|
.. attribute:: template_name_ul
|
||||||
|
|
||||||
|
.. versionadded:: 4.0
|
||||||
|
|
||||||
|
The name of the template used when calling :meth:`.as_ul`. By default
|
||||||
|
this is ``'django/forms/errors/list/ul.html'``. This template renders
|
||||||
|
the errors in ``<li>`` tags with a wrapping ``<ul>`` with the CSS
|
||||||
|
classes as defined by :attr:`.error_class`.
|
||||||
|
|
||||||
|
.. method:: get_context()
|
||||||
|
|
||||||
|
.. versionadded:: 4.0
|
||||||
|
|
||||||
|
Return context for rendering of errors in a template.
|
||||||
|
|
||||||
|
The available context is:
|
||||||
|
|
||||||
|
* ``errors`` : A list of the errors.
|
||||||
|
* ``error_class`` : A string of CSS classes.
|
||||||
|
|
||||||
|
.. method:: render(template_name=None, context=None, renderer=None)
|
||||||
|
|
||||||
|
.. versionadded:: 4.0
|
||||||
|
|
||||||
|
The render method is called by ``__str__`` as well as by the
|
||||||
|
:meth:`.as_ul` method.
|
||||||
|
|
||||||
|
All arguments are optional and will default to:
|
||||||
|
|
||||||
|
* ``template_name``: Value returned by :attr:`.template_name`
|
||||||
|
* ``context``: Value returned by :meth:`.get_context`
|
||||||
|
* ``renderer``: Value returned by :attr:`.renderer`
|
||||||
|
|
||||||
|
.. method:: as_text()
|
||||||
|
|
||||||
|
Renders the error list using the template defined by
|
||||||
|
:attr:`.template_name_text`.
|
||||||
|
|
||||||
|
.. method:: as_ul()
|
||||||
|
|
||||||
|
Renders the error list using the template defined by
|
||||||
|
:attr:`.template_name_ul`.
|
||||||
|
|
||||||
|
If you'd like to customize the rendering of errors this can be achieved by
|
||||||
|
overriding the :attr:`.template_name` attribute or more generally by
|
||||||
|
overriding the default template, see also
|
||||||
|
:ref:`overriding-built-in-form-templates`.
|
||||||
|
|
||||||
|
.. versionchanged:: 4.0
|
||||||
|
|
||||||
|
Rendering of :class:`ErrorList` was moved to the template engine.
|
||||||
|
|
||||||
|
.. deprecated:: 4.0
|
||||||
|
|
||||||
|
The ability to return a ``str`` when calling the ``__str__`` method is
|
||||||
|
deprecated. Use the template engine instead which returns a ``SafeString``.
|
||||||
|
|
||||||
More granular output
|
More granular output
|
||||||
====================
|
====================
|
||||||
|
@ -1086,12 +1222,16 @@ Methods of ``BoundField``
|
||||||
attributes for the ``<label>`` tag.
|
attributes for the ``<label>`` tag.
|
||||||
|
|
||||||
The HTML that's generated includes the form's
|
The HTML that's generated includes the form's
|
||||||
:attr:`~django.forms.Form.label_suffix` (a colon, by default) or, if set, the
|
:attr:`~django.forms.Form.label_suffix` (a colon, by default) or, if set,
|
||||||
current field's :attr:`~django.forms.Field.label_suffix`. The optional
|
the current field's :attr:`~django.forms.Field.label_suffix`. The optional
|
||||||
``label_suffix`` parameter allows you to override any previously set
|
``label_suffix`` parameter allows you to override any previously set
|
||||||
suffix. For example, you can use an empty string to hide the label on selected
|
suffix. For example, you can use an empty string to hide the label on
|
||||||
fields. If you need to do this in a template, you could write a custom
|
selected fields. The label is rendered using the template specified by the
|
||||||
filter to allow passing parameters to ``label_tag``.
|
forms :attr:`.Form.template_name_label`.
|
||||||
|
|
||||||
|
.. versionchanged:: 4.0
|
||||||
|
|
||||||
|
The label is now rendered using the template engine.
|
||||||
|
|
||||||
.. method:: BoundField.value()
|
.. method:: BoundField.value()
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ Formset API reference. For introductory material about formsets, see the
|
||||||
``formset_factory``
|
``formset_factory``
|
||||||
===================
|
===================
|
||||||
|
|
||||||
.. function:: formset_factory(form, formset=BaseFormSet, extra=1, can_order=False, can_delete=False, max_num=None, validate_max=False, min_num=None, validate_min=False, absolute_max=None, can_delete_extra=True)
|
.. function:: formset_factory(form, formset=BaseFormSet, extra=1, can_order=False, can_delete=False, max_num=None, validate_max=False, min_num=None, validate_min=False, absolute_max=None, can_delete_extra=True, renderer=None)
|
||||||
|
|
||||||
Returns a ``FormSet`` class for the given ``form`` class.
|
Returns a ``FormSet`` class for the given ``form`` class.
|
||||||
|
|
||||||
|
@ -20,3 +20,7 @@ Formset API reference. For introductory material about formsets, see the
|
||||||
.. versionchanged:: 3.2
|
.. versionchanged:: 3.2
|
||||||
|
|
||||||
The ``absolute_max`` and ``can_delete_extra`` arguments were added.
|
The ``absolute_max`` and ``can_delete_extra`` arguments were added.
|
||||||
|
|
||||||
|
.. versionchanged:: 4.0
|
||||||
|
|
||||||
|
The ``renderer`` argument was added.
|
||||||
|
|
|
@ -52,7 +52,7 @@ Model Form API reference. For introductory material about model forms, see the
|
||||||
``modelformset_factory``
|
``modelformset_factory``
|
||||||
========================
|
========================
|
||||||
|
|
||||||
.. function:: 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, widgets=None, validate_max=False, localized_fields=None, labels=None, help_texts=None, error_messages=None, min_num=None, validate_min=False, field_classes=None, absolute_max=None, can_delete_extra=True)
|
.. function:: 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, widgets=None, validate_max=False, localized_fields=None, labels=None, help_texts=None, error_messages=None, min_num=None, validate_min=False, field_classes=None, absolute_max=None, can_delete_extra=True, renderer=None)
|
||||||
|
|
||||||
Returns a ``FormSet`` class for the given ``model`` class.
|
Returns a ``FormSet`` class for the given ``model`` class.
|
||||||
|
|
||||||
|
@ -63,9 +63,9 @@ Model Form API reference. For introductory material about model forms, see the
|
||||||
|
|
||||||
Arguments ``formset``, ``extra``, ``can_delete``, ``can_order``,
|
Arguments ``formset``, ``extra``, ``can_delete``, ``can_order``,
|
||||||
``max_num``, ``validate_max``, ``min_num``, ``validate_min``,
|
``max_num``, ``validate_max``, ``min_num``, ``validate_min``,
|
||||||
``absolute_max``, and ``can_delete_extra`` are passed through to
|
``absolute_max``, ``can_delete_extra``, and ``renderer`` are passed
|
||||||
:func:`~django.forms.formsets.formset_factory`. See :doc:`formsets
|
through to :func:`~django.forms.formsets.formset_factory`. See
|
||||||
</topics/forms/formsets>` for details.
|
:doc:`formsets </topics/forms/formsets>` for details.
|
||||||
|
|
||||||
See :ref:`model-formsets` for example usage.
|
See :ref:`model-formsets` for example usage.
|
||||||
|
|
||||||
|
@ -73,10 +73,14 @@ Model Form API reference. For introductory material about model forms, see the
|
||||||
|
|
||||||
The ``absolute_max`` and ``can_delete_extra`` arguments were added.
|
The ``absolute_max`` and ``can_delete_extra`` arguments were added.
|
||||||
|
|
||||||
|
.. versionchanged:: 4.0
|
||||||
|
|
||||||
|
The ``renderer`` argument was added.
|
||||||
|
|
||||||
``inlineformset_factory``
|
``inlineformset_factory``
|
||||||
=========================
|
=========================
|
||||||
|
|
||||||
.. function:: inlineformset_factory(parent_model, model, form=ModelForm, formset=BaseInlineFormSet, fk_name=None, fields=None, exclude=None, extra=3, can_order=False, can_delete=True, max_num=None, formfield_callback=None, widgets=None, validate_max=False, localized_fields=None, labels=None, help_texts=None, error_messages=None, min_num=None, validate_min=False, field_classes=None, absolute_max=None, can_delete_extra=True)
|
.. function:: inlineformset_factory(parent_model, model, form=ModelForm, formset=BaseInlineFormSet, fk_name=None, fields=None, exclude=None, extra=3, can_order=False, can_delete=True, max_num=None, formfield_callback=None, widgets=None, validate_max=False, localized_fields=None, labels=None, help_texts=None, error_messages=None, min_num=None, validate_min=False, field_classes=None, absolute_max=None, can_delete_extra=True, renderer=None)
|
||||||
|
|
||||||
Returns an ``InlineFormSet`` using :func:`modelformset_factory` with
|
Returns an ``InlineFormSet`` using :func:`modelformset_factory` with
|
||||||
defaults of ``formset=``:class:`~django.forms.models.BaseInlineFormSet`,
|
defaults of ``formset=``:class:`~django.forms.models.BaseInlineFormSet`,
|
||||||
|
@ -90,3 +94,7 @@ Model Form API reference. For introductory material about model forms, see the
|
||||||
.. versionchanged:: 3.2
|
.. versionchanged:: 3.2
|
||||||
|
|
||||||
The ``absolute_max`` and ``can_delete_extra`` arguments were added.
|
The ``absolute_max`` and ``can_delete_extra`` arguments were added.
|
||||||
|
|
||||||
|
.. versionchanged:: 4.0
|
||||||
|
|
||||||
|
The ``renderer`` argument was added.
|
||||||
|
|
|
@ -68,11 +68,11 @@ it uses a :class:`~django.template.backends.jinja2.Jinja2` backend. Templates
|
||||||
for the built-in widgets are located in ``django/forms/jinja2`` and installed
|
for the built-in widgets are located in ``django/forms/jinja2`` and installed
|
||||||
apps can provide templates in a ``jinja2`` directory.
|
apps can provide templates in a ``jinja2`` directory.
|
||||||
|
|
||||||
To use this backend, all the widgets in your project and its third-party apps
|
To use this backend, all the forms and widgets in your project and its
|
||||||
must have Jinja2 templates. Unless you provide your own Jinja2 templates for
|
third-party apps must have Jinja2 templates. Unless you provide your own Jinja2
|
||||||
widgets that don't have any, you can't use this renderer. For example,
|
templates for widgets that don't have any, you can't use this renderer. For
|
||||||
:mod:`django.contrib.admin` doesn't include Jinja2 templates for its widgets
|
example, :mod:`django.contrib.admin` doesn't include Jinja2 templates for its
|
||||||
due to their usage of Django template tags.
|
widgets due to their usage of Django template tags.
|
||||||
|
|
||||||
``TemplatesSetting``
|
``TemplatesSetting``
|
||||||
--------------------
|
--------------------
|
||||||
|
@ -97,6 +97,29 @@ Using this renderer along with the built-in widget templates requires either:
|
||||||
Using this renderer requires you to make sure the form templates your project
|
Using this renderer requires you to make sure the form templates your project
|
||||||
needs can be located.
|
needs can be located.
|
||||||
|
|
||||||
|
Context available in formset templates
|
||||||
|
======================================
|
||||||
|
|
||||||
|
.. versionadded:: 4.0
|
||||||
|
|
||||||
|
Formset templates receive a context from :meth:`.BaseFormSet.get_context`. By
|
||||||
|
default, formsets receive a dictionary with the following values:
|
||||||
|
|
||||||
|
* ``formset``: The formset instance.
|
||||||
|
|
||||||
|
Context available in form templates
|
||||||
|
===================================
|
||||||
|
|
||||||
|
.. versionadded:: 4.0
|
||||||
|
|
||||||
|
Form templates receive a context from :meth:`.Form.get_context`. By default,
|
||||||
|
forms receive a dictionary with the following values:
|
||||||
|
|
||||||
|
* ``form``: The bound form.
|
||||||
|
* ``fields``: All bound fields, except the hidden fields.
|
||||||
|
* ``hidden_fields``: All hidden bound fields.
|
||||||
|
* ``errors``: All non field related or hidden field related form errors.
|
||||||
|
|
||||||
Context available in widget templates
|
Context available in widget templates
|
||||||
=====================================
|
=====================================
|
||||||
|
|
||||||
|
@ -114,6 +137,32 @@ Some widgets add further information to the context. For instance, all widgets
|
||||||
that subclass ``Input`` defines ``widget['type']`` and :class:`.MultiWidget`
|
that subclass ``Input`` defines ``widget['type']`` and :class:`.MultiWidget`
|
||||||
defines ``widget['subwidgets']`` for looping purposes.
|
defines ``widget['subwidgets']`` for looping purposes.
|
||||||
|
|
||||||
|
.. _overriding-built-in-formset-templates:
|
||||||
|
|
||||||
|
Overriding built-in formset templates
|
||||||
|
=====================================
|
||||||
|
|
||||||
|
.. versionadded:: 4.0
|
||||||
|
|
||||||
|
:attr:`.BaseFormSet.template_name`
|
||||||
|
|
||||||
|
To override formset templates, you must use the :class:`TemplatesSetting`
|
||||||
|
renderer. Then overriding widget templates works :doc:`the same as
|
||||||
|
</howto/overriding-templates>` overriding any other template in your project.
|
||||||
|
|
||||||
|
.. _overriding-built-in-form-templates:
|
||||||
|
|
||||||
|
Overriding built-in form templates
|
||||||
|
==================================
|
||||||
|
|
||||||
|
.. versionadded:: 4.0
|
||||||
|
|
||||||
|
:attr:`.Form.template_name`
|
||||||
|
|
||||||
|
To override form templates, you must use the :class:`TemplatesSetting`
|
||||||
|
renderer. Then overriding widget templates works :doc:`the same as
|
||||||
|
</howto/overriding-templates>` overriding any other template in your project.
|
||||||
|
|
||||||
.. _overriding-built-in-widget-templates:
|
.. _overriding-built-in-widget-templates:
|
||||||
|
|
||||||
Overriding built-in widget templates
|
Overriding built-in widget templates
|
||||||
|
|
|
@ -1671,8 +1671,9 @@ generate correct URLs when ``SCRIPT_NAME`` is not ``/``.
|
||||||
|
|
||||||
Default: ``'``:class:`django.forms.renderers.DjangoTemplates`\ ``'``
|
Default: ``'``:class:`django.forms.renderers.DjangoTemplates`\ ``'``
|
||||||
|
|
||||||
The class that renders form widgets. It must implement :ref:`the low-level
|
The class that renders forms and form widgets. It must implement
|
||||||
render API <low-level-widget-render-api>`. Included form renderers are:
|
:ref:`the low-level render API <low-level-widget-render-api>`. Included form
|
||||||
|
renderers are:
|
||||||
|
|
||||||
* ``'``:class:`django.forms.renderers.DjangoTemplates`\ ``'``
|
* ``'``:class:`django.forms.renderers.DjangoTemplates`\ ``'``
|
||||||
* ``'``:class:`django.forms.renderers.Jinja2`\ ``'``
|
* ``'``:class:`django.forms.renderers.Jinja2`\ ``'``
|
||||||
|
|
|
@ -115,6 +115,17 @@ in Django <redis>`.
|
||||||
|
|
||||||
.. _`redis-py`: https://pypi.org/project/redis/
|
.. _`redis-py`: https://pypi.org/project/redis/
|
||||||
|
|
||||||
|
Template based form rendering
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
To enhance customization of :class:`Forms <django.forms.Form>`,
|
||||||
|
:doc:`Formsets </topics/forms/formsets>`, and
|
||||||
|
:class:`~django.forms.ErrorList` they are now rendered using the template
|
||||||
|
engine. See the new :meth:`~django.forms.Form.render`,
|
||||||
|
:meth:`~django.forms.Form.get_context`, and
|
||||||
|
:attr:`~django.forms.Form.template_name` for ``Form`` and
|
||||||
|
:ref:`formset rendering <formset-rendering>` for ``Formset``.
|
||||||
|
|
||||||
Minor features
|
Minor features
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
|
@ -735,6 +746,12 @@ Miscellaneous
|
||||||
are deprecated. Use :class:`~django.contrib.admin.ModelAdmin` and
|
are deprecated. Use :class:`~django.contrib.admin.ModelAdmin` and
|
||||||
:class:`~django.contrib.gis.admin.GISModelAdmin` instead.
|
:class:`~django.contrib.gis.admin.GISModelAdmin` instead.
|
||||||
|
|
||||||
|
* Since form rendering now uses the template engine, the undocumented
|
||||||
|
``BaseForm._html_output()`` helper method is deprecated.
|
||||||
|
|
||||||
|
* The ability to return a ``str`` from ``ErrorList`` and ``ErrorDict`` is
|
||||||
|
deprecated. It is expected these methods return a ``SafeString``.
|
||||||
|
|
||||||
Features removed in 4.0
|
Features removed in 4.0
|
||||||
=======================
|
=======================
|
||||||
|
|
||||||
|
|
|
@ -775,9 +775,92 @@ But with ``ArticleFormset(prefix='article')`` that becomes:
|
||||||
This is useful if you want to :ref:`use more than one formset in a view
|
This is useful if you want to :ref:`use more than one formset in a view
|
||||||
<multiple-formsets-in-view>`.
|
<multiple-formsets-in-view>`.
|
||||||
|
|
||||||
|
.. _formset-rendering:
|
||||||
|
|
||||||
Using a formset in views and templates
|
Using a formset in views and templates
|
||||||
======================================
|
======================================
|
||||||
|
|
||||||
|
Formsets have five attributes and five methods associated with rendering.
|
||||||
|
|
||||||
|
.. attribute:: BaseFormSet.renderer
|
||||||
|
|
||||||
|
.. versionadded:: 4.0
|
||||||
|
|
||||||
|
Specifies the :doc:`renderer </ref/forms/renderers>` to use for the
|
||||||
|
formset. Defaults to the renderer specified by the :setting:`FORM_RENDERER`
|
||||||
|
setting.
|
||||||
|
|
||||||
|
.. attribute:: BaseFormSet.template_name
|
||||||
|
|
||||||
|
.. versionadded:: 4.0
|
||||||
|
|
||||||
|
The name of the template used when calling ``__str__`` or :meth:`.render`.
|
||||||
|
This template renders the formsets management forms and then each form in
|
||||||
|
the formset as per the template defined by the
|
||||||
|
forms :attr:`~django.forms.Form.template_name`. This is a proxy of
|
||||||
|
``as_table`` by default.
|
||||||
|
|
||||||
|
.. attribute:: BaseFormSet.template_name_p
|
||||||
|
|
||||||
|
.. versionadded:: 4.0
|
||||||
|
|
||||||
|
The name of the template used when calling :meth:`.as_p`. By default this
|
||||||
|
is ``'django/forms/formsets/p.html'``. This template renders the formsets
|
||||||
|
management forms and then each form in the formset as per the forms
|
||||||
|
:meth:`~django.forms.Form.as_p` method.
|
||||||
|
|
||||||
|
.. attribute:: BaseFormSet.template_name_table
|
||||||
|
|
||||||
|
.. versionadded:: 4.0
|
||||||
|
|
||||||
|
The name of the template used when calling :meth:`.as_table`. By default
|
||||||
|
this is ``'django/forms/formsets/table.html'``. This template renders the
|
||||||
|
formsets management forms and then each form in the formset as per the
|
||||||
|
forms :meth:`~django.forms.Form.as_table` method.
|
||||||
|
|
||||||
|
.. attribute:: BaseFormSet.template_name_ul
|
||||||
|
|
||||||
|
.. versionadded:: 4.0
|
||||||
|
|
||||||
|
The name of the template used when calling :meth:`.as_ul`. By default this
|
||||||
|
is ``'django/forms/formsets/ul.html'``. This template renders the formsets
|
||||||
|
management forms and then each form in the formset as per the forms
|
||||||
|
:meth:`~django.forms.Form.as_ul` method.
|
||||||
|
|
||||||
|
.. method:: BaseFormSet.get_context()
|
||||||
|
|
||||||
|
.. versionadded:: 4.0
|
||||||
|
|
||||||
|
Returns the context for rendering a formset in a template.
|
||||||
|
|
||||||
|
The available context is:
|
||||||
|
|
||||||
|
* ``formset`` : The instance of the formset.
|
||||||
|
|
||||||
|
.. method:: BaseFormSet.render(template_name=None, context=None, renderer=None)
|
||||||
|
|
||||||
|
.. versionadded:: 4.0
|
||||||
|
|
||||||
|
The render method is called by ``__str__`` as well as the :meth:`.as_p`,
|
||||||
|
:meth:`.as_ul`, and :meth:`.as_table` methods. All arguments are optional
|
||||||
|
and will default to:
|
||||||
|
|
||||||
|
* ``template_name``: :attr:`.template_name`
|
||||||
|
* ``context``: Value returned by :meth:`.get_context`
|
||||||
|
* ``renderer``: Value returned by :attr:`.renderer`
|
||||||
|
|
||||||
|
.. method:: BaseFormSet.as_p()
|
||||||
|
|
||||||
|
Renders the formset with the :attr:`.template_name_p` template.
|
||||||
|
|
||||||
|
.. method:: BaseFormSet.as_table()
|
||||||
|
|
||||||
|
Renders the formset with the :attr:`.template_name_table` template.
|
||||||
|
|
||||||
|
.. method:: BaseFormSet.as_ul()
|
||||||
|
|
||||||
|
Renders the formset with the :attr:`.template_name_ul` template.
|
||||||
|
|
||||||
Using a formset inside a view is not very different from using a regular
|
Using a formset inside a view is not very different from using a regular
|
||||||
``Form`` class. The only thing you will want to be aware of is making sure to
|
``Form`` class. The only thing you will want to be aware of is making sure to
|
||||||
use the management form inside the template. Let's look at a sample view::
|
use the management form inside the template. Let's look at a sample view::
|
||||||
|
@ -821,7 +904,17 @@ deal with the management form:
|
||||||
</table>
|
</table>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
The above ends up calling the ``as_table`` method on the formset class.
|
The above ends up calling the :meth:`BaseFormSet.render` method on the formset
|
||||||
|
class. This renders the formset using the template specified by the
|
||||||
|
:attr:`~BaseFormSet.template_name` attribute. Similar to forms, by default the
|
||||||
|
formset will be rendered ``as_table``, with other helper methods of ``as_p``
|
||||||
|
and ``as_ul`` being available. The rendering of the formset can be customized
|
||||||
|
by specifying the ``template_name`` attribute, or more generally by
|
||||||
|
:ref:`overriding the default template <overriding-built-in-formset-templates>`.
|
||||||
|
|
||||||
|
.. versionchanged:: 4.0
|
||||||
|
|
||||||
|
Rendering of formsets was moved to the template engine.
|
||||||
|
|
||||||
.. _manually-rendered-can-delete-and-can-order:
|
.. _manually-rendered-can-delete-and-can-order:
|
||||||
|
|
||||||
|
|
|
@ -733,12 +733,17 @@ Reusable form templates
|
||||||
|
|
||||||
If your site uses the same rendering logic for forms in multiple places, you
|
If your site uses the same rendering logic for forms in multiple places, you
|
||||||
can reduce duplication by saving the form's loop in a standalone template and
|
can reduce duplication by saving the form's loop in a standalone template and
|
||||||
using the :ttag:`include` tag to reuse it in other templates:
|
overriding the forms :attr:`~django.forms.Form.template_name` attribute to
|
||||||
|
render the form using the custom template. The below example will result in
|
||||||
|
``{{ form }}`` being rendered as the output of the ``form_snippet.html``
|
||||||
|
template.
|
||||||
|
|
||||||
|
In your templates:
|
||||||
|
|
||||||
.. code-block:: html+django
|
.. code-block:: html+django
|
||||||
|
|
||||||
# In your form template:
|
# In your template:
|
||||||
{% include "form_snippet.html" %}
|
{{ form }}
|
||||||
|
|
||||||
# In form_snippet.html:
|
# In form_snippet.html:
|
||||||
{% for field in form %}
|
{% for field in form %}
|
||||||
|
@ -748,16 +753,15 @@ using the :ttag:`include` tag to reuse it in other templates:
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
If the form object passed to a template has a different name within the
|
In your form::
|
||||||
context, you can alias it using the ``with`` argument of the :ttag:`include`
|
|
||||||
tag:
|
|
||||||
|
|
||||||
.. code-block:: html+django
|
class MyForm(forms.Form):
|
||||||
|
template_name = 'form_snippet.html'
|
||||||
|
...
|
||||||
|
|
||||||
{% include "form_snippet.html" with form=comment_form %}
|
.. versionchanged:: 4.0
|
||||||
|
|
||||||
If you find yourself doing this often, you might consider creating a custom
|
Template rendering of forms was added.
|
||||||
:ref:`inclusion tag<howto-custom-template-tags-inclusion-tags>`.
|
|
||||||
|
|
||||||
Further topics
|
Further topics
|
||||||
==============
|
==============
|
||||||
|
|
|
@ -6374,7 +6374,7 @@ class AdminViewOnSiteTests(TestCase):
|
||||||
response, 'inline_admin_formset', 0, None,
|
response, 'inline_admin_formset', 0, None,
|
||||||
['Children must share a family name with their parents in this contrived test case']
|
['Children must share a family name with their parents in this contrived test case']
|
||||||
)
|
)
|
||||||
msg = "The formset 'inline_admin_formset' in context 12 does not contain any non-form errors."
|
msg = "The formset 'inline_admin_formset' in context 22 does not contain any non-form errors."
|
||||||
with self.assertRaisesMessage(AssertionError, msg):
|
with self.assertRaisesMessage(AssertionError, msg):
|
||||||
self.assertFormsetError(response, 'inline_admin_formset', None, None, ['Error'])
|
self.assertFormsetError(response, 'inline_admin_formset', None, None, ['Error'])
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
{% if errors %}<div class="errorlist">{% for error in errors %}<div class="error">{{ error }}</div>{% endfor %}</div>{% endif %}
|
|
@ -0,0 +1,6 @@
|
||||||
|
{% for field in form %}
|
||||||
|
<div class="fieldWrapper">
|
||||||
|
{{ field.errors }}
|
||||||
|
{{ field.label_tag }} {{ field }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
|
@ -0,0 +1,30 @@
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
from django.test.utils import override_settings
|
||||||
|
|
||||||
|
TEST_SETTINGS = [
|
||||||
|
{
|
||||||
|
'FORM_RENDERER': 'django.forms.renderers.DjangoTemplates',
|
||||||
|
'TEMPLATES': {'BACKEND': 'django.template.backends.django.DjangoTemplates'},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'FORM_RENDERER': 'django.forms.renderers.Jinja2',
|
||||||
|
'TEMPLATES': {'BACKEND': 'django.template.backends.jinja2.Jinja2'},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_all_form_renderers():
|
||||||
|
def wrapper(func):
|
||||||
|
def inner(*args, **kwargs):
|
||||||
|
for settings in TEST_SETTINGS:
|
||||||
|
with override_settings(**settings):
|
||||||
|
func(*args, **kwargs)
|
||||||
|
return inner
|
||||||
|
|
||||||
|
def decorator(cls):
|
||||||
|
for name, func in inspect.getmembers(cls, inspect.isfunction):
|
||||||
|
if name.startswith('test_'):
|
||||||
|
setattr(cls, name, wrapper(func))
|
||||||
|
return cls
|
||||||
|
return decorator
|
|
@ -0,0 +1,183 @@
|
||||||
|
# RemovedInDjango50
|
||||||
|
from django.forms import CharField, EmailField, Form, HiddenInput
|
||||||
|
from django.forms.utils import ErrorList
|
||||||
|
from django.test import SimpleTestCase, ignore_warnings
|
||||||
|
from django.utils.deprecation import RemovedInDjango50Warning
|
||||||
|
|
||||||
|
from .test_forms import Person
|
||||||
|
|
||||||
|
|
||||||
|
class DivErrorList(ErrorList):
|
||||||
|
def __str__(self):
|
||||||
|
return self.as_divs()
|
||||||
|
|
||||||
|
def as_divs(self):
|
||||||
|
if not self:
|
||||||
|
return ''
|
||||||
|
return '<div class="errorlist">%s</div>' % ''.join(
|
||||||
|
f'<div class="error">{error}</div>' for error in self
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DeprecationTests(SimpleTestCase):
|
||||||
|
def test_deprecation_warning_html_output(self):
|
||||||
|
msg = (
|
||||||
|
'django.forms.BaseForm._html_output() is deprecated. Please use '
|
||||||
|
'.render() and .get_context() instead.'
|
||||||
|
)
|
||||||
|
with self.assertRaisesMessage(RemovedInDjango50Warning, msg):
|
||||||
|
form = Person()
|
||||||
|
form._html_output(
|
||||||
|
normal_row='<p id="p_%(field_name)s"></p>',
|
||||||
|
error_row='%s',
|
||||||
|
row_ender='</p>',
|
||||||
|
help_text_html=' %s',
|
||||||
|
errors_on_separate_row=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_deprecation_warning_error_list(self):
|
||||||
|
class EmailForm(Form):
|
||||||
|
email = EmailField()
|
||||||
|
comment = CharField()
|
||||||
|
|
||||||
|
data = {'email': 'invalid'}
|
||||||
|
f = EmailForm(data, error_class=DivErrorList)
|
||||||
|
msg = (
|
||||||
|
'Returning a plain string from DivErrorList is deprecated. Please '
|
||||||
|
'customize via the template system instead.'
|
||||||
|
)
|
||||||
|
with self.assertRaisesMessage(RemovedInDjango50Warning, msg):
|
||||||
|
f.as_p()
|
||||||
|
|
||||||
|
|
||||||
|
@ignore_warnings(category=RemovedInDjango50Warning)
|
||||||
|
class DeprecatedTests(SimpleTestCase):
|
||||||
|
def test_errorlist_override_str(self):
|
||||||
|
class CommentForm(Form):
|
||||||
|
name = CharField(max_length=50, required=False)
|
||||||
|
email = EmailField()
|
||||||
|
comment = CharField()
|
||||||
|
|
||||||
|
data = {'email': 'invalid'}
|
||||||
|
f = CommentForm(data, auto_id=False, error_class=DivErrorList)
|
||||||
|
self.assertHTMLEqual(
|
||||||
|
f.as_p(),
|
||||||
|
'<p>Name: <input type="text" name="name" maxlength="50"></p>'
|
||||||
|
'<div class="errorlist">'
|
||||||
|
'<div class="error">Enter a valid email address.</div></div>'
|
||||||
|
'<p>Email: <input type="email" name="email" value="invalid" required></p>'
|
||||||
|
'<div class="errorlist">'
|
||||||
|
'<div class="error">This field is required.</div></div>'
|
||||||
|
'<p>Comment: <input type="text" name="comment" required></p>',
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_field_name(self):
|
||||||
|
"""#5749 - `field_name` may be used as a key in _html_output()."""
|
||||||
|
class SomeForm(Form):
|
||||||
|
some_field = CharField()
|
||||||
|
|
||||||
|
def as_p(self):
|
||||||
|
return self._html_output(
|
||||||
|
normal_row='<p id="p_%(field_name)s"></p>',
|
||||||
|
error_row='%s',
|
||||||
|
row_ender='</p>',
|
||||||
|
help_text_html=' %s',
|
||||||
|
errors_on_separate_row=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
form = SomeForm()
|
||||||
|
self.assertHTMLEqual(form.as_p(), '<p id="p_some_field"></p>')
|
||||||
|
|
||||||
|
def test_field_without_css_classes(self):
|
||||||
|
"""
|
||||||
|
`css_classes` may be used as a key in _html_output() (empty classes).
|
||||||
|
"""
|
||||||
|
class SomeForm(Form):
|
||||||
|
some_field = CharField()
|
||||||
|
|
||||||
|
def as_p(self):
|
||||||
|
return self._html_output(
|
||||||
|
normal_row='<p class="%(css_classes)s"></p>',
|
||||||
|
error_row='%s',
|
||||||
|
row_ender='</p>',
|
||||||
|
help_text_html=' %s',
|
||||||
|
errors_on_separate_row=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
form = SomeForm()
|
||||||
|
self.assertHTMLEqual(form.as_p(), '<p class=""></p>')
|
||||||
|
|
||||||
|
def test_field_with_css_class(self):
|
||||||
|
"""
|
||||||
|
`css_classes` may be used as a key in _html_output() (class comes
|
||||||
|
from required_css_class in this case).
|
||||||
|
"""
|
||||||
|
class SomeForm(Form):
|
||||||
|
some_field = CharField()
|
||||||
|
required_css_class = 'foo'
|
||||||
|
|
||||||
|
def as_p(self):
|
||||||
|
return self._html_output(
|
||||||
|
normal_row='<p class="%(css_classes)s"></p>',
|
||||||
|
error_row='%s',
|
||||||
|
row_ender='</p>',
|
||||||
|
help_text_html=' %s',
|
||||||
|
errors_on_separate_row=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
form = SomeForm()
|
||||||
|
self.assertHTMLEqual(form.as_p(), '<p class="foo"></p>')
|
||||||
|
|
||||||
|
def test_field_name_with_hidden_input(self):
|
||||||
|
"""
|
||||||
|
BaseForm._html_output() should merge all the hidden input fields and
|
||||||
|
put them in the last row.
|
||||||
|
"""
|
||||||
|
class SomeForm(Form):
|
||||||
|
hidden1 = CharField(widget=HiddenInput)
|
||||||
|
custom = CharField()
|
||||||
|
hidden2 = CharField(widget=HiddenInput)
|
||||||
|
|
||||||
|
def as_p(self):
|
||||||
|
return self._html_output(
|
||||||
|
normal_row='<p%(html_class_attr)s>%(field)s %(field_name)s</p>',
|
||||||
|
error_row='%s',
|
||||||
|
row_ender='</p>',
|
||||||
|
help_text_html=' %s',
|
||||||
|
errors_on_separate_row=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
form = SomeForm()
|
||||||
|
self.assertHTMLEqual(
|
||||||
|
form.as_p(),
|
||||||
|
'<p><input id="id_custom" name="custom" type="text" required> custom'
|
||||||
|
'<input id="id_hidden1" name="hidden1" type="hidden">'
|
||||||
|
'<input id="id_hidden2" name="hidden2" type="hidden"></p>'
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_field_name_with_hidden_input_and_non_matching_row_ender(self):
|
||||||
|
"""
|
||||||
|
BaseForm._html_output() should merge all the hidden input fields and
|
||||||
|
put them in the last row ended with the specific row ender.
|
||||||
|
"""
|
||||||
|
class SomeForm(Form):
|
||||||
|
hidden1 = CharField(widget=HiddenInput)
|
||||||
|
custom = CharField()
|
||||||
|
hidden2 = CharField(widget=HiddenInput)
|
||||||
|
|
||||||
|
def as_p(self):
|
||||||
|
return self._html_output(
|
||||||
|
normal_row='<p%(html_class_attr)s>%(field)s %(field_name)s</p>',
|
||||||
|
error_row='%s',
|
||||||
|
row_ender='<hr><hr>',
|
||||||
|
help_text_html=' %s',
|
||||||
|
errors_on_separate_row=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
form = SomeForm()
|
||||||
|
self.assertHTMLEqual(
|
||||||
|
form.as_p(),
|
||||||
|
'<p><input id="id_custom" name="custom" type="text" required> custom</p>\n'
|
||||||
|
'<input id="id_hidden1" name="hidden1" type="hidden">'
|
||||||
|
'<input id="id_hidden2" name="hidden2" type="hidden"><hr><hr>'
|
||||||
|
)
|
|
@ -23,6 +23,7 @@ from django.test import SimpleTestCase
|
||||||
from django.test.utils import override_settings
|
from django.test.utils import override_settings
|
||||||
from django.utils.datastructures import MultiValueDict
|
from django.utils.datastructures import MultiValueDict
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
from tests.forms_tests.tests import test_all_form_renderers
|
||||||
|
|
||||||
|
|
||||||
class FrameworkForm(Form):
|
class FrameworkForm(Form):
|
||||||
|
@ -55,6 +56,7 @@ class MultiValueDictLike(dict):
|
||||||
return [self[key]]
|
return [self[key]]
|
||||||
|
|
||||||
|
|
||||||
|
@test_all_form_renderers()
|
||||||
class FormsTestCase(SimpleTestCase):
|
class FormsTestCase(SimpleTestCase):
|
||||||
# A Form is a collection of Fields. It knows how to validate a set of data and it
|
# A Form is a collection of Fields. It knows how to validate a set of data and it
|
||||||
# knows how to render itself in a couple of default ways (e.g., an HTML table).
|
# knows how to render itself in a couple of default ways (e.g., an HTML table).
|
||||||
|
@ -3077,117 +3079,6 @@ Password: <input type="password" name="password" required>
|
||||||
|
|
||||||
self.assertHTMLEqual(boundfield.label_tag(label_suffix='$'), '<label for="id_field">Field$</label>')
|
self.assertHTMLEqual(boundfield.label_tag(label_suffix='$'), '<label for="id_field">Field$</label>')
|
||||||
|
|
||||||
def test_field_name(self):
|
|
||||||
"""#5749 - `field_name` may be used as a key in _html_output()."""
|
|
||||||
class SomeForm(Form):
|
|
||||||
some_field = CharField()
|
|
||||||
|
|
||||||
def as_p(self):
|
|
||||||
return self._html_output(
|
|
||||||
normal_row='<p id="p_%(field_name)s"></p>',
|
|
||||||
error_row='%s',
|
|
||||||
row_ender='</p>',
|
|
||||||
help_text_html=' %s',
|
|
||||||
errors_on_separate_row=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
form = SomeForm()
|
|
||||||
self.assertHTMLEqual(form.as_p(), '<p id="p_some_field"></p>')
|
|
||||||
|
|
||||||
def test_field_without_css_classes(self):
|
|
||||||
"""
|
|
||||||
`css_classes` may be used as a key in _html_output() (empty classes).
|
|
||||||
"""
|
|
||||||
class SomeForm(Form):
|
|
||||||
some_field = CharField()
|
|
||||||
|
|
||||||
def as_p(self):
|
|
||||||
return self._html_output(
|
|
||||||
normal_row='<p class="%(css_classes)s"></p>',
|
|
||||||
error_row='%s',
|
|
||||||
row_ender='</p>',
|
|
||||||
help_text_html=' %s',
|
|
||||||
errors_on_separate_row=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
form = SomeForm()
|
|
||||||
self.assertHTMLEqual(form.as_p(), '<p class=""></p>')
|
|
||||||
|
|
||||||
def test_field_with_css_class(self):
|
|
||||||
"""
|
|
||||||
`css_classes` may be used as a key in _html_output() (class comes
|
|
||||||
from required_css_class in this case).
|
|
||||||
"""
|
|
||||||
class SomeForm(Form):
|
|
||||||
some_field = CharField()
|
|
||||||
required_css_class = 'foo'
|
|
||||||
|
|
||||||
def as_p(self):
|
|
||||||
return self._html_output(
|
|
||||||
normal_row='<p class="%(css_classes)s"></p>',
|
|
||||||
error_row='%s',
|
|
||||||
row_ender='</p>',
|
|
||||||
help_text_html=' %s',
|
|
||||||
errors_on_separate_row=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
form = SomeForm()
|
|
||||||
self.assertHTMLEqual(form.as_p(), '<p class="foo"></p>')
|
|
||||||
|
|
||||||
def test_field_name_with_hidden_input(self):
|
|
||||||
"""
|
|
||||||
BaseForm._html_output() should merge all the hidden input fields and
|
|
||||||
put them in the last row.
|
|
||||||
"""
|
|
||||||
class SomeForm(Form):
|
|
||||||
hidden1 = CharField(widget=HiddenInput)
|
|
||||||
custom = CharField()
|
|
||||||
hidden2 = CharField(widget=HiddenInput)
|
|
||||||
|
|
||||||
def as_p(self):
|
|
||||||
return self._html_output(
|
|
||||||
normal_row='<p%(html_class_attr)s>%(field)s %(field_name)s</p>',
|
|
||||||
error_row='%s',
|
|
||||||
row_ender='</p>',
|
|
||||||
help_text_html=' %s',
|
|
||||||
errors_on_separate_row=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
form = SomeForm()
|
|
||||||
self.assertHTMLEqual(
|
|
||||||
form.as_p(),
|
|
||||||
'<p><input id="id_custom" name="custom" type="text" required> custom'
|
|
||||||
'<input id="id_hidden1" name="hidden1" type="hidden">'
|
|
||||||
'<input id="id_hidden2" name="hidden2" type="hidden"></p>'
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_field_name_with_hidden_input_and_non_matching_row_ender(self):
|
|
||||||
"""
|
|
||||||
BaseForm._html_output() should merge all the hidden input fields and
|
|
||||||
put them in the last row ended with the specific row ender.
|
|
||||||
"""
|
|
||||||
class SomeForm(Form):
|
|
||||||
hidden1 = CharField(widget=HiddenInput)
|
|
||||||
custom = CharField()
|
|
||||||
hidden2 = CharField(widget=HiddenInput)
|
|
||||||
|
|
||||||
def as_p(self):
|
|
||||||
return self._html_output(
|
|
||||||
normal_row='<p%(html_class_attr)s>%(field)s %(field_name)s</p>',
|
|
||||||
error_row='%s',
|
|
||||||
row_ender='<hr><hr>',
|
|
||||||
help_text_html=' %s',
|
|
||||||
errors_on_separate_row=True
|
|
||||||
)
|
|
||||||
|
|
||||||
form = SomeForm()
|
|
||||||
self.assertHTMLEqual(
|
|
||||||
form.as_p(),
|
|
||||||
'<p><input id="id_custom" name="custom" type="text" required> custom</p>\n'
|
|
||||||
'<input id="id_hidden1" name="hidden1" type="hidden">'
|
|
||||||
'<input id="id_hidden2" name="hidden2" type="hidden"><hr><hr>'
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_error_dict(self):
|
def test_error_dict(self):
|
||||||
class MyForm(Form):
|
class MyForm(Form):
|
||||||
foo = CharField()
|
foo = CharField()
|
||||||
|
@ -3377,30 +3268,6 @@ Password: <input type="password" name="password" required>
|
||||||
<input id="id_last_name" name="last_name" type="text" value="Lennon" required></td></tr>"""
|
<input id="id_last_name" name="last_name" type="text" value="Lennon" required></td></tr>"""
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_errorlist_override(self):
|
|
||||||
class DivErrorList(ErrorList):
|
|
||||||
def __str__(self):
|
|
||||||
return self.as_divs()
|
|
||||||
|
|
||||||
def as_divs(self):
|
|
||||||
if not self:
|
|
||||||
return ''
|
|
||||||
return '<div class="errorlist">%s</div>' % ''.join(
|
|
||||||
'<div class="error">%s</div>' % e for e in self)
|
|
||||||
|
|
||||||
class CommentForm(Form):
|
|
||||||
name = CharField(max_length=50, required=False)
|
|
||||||
email = EmailField()
|
|
||||||
comment = CharField()
|
|
||||||
|
|
||||||
data = {'email': 'invalid'}
|
|
||||||
f = CommentForm(data, auto_id=False, error_class=DivErrorList)
|
|
||||||
self.assertHTMLEqual(f.as_p(), """<p>Name: <input type="text" name="name" maxlength="50"></p>
|
|
||||||
<div class="errorlist"><div class="error">Enter a valid email address.</div></div>
|
|
||||||
<p>Email: <input type="email" name="email" value="invalid" required></p>
|
|
||||||
<div class="errorlist"><div class="error">This field is required.</div></div>
|
|
||||||
<p>Comment: <input type="text" name="comment" required></p>""")
|
|
||||||
|
|
||||||
def test_error_escaping(self):
|
def test_error_escaping(self):
|
||||||
class TestForm(Form):
|
class TestForm(Form):
|
||||||
hidden = CharField(widget=HiddenInput(), required=False)
|
hidden = CharField(widget=HiddenInput(), required=False)
|
||||||
|
@ -4045,3 +3912,40 @@ class TemplateTests(SimpleTestCase):
|
||||||
"VALID: [('password1', 'secret'), ('password2', 'secret'), "
|
"VALID: [('password1', 'secret'), ('password2', 'secret'), "
|
||||||
"('username', 'adrian')]",
|
"('username', 'adrian')]",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class OverrideTests(SimpleTestCase):
|
||||||
|
def test_use_custom_template(self):
|
||||||
|
class Person(Form):
|
||||||
|
first_name = CharField()
|
||||||
|
template_name = 'forms_tests/form_snippet.html'
|
||||||
|
|
||||||
|
t = Template('{{ form }}')
|
||||||
|
html = t.render(Context({'form': Person()}))
|
||||||
|
expected = """
|
||||||
|
<div class="fieldWrapper"><label for="id_first_name">First name:</label>
|
||||||
|
<input type="text" name="first_name" required id="id_first_name"></div>
|
||||||
|
"""
|
||||||
|
self.assertHTMLEqual(html, expected)
|
||||||
|
|
||||||
|
def test_errorlist_override(self):
|
||||||
|
class CustomErrorList(ErrorList):
|
||||||
|
template_name = 'forms_tests/error.html'
|
||||||
|
|
||||||
|
class CommentForm(Form):
|
||||||
|
name = CharField(max_length=50, required=False)
|
||||||
|
email = EmailField()
|
||||||
|
comment = CharField()
|
||||||
|
|
||||||
|
data = {'email': 'invalid'}
|
||||||
|
f = CommentForm(data, auto_id=False, error_class=CustomErrorList)
|
||||||
|
self.assertHTMLEqual(
|
||||||
|
f.as_p(),
|
||||||
|
'<p>Name: <input type="text" name="name" maxlength="50"></p>'
|
||||||
|
'<div class="errorlist">'
|
||||||
|
'<div class="error">Enter a valid email address.</div></div>'
|
||||||
|
'<p>Email: <input type="email" name="email" value="invalid" required></p>'
|
||||||
|
'<div class="errorlist">'
|
||||||
|
'<div class="error">This field is required.</div></div>'
|
||||||
|
'<p>Comment: <input type="text" name="comment" required></p>',
|
||||||
|
)
|
||||||
|
|
|
@ -11,6 +11,7 @@ from django.forms.formsets import BaseFormSet, all_valid, formset_factory
|
||||||
from django.forms.utils import ErrorList
|
from django.forms.utils import ErrorList
|
||||||
from django.forms.widgets import HiddenInput
|
from django.forms.widgets import HiddenInput
|
||||||
from django.test import SimpleTestCase
|
from django.test import SimpleTestCase
|
||||||
|
from tests.forms_tests.tests import test_all_form_renderers
|
||||||
|
|
||||||
|
|
||||||
class Choice(Form):
|
class Choice(Form):
|
||||||
|
@ -47,6 +48,7 @@ class CustomKwargForm(Form):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@test_all_form_renderers()
|
||||||
class FormsFormsetTestCase(SimpleTestCase):
|
class FormsFormsetTestCase(SimpleTestCase):
|
||||||
|
|
||||||
def make_choiceformset(
|
def make_choiceformset(
|
||||||
|
@ -1288,7 +1290,32 @@ class FormsFormsetTestCase(SimpleTestCase):
|
||||||
self.assertIs(formset._should_delete_form(formset.forms[1]), False)
|
self.assertIs(formset._should_delete_form(formset.forms[1]), False)
|
||||||
self.assertIs(formset._should_delete_form(formset.forms[2]), False)
|
self.assertIs(formset._should_delete_form(formset.forms[2]), False)
|
||||||
|
|
||||||
|
def test_custom_renderer(self):
|
||||||
|
"""
|
||||||
|
A custom renderer passed to a formset_factory() is passed to all forms
|
||||||
|
and ErrorList.
|
||||||
|
"""
|
||||||
|
from django.forms.renderers import Jinja2
|
||||||
|
renderer = Jinja2()
|
||||||
|
data = {
|
||||||
|
'choices-TOTAL_FORMS': '2',
|
||||||
|
'choices-INITIAL_FORMS': '0',
|
||||||
|
'choices-MIN_NUM_FORMS': '0',
|
||||||
|
'choices-0-choice': 'Zero',
|
||||||
|
'choices-0-votes': '',
|
||||||
|
'choices-1-choice': 'One',
|
||||||
|
'choices-1-votes': '',
|
||||||
|
}
|
||||||
|
ChoiceFormSet = formset_factory(Choice, renderer=renderer)
|
||||||
|
formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
|
||||||
|
self.assertEqual(formset.renderer, renderer)
|
||||||
|
self.assertEqual(formset.forms[0].renderer, renderer)
|
||||||
|
self.assertEqual(formset.management_form.renderer, renderer)
|
||||||
|
self.assertEqual(formset.non_form_errors().renderer, renderer)
|
||||||
|
self.assertEqual(formset.empty_form.renderer, renderer)
|
||||||
|
|
||||||
|
|
||||||
|
@test_all_form_renderers()
|
||||||
class FormsetAsTagTests(SimpleTestCase):
|
class FormsetAsTagTests(SimpleTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
data = {
|
data = {
|
||||||
|
@ -1345,6 +1372,7 @@ class ArticleForm(Form):
|
||||||
ArticleFormSet = formset_factory(ArticleForm)
|
ArticleFormSet = formset_factory(ArticleForm)
|
||||||
|
|
||||||
|
|
||||||
|
@test_all_form_renderers()
|
||||||
class TestIsBoundBehavior(SimpleTestCase):
|
class TestIsBoundBehavior(SimpleTestCase):
|
||||||
def test_no_data_error(self):
|
def test_no_data_error(self):
|
||||||
formset = ArticleFormSet({})
|
formset = ArticleFormSet({})
|
||||||
|
@ -1359,7 +1387,7 @@ class TestIsBoundBehavior(SimpleTestCase):
|
||||||
)
|
)
|
||||||
self.assertEqual(formset.errors, [])
|
self.assertEqual(formset.errors, [])
|
||||||
# Can still render the formset.
|
# Can still render the formset.
|
||||||
self.assertEqual(
|
self.assertHTMLEqual(
|
||||||
str(formset),
|
str(formset),
|
||||||
'<tr><td colspan="2">'
|
'<tr><td colspan="2">'
|
||||||
'<ul class="errorlist nonfield">'
|
'<ul class="errorlist nonfield">'
|
||||||
|
@ -1390,7 +1418,7 @@ class TestIsBoundBehavior(SimpleTestCase):
|
||||||
)
|
)
|
||||||
self.assertEqual(formset.errors, [])
|
self.assertEqual(formset.errors, [])
|
||||||
# Can still render the formset.
|
# Can still render the formset.
|
||||||
self.assertEqual(
|
self.assertHTMLEqual(
|
||||||
str(formset),
|
str(formset),
|
||||||
'<tr><td colspan="2">'
|
'<tr><td colspan="2">'
|
||||||
'<ul class="errorlist nonfield">'
|
'<ul class="errorlist nonfield">'
|
||||||
|
|
|
@ -4,8 +4,10 @@ from django.forms import (
|
||||||
from django.test import SimpleTestCase
|
from django.test import SimpleTestCase
|
||||||
from django.utils import translation
|
from django.utils import translation
|
||||||
from django.utils.translation import gettext_lazy
|
from django.utils.translation import gettext_lazy
|
||||||
|
from tests.forms_tests.tests import test_all_form_renderers
|
||||||
|
|
||||||
|
|
||||||
|
@test_all_form_renderers()
|
||||||
class FormsI18nTests(SimpleTestCase):
|
class FormsI18nTests(SimpleTestCase):
|
||||||
def test_lazy_labels(self):
|
def test_lazy_labels(self):
|
||||||
class SomeForm(Form):
|
class SomeForm(Form):
|
||||||
|
|
|
@ -5,6 +5,7 @@ from django.db import models
|
||||||
from django.forms import CharField, FileField, Form, ModelForm
|
from django.forms import CharField, FileField, Form, ModelForm
|
||||||
from django.forms.models import ModelFormMetaclass
|
from django.forms.models import ModelFormMetaclass
|
||||||
from django.test import SimpleTestCase, TestCase
|
from django.test import SimpleTestCase, TestCase
|
||||||
|
from tests.forms_tests.tests import test_all_form_renderers
|
||||||
|
|
||||||
from ..models import (
|
from ..models import (
|
||||||
BoundaryModel, ChoiceFieldModel, ChoiceModel, ChoiceOptionModel, Defaults,
|
BoundaryModel, ChoiceFieldModel, ChoiceModel, ChoiceOptionModel, Defaults,
|
||||||
|
@ -283,6 +284,7 @@ class ManyToManyExclusionTestCase(TestCase):
|
||||||
self.assertEqual([obj.pk for obj in form.instance.multi_choice_int.all()], data['multi_choice_int'])
|
self.assertEqual([obj.pk for obj in form.instance.multi_choice_int.all()], data['multi_choice_int'])
|
||||||
|
|
||||||
|
|
||||||
|
@test_all_form_renderers()
|
||||||
class EmptyLabelTestCase(TestCase):
|
class EmptyLabelTestCase(TestCase):
|
||||||
def test_empty_field_char(self):
|
def test_empty_field_char(self):
|
||||||
f = EmptyCharLabelChoiceForm()
|
f = EmptyCharLabelChoiceForm()
|
||||||
|
|
|
@ -1994,3 +1994,22 @@ class TestModelFormsetOverridesTroughFormMeta(TestCase):
|
||||||
self.assertEqual(len(formset), 2)
|
self.assertEqual(len(formset), 2)
|
||||||
self.assertNotIn('DELETE', formset.forms[0].fields)
|
self.assertNotIn('DELETE', formset.forms[0].fields)
|
||||||
self.assertNotIn('DELETE', formset.forms[1].fields)
|
self.assertNotIn('DELETE', formset.forms[1].fields)
|
||||||
|
|
||||||
|
def test_inlineformset_factory_passes_renderer(self):
|
||||||
|
from django.forms.renderers import Jinja2
|
||||||
|
renderer = Jinja2()
|
||||||
|
BookFormSet = inlineformset_factory(
|
||||||
|
Author,
|
||||||
|
Book,
|
||||||
|
fields='__all__',
|
||||||
|
renderer=renderer,
|
||||||
|
)
|
||||||
|
formset = BookFormSet()
|
||||||
|
self.assertEqual(formset.renderer, renderer)
|
||||||
|
|
||||||
|
def test_modelformset_factory_passes_renderer(self):
|
||||||
|
from django.forms.renderers import Jinja2
|
||||||
|
renderer = Jinja2()
|
||||||
|
BookFormSet = modelformset_factory(Author, fields='__all__', renderer=renderer)
|
||||||
|
formset = BookFormSet()
|
||||||
|
self.assertEqual(formset.renderer, renderer)
|
||||||
|
|
Loading…
Reference in New Issue