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:
David Smith 2021-09-10 08:06:01 +01:00 committed by Mariusz Felisiak
parent 5353e7c250
commit 456466d932
56 changed files with 1047 additions and 329 deletions

View File

@ -1,11 +1,10 @@
import re
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.utils.functional import cached_property
from django.utils.html import conditional_escape, format_html, html_safe
from django.utils.safestring import mark_safe
from django.utils.html import format_html, html_safe
from django.utils.translation import gettext_lazy as _
__all__ = ('BoundField',)
@ -75,7 +74,7 @@ class BoundField:
"""
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):
"""
@ -177,11 +176,14 @@ class BoundField:
attrs['class'] += ' ' + self.form.required_css_class
else:
attrs['class'] = self.form.required_css_class
attrs = flatatt(attrs) if attrs else ''
contents = format_html('<label{}>{}</label>', attrs, contents)
else:
contents = conditional_escape(contents)
return mark_safe(contents)
context = {
'form': self.form,
'field': self,
'label': contents,
'attrs': attrs,
'use_tag': bool(id_),
}
return self.form.render(self.form.template_name_label, context)
def css_classes(self, extra_classes=None):
"""

View File

@ -4,15 +4,17 @@ Form classes
import copy
import datetime
import warnings
from django.core.exceptions import NON_FIELD_ERRORS, ValidationError
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.utils.datastructures import MultiValueDict
from django.utils.deprecation import RemovedInDjango50Warning
from django.utils.functional import cached_property
from django.utils.html import conditional_escape, html_safe
from django.utils.safestring import mark_safe
from django.utils.html import conditional_escape
from django.utils.safestring import SafeString, mark_safe
from django.utils.translation import gettext as _
from .renderers import get_default_renderer
@ -49,8 +51,7 @@ class DeclarativeFieldsMetaclass(MediaDefiningClass):
return new_class
@html_safe
class BaseForm:
class BaseForm(RenderableFormMixin):
"""
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
@ -62,6 +63,12 @@ class BaseForm:
prefix = None
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,
initial=None, error_class=ErrorList, label_suffix=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
self.fields = fields
def __str__(self):
return self.as_table()
def __repr__(self):
if self._errors is None:
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):
"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.
top_errors = self.non_field_errors().copy()
output, hidden_fields = [], []
@ -282,35 +292,37 @@ class BaseForm:
output.append(str_hidden)
return mark_safe('\n'.join(output))
def as_table(self):
"Return this form rendered as HTML <tr>s -- excluding the <table></table>."
return self._html_output(
normal_row='<tr%(html_class_attr)s><th>%(label)s</th><td>%(errors)s%(field)s%(help_text)s</td></tr>',
error_row='<tr><td colspan="2">%s</td></tr>',
row_ender='</td></tr>',
help_text_html='<br><span class="helptext">%s</span>',
errors_on_separate_row=False,
)
def as_ul(self):
"Return this form rendered as HTML <li>s -- excluding the <ul></ul>."
return self._html_output(
normal_row='<li%(html_class_attr)s>%(errors)s%(label)s %(field)s%(help_text)s</li>',
error_row='<li>%s</li>',
row_ender='</li>',
help_text_html=' <span class="helptext">%s</span>',
errors_on_separate_row=False,
)
def as_p(self):
"Return this form rendered as HTML <p>s."
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,
)
def get_context(self):
fields = []
hidden_fields = []
top_errors = self.non_field_errors().copy()
for name, bf in self._bound_items():
bf_errors = self.error_class(bf.errors, renderer=self.renderer)
if bf.is_hidden:
if bf_errors:
top_errors += [
_('(Hidden field %(name)s) %(error)s') % {'name': name, 'error': str(e)}
for e in bf_errors
]
hidden_fields.append(bf)
else:
errors_str = str(bf_errors)
# RemovedInDjango50Warning.
if not isinstance(errors_str, SafeString):
warnings.warn(
f'Returning a plain string from '
f'{self.error_class.__name__} is deprecated. Please '
f'customize via the template system instead.',
RemovedInDjango50Warning,
)
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):
"""
@ -318,7 +330,10 @@ class BaseForm:
field -- i.e., from Form.clean(). Return an empty ErrorList if there
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):
"""
@ -360,9 +375,9 @@ class BaseForm:
raise ValueError(
"'%s' has no field named '%s'." % (self.__class__.__name__, field))
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:
self._errors[field] = self.error_class()
self._errors[field] = self.error_class(renderer=self.renderer)
self._errors[field].extend(error_list)
if field in self.cleaned_data:
del self.cleaned_data[field]

