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 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):
""" """

View File

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

View File

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

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

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

View File

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

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 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()

View File

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

View File

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

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

View File

@ -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`\ ``'``

View File

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

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

View File

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

View File

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

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.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>',
)

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.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">'

View File

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

View File

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

View File

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