View File

@ -1,11 +1,10 @@
from django.core.exceptions import ValidationError
from django.forms import Form
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.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
__all__ = ('BaseFormSet', 'formset_factory', 'all_valid')
@ -50,8 +49,7 @@ class ManagementForm(Form):
return cleaned_data
@html_safe
class BaseFormSet:
class BaseFormSet(RenderableFormMixin):
"""
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.'
),
}
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,
initial=None, error_class=ErrorList, form_kwargs=None,
@ -85,9 +87,6 @@ class BaseFormSet:
messages.update(error_messages)
self.error_messages = messages
def __str__(self):
return self.as_table()
def __iter__(self):
"""Yield the forms in the order they should be rendered."""
return iter(self.forms)
@ -110,15 +109,20 @@ class BaseFormSet:
def management_form(self):
"""Return the ManagementForm instance for this FormSet."""
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()
else:
form = ManagementForm(auto_id=self.auto_id, prefix=self.prefix, initial={
TOTAL_FORM_COUNT: self.total_form_count(),
INITIAL_FORM_COUNT: self.initial_form_count(),
MIN_NUM_FORM_COUNT: self.min_num,
MAX_NUM_FORM_COUNT: self.max_num
})
form = ManagementForm(
auto_id=self.auto_id,
prefix=self.prefix,
initial={
TOTAL_FORM_COUNT: self.total_form_count(),
INITIAL_FORM_COUNT: self.initial_form_count(),
MIN_NUM_FORM_COUNT: self.min_num,
MAX_NUM_FORM_COUNT: self.max_num,
},
renderer=self.renderer,
)
return form
def total_form_count(self):
@ -177,6 +181,7 @@ class BaseFormSet:
# incorrect validation for extra, optional, and deleted
# forms in the formset.
'use_required_attribute': False,
'renderer': self.renderer,
}
if self.is_bound:
defaults['data'] = self.data
@ -212,7 +217,8 @@ class BaseFormSet:
prefix=self.add_prefix('__prefix__'),
empty_permitted=True,
use_required_attribute=False,
**self.get_form_kwargs(None)
**self.get_form_kwargs(None),
renderer=self.renderer,
)
self.add_fields(form, None)
return form
@ -338,7 +344,7 @@ class BaseFormSet:
self._non_form_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
if not self.is_bound: # Stop further processing.
@ -387,7 +393,8 @@ class BaseFormSet:
except ValidationError as e:
self._non_form_errors = self.error_class(
e.error_list,
error_class='nonform'
error_class='nonform',
renderer=self.renderer,
)
def clean(self):
@ -450,29 +457,14 @@ class BaseFormSet:
else:
return self.empty_form.media
def as_table(self):
"Return this formset rendered as HTML <tr>s -- excluding the <table></table>."
# 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 get_context(self):
return {'formset': self}
def 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):
can_delete_extra=True, renderer=None):
"""Return a FormSet for the given form class."""
if min_num is None:
min_num = DEFAULT_MIN_NUM
@ -498,6 +490,7 @@ def formset_factory(form, formset=BaseFormSet, extra=1, can_order=False,
'absolute_max': absolute_max,
'validate_min': validate_min,
'validate_max': validate_max,
'renderer': renderer or get_default_renderer(),
}
return type(form.__name__ + 'FormSet', (formset,), attrs)

View File

@ -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 %}

View File

@ -0,0 +1 @@
{% include "django/forms/table.html" %}

View File

@ -0,0 +1 @@
{% include "django/forms/errors/dict/ul.html" %}

View File

@ -0,0 +1,3 @@
{% for field, errors in errors %}* {{ field }}
{% for error in errors %} * {{ error }}
{% endfor %}{% endfor %}

View File

@ -0,0 +1 @@
{% if errors %}<ul class="{{ error_class }}">{% for field, error in errors %}<li>{{ field }}{{ error }}</li>{% endfor %}</ul>{% endif %}

View File

@ -0,0 +1 @@
{% include "django/forms/errors/list/ul.html" %}

View File

@ -0,0 +1,2 @@
{% for error in errors %}* {{ error }}
{% endfor %}

View File

@ -0,0 +1 @@
{% if errors %}<ul class="{{ error_class }}">{% for error in errors %}<li>{{ error }}</li>{% endfor %}</ul>{% endif %}

View File

@ -0,0 +1 @@
{{ formset.management_form }}{% for form in formset %}{{ form }}{% endfor %}

View File

@ -0,0 +1 @@
{{ formset.management_form }}{% for form in formset %}{{ form.as_p() }}{% endfor %}

View File

@ -0,0 +1 @@
{{ formset.management_form }}{% for form in formset %}{{ form.as_table() }}{% endfor %}

View File

@ -0,0 +1 @@
{{ formset.management_form }}{% for form in formset %}{{ form.as_ul() }}{% endfor %}

View File

@ -0,0 +1 @@
{% if use_tag %}<label{% if attrs %}{% include 'django/forms/attrs.html' %}{% endif %}>{{ label }}</label>{% else %}{{ label }}{% endif %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -718,7 +718,10 @@ class BaseModelFormSet(BaseFormSet):
# poke error messages into the right places and mark
# the form as invalid
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
for field in unique_check:
if field in form.cleaned_data:
@ -747,7 +750,10 @@ class BaseModelFormSet(BaseFormSet):
# poke error messages into the right places and mark
# the form as invalid
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
del form.cleaned_data[field]
# 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,
labels=None, help_texts=None, error_messages=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."""
meta = getattr(form, 'Meta', None)
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,
can_order=can_order, can_delete=can_delete,
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
return FormSet
@ -1069,7 +1076,7 @@ def inlineformset_factory(parent_model, model, form=ModelForm,
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):
absolute_max=None, can_delete_extra=True, renderer=None):
"""
Return an ``InlineFormSet`` for the given kwargs.
@ -1101,6 +1108,7 @@ def inlineformset_factory(parent_model, model, form=ModelForm,
'field_classes': field_classes,
'absolute_max': absolute_max,
'can_delete_extra': can_delete_extra,
'renderer': renderer,
}
FormSet = modelformset_factory(model, **kwargs)
FormSet.fk = fk

View File

@ -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 %}

View File

@ -0,0 +1 @@
{% include "django/forms/table.html" %}

View File

@ -0,0 +1 @@
{% include "django/forms/errors/dict/ul.html" %}

View File

@ -0,0 +1,3 @@
{% for field, errors in errors %}* {{ field }}
{% for error in errors %} * {{ error }}
{% endfor %}{% endfor %}

View File

@ -0,0 +1 @@
{% if errors %}<ul class="{{ error_class }}">{% for field, error in errors %}<li>{{ field }}{{ error }}</li>{% endfor %}</ul>{% endif %}

View File

@ -0,0 +1 @@
{% include "django/forms/errors/list/ul.html" %}

View File

@ -0,0 +1,2 @@
{% for error in errors %}* {{ error }}
{% endfor %}

View File

@ -0,0 +1 @@
{% if errors %}<ul class="{{ error_class }}">{% for error in errors %}<li>{{ error }}</li>{% endfor %}</ul>{% endif %}

View File

@ -0,0 +1 @@
{{ formset.management_form }}{% for form in formset %}{{ form }}{% endfor %}

View File

@ -0,0 +1 @@
{{ formset.management_form }}{% for form in formset %}{{ form.as_p }}{% endfor %}

View File

@ -0,0 +1 @@
{{ formset.management_form }}{% for form in formset %}{{ form.as_table }}{% endfor %}

View File

@ -0,0 +1 @@
{{ formset.management_form }}{% for form in formset %}{{ form.as_ul }}{% endfor %}

View File

@ -0,0 +1 @@
{% if use_tag %}<label{% include 'django/forms/attrs.html' %}>{{ label }}</label>{% else %}{{ label }}{% endif %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -1,10 +1,12 @@
import json
from collections import UserList
from collections import UserDict, UserList
from django.conf import settings
from django.core.exceptions import ValidationError
from django.forms.renderers import get_default_renderer
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 _
@ -41,53 +43,90 @@ def flatatt(attrs):
)
@html_safe
class ErrorDict(dict):
class RenderableMixin:
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.
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):
return {f: e.as_data() for f, e in self.items()}
def get_json_data(self, escape_html=False):
return {f: e.get_json_data(escape_html) for f, e in self.items()}
def as_json(self, escape_html=False):
return json.dumps(self.get_json_data(escape_html))
def as_ul(self):
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()
def get_context(self):
return {
'errors': self.items(),
'error_class': 'errorlist',
}
@html_safe
class ErrorList(UserList, list):
class ErrorList(UserList, list, RenderableErrorMixin):
"""
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)
if error_class is None:
self.error_class = 'errorlist'
else:
self.error_class = 'errorlist {}'.format(error_class)
self.renderer = renderer or get_default_renderer()
def as_data(self):
return ValidationError(self.data).error_list
@ -107,24 +146,11 @@ class ErrorList(UserList, list):
})
return errors
def as_json(self, escape_html=False):
return json.dumps(self.get_json_data(escape_html))
def as_ul(self):
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 get_context(self):
return {
'errors': self,
'error_class': self.error_class,
}
def __repr__(self):
return repr(list(self))

View File

@ -57,6 +57,11 @@ details on these changes.
* The ``django.contrib.gis.admin.GeoModelAdmin`` and ``OSMGeoAdmin`` classes
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:
4.1

View File

@ -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
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()``
----------
.. method:: Form.as_p()
``as_p()`` renders the form as a series of ``<p>`` tags, with each ``<p>``
containing one field::
``as_p()`` renders the form using the template assigned to the forms
``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.as_p()
@ -542,10 +570,12 @@ containing one field::
.. method:: Form.as_ul()
``as_ul()`` renders the form as a series of ``<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::
``as_ul()`` renders the form using the template assigned to the forms
``template_name_ul`` attribute, by default this template is
``'django/forms/ul.html'``. This template renders the form as a series of
``<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.as_ul()
@ -561,9 +591,10 @@ flexibility::
.. method:: Form.as_table()
Finally, ``as_table()`` outputs the form as an HTML ``<table>``. This is
exactly the same as ``print``. In fact, when you ``print`` a form object,
it calls its ``as_table()`` method behind the scenes::
Finally, ``as_table()`` renders the form using the template assigned to the
forms ``template_name_table`` attribute, by default this template is
``'django/forms/table.html'``. This template outputs the form as an HTML
``<table>``::
>>> f = ContactForm()
>>> 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_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:
Styling required or erroneous form rows
@ -834,25 +896,99 @@ method you're using::
Customizing the error list format
---------------------------------
By default, forms use ``django.forms.utils.ErrorList`` to format validation
errors. If you'd like to use an alternate class for displaying errors, you can
pass that in at construction time::
.. class:: ErrorList(initlist=None, error_class=None, renderer=None)
>>> from django.forms.utils import ErrorList
>>> 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])
>>> f = ContactForm(data, auto_id=False, error_class=DivErrorList)
>>> f.as_p()
<div class="errorlist"><div class="error">This field is required.</div></div>
<p>Subject: <input type="text" name="subject" maxlength="100" required></p>
<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>
<p>Sender: <input type="email" name="sender" value="invalid email address" required></p>
<p>Cc myself: <input checked type="checkbox" name="cc_myself"></p>
By default, forms use ``django.forms.utils.ErrorList`` to format validation
errors. ``ErrorList`` is a list like object where ``initlist`` is the
list of errors. In addition this class has the following attributes and
methods.
.. attribute:: error_class
The CSS classes to be used when rendering the error list. Any provided
classes are added to the default ``errorlist`` class.
.. attribute:: renderer
.. versionadded:: 4.0
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
====================
@ -1086,12 +1222,16 @@ Methods of ``BoundField``
attributes for the ``<label>`` tag.
The HTML that's generated includes the form's
:attr:`~django.forms.Form.label_suffix` (a colon, by default) or, if set, the
current field's :attr:`~django.forms.Field.label_suffix`. The optional
:attr:`~django.forms.Form.label_suffix` (a colon, by default) or, if set,
the current field's :attr:`~django.forms.Field.label_suffix`. The optional
``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
fields. If you need to do this in a template, you could write a custom
filter to allow passing parameters to ``label_tag``.
suffix. For example, you can use an empty string to hide the label on
selected fields. The label is rendered using the template specified by the
forms :attr:`.Form.template_name_label`.
.. versionchanged:: 4.0
The label is now rendered using the template engine.
.. method:: BoundField.value()

View File

@ -11,7 +11,7 @@ Formset API reference. For introductory material about formsets, see the
``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.
@ -20,3 +20,7 @@ Formset API reference. For introductory material about formsets, see the
.. versionchanged:: 3.2
The ``absolute_max`` and ``can_delete_extra`` arguments were added.
.. versionchanged:: 4.0
The ``renderer`` argument was added.

View File

@ -52,7 +52,7 @@ Model Form API reference. For introductory material about model forms, see the
``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.
@ -63,9 +63,9 @@ Model Form API reference. For introductory material about model forms, see the
Arguments ``formset``, ``extra``, ``can_delete``, ``can_order``,
``max_num``, ``validate_max``, ``min_num``, ``validate_min``,
``absolute_max``, and ``can_delete_extra`` are passed through to
:func:`~django.forms.formsets.formset_factory`. See :doc:`formsets
</topics/forms/formsets>` for details.
``absolute_max``, ``can_delete_extra``, and ``renderer`` are passed
through to :func:`~django.forms.formsets.formset_factory`. See
:doc:`formsets </topics/forms/formsets>` for details.
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.
.. versionchanged:: 4.0
The ``renderer`` argument was added.
``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
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
The ``absolute_max`` and ``can_delete_extra`` arguments were added.
.. versionchanged:: 4.0
The ``renderer`` argument was added.

View File

@ -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
apps can provide templates in a ``jinja2`` directory.
To use this backend, all the widgets in your project and its third-party apps
must have Jinja2 templates. Unless you provide your own Jinja2 templates for
widgets that don't have any, you can't use this renderer. For example,
:mod:`django.contrib.admin` doesn't include Jinja2 templates for its widgets
due to their usage of Django template tags.
To use this backend, all the forms and widgets in your project and its
third-party apps must have Jinja2 templates. Unless you provide your own Jinja2
templates for widgets that don't have any, you can't use this renderer. For
example, :mod:`django.contrib.admin` doesn't include Jinja2 templates for its
widgets due to their usage of Django template tags.
``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
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
=====================================
@ -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`
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

View File

@ -1671,8 +1671,9 @@ generate correct URLs when ``SCRIPT_NAME`` is not ``/``.
Default: ``'``:class:`django.forms.renderers.DjangoTemplates`\ ``'``
The class that renders form widgets. It must implement :ref:`the low-level
render API <low-level-widget-render-api>`. Included form renderers are:
The class that renders forms and form widgets. It must implement
:ref:`the low-level render API <low-level-widget-render-api>`. Included form
renderers are:
* ``'``:class:`django.forms.renderers.DjangoTemplates`\ ``'``
* ``'``:class:`django.forms.renderers.Jinja2`\ ``'``

View File

@ -115,6 +115,17 @@ in Django <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
--------------
@ -735,6 +746,12 @@ Miscellaneous
are deprecated. Use :class:`~django.contrib.admin.ModelAdmin` and
: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
=======================

View File

@ -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
<multiple-formsets-in-view>`.
.. _formset-rendering:
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
``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::
@ -821,7 +904,17 @@ deal with the management form:
</table>
</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:

View File

@ -733,12 +733,17 @@ Reusable form templates
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
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
# In your form template:
{% include "form_snippet.html" %}
# In your template:
{{ form }}
# In form_snippet.html:
{% for field in form %}
@ -748,16 +753,15 @@ using the :ttag:`include` tag to reuse it in other templates:
</div>
{% endfor %}
If the form object passed to a template has a different name within the
context, you can alias it using the ``with`` argument of the :ttag:`include`
tag:
In your form::
.. 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
:ref:`inclusion tag<howto-custom-template-tags-inclusion-tags>`.
Template rendering of forms was added.
Further topics
==============

View File

@ -6374,7 +6374,7 @@ class AdminViewOnSiteTests(TestCase):
response, 'inline_admin_formset', 0, None,
['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):
self.assertFormsetError(response, 'inline_admin_formset', None, None, ['Error'])

View File

@ -0,0 +1 @@
{% if errors %}<div class="errorlist">{% for error in errors %}<div class="error">{{ error }}</div>{% endfor %}</div>{% endif %}

View File

@ -0,0 +1,6 @@
{% for field in form %}
<div class="fieldWrapper">
{{ field.errors }}
{{ field.label_tag }} {{ field }}
</div>
{% endfor %}

View File

@ -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

View File

@ -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>'
)

View File

@ -23,6 +23,7 @@ from django.test import SimpleTestCase
from django.test.utils import override_settings
from django.utils.datastructures import MultiValueDict
from django.utils.safestring import mark_safe
from tests.forms_tests.tests import test_all_form_renderers
class FrameworkForm(Form):
@ -55,6 +56,7 @@ class MultiValueDictLike(dict):
return [self[key]]
@test_all_form_renderers()
class FormsTestCase(SimpleTestCase):
# 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).
@ -3077,117 +3079,6 @@ Password: <input type="password" name="password" required>
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):
class MyForm(Form):
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>"""
)
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):
class TestForm(Form):
hidden = CharField(widget=HiddenInput(), required=False)
@ -4045,3 +3912,40 @@ class TemplateTests(SimpleTestCase):
"VALID: [('password1', 'secret'), ('password2', 'secret'), "
"('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>',
)

View File

@ -11,6 +11,7 @@ from django.forms.formsets import BaseFormSet, all_valid, formset_factory
from django.forms.utils import ErrorList
from django.forms.widgets import HiddenInput
from django.test import SimpleTestCase
from tests.forms_tests.tests import test_all_form_renderers
class Choice(Form):
@ -47,6 +48,7 @@ class CustomKwargForm(Form):
super().__init__(*args, **kwargs)
@test_all_form_renderers()
class FormsFormsetTestCase(SimpleTestCase):
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[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):
def setUp(self):
data = {
@ -1345,6 +1372,7 @@ class ArticleForm(Form):
ArticleFormSet = formset_factory(ArticleForm)
@test_all_form_renderers()
class TestIsBoundBehavior(SimpleTestCase):
def test_no_data_error(self):
formset = ArticleFormSet({})
@ -1359,7 +1387,7 @@ class TestIsBoundBehavior(SimpleTestCase):
)
self.assertEqual(formset.errors, [])
# Can still render the formset.
self.assertEqual(
self.assertHTMLEqual(
str(formset),
'<tr><td colspan="2">'
'<ul class="errorlist nonfield">'
@ -1390,7 +1418,7 @@ class TestIsBoundBehavior(SimpleTestCase):
)
self.assertEqual(formset.errors, [])
# Can still render the formset.
self.assertEqual(
self.assertHTMLEqual(
str(formset),
'<tr><td colspan="2">'
'<ul class="errorlist nonfield">'

View File

@ -4,8 +4,10 @@ from django.forms import (
from django.test import SimpleTestCase
from django.utils import translation
from django.utils.translation import gettext_lazy
from tests.forms_tests.tests import test_all_form_renderers
@test_all_form_renderers()
class FormsI18nTests(SimpleTestCase):
def test_lazy_labels(self):
class SomeForm(Form):

View File

@ -5,6 +5,7 @@ from django.db import models
from django.forms import CharField, FileField, Form, ModelForm
from django.forms.models import ModelFormMetaclass
from django.test import SimpleTestCase, TestCase
from tests.forms_tests.tests import test_all_form_renderers
from ..models import (
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'])
@test_all_form_renderers()
class EmptyLabelTestCase(TestCase):
def test_empty_field_char(self):
f = EmptyCharLabelChoiceForm()

View File

@ -1994,3 +1994,22 @@ class TestModelFormsetOverridesTroughFormMeta(TestCase):
self.assertEqual(len(formset), 2)
self.assertNotIn('DELETE', formset.forms[0].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)