',
- row_ender='',
- help_text_html=' %s',
- errors_on_separate_row=False,
- )
-
- def as_ul(self):
- "Return this form rendered as HTML
s -- excluding the
."
- return self._html_output(
- normal_row='
%(errors)s%(label)s %(field)s%(help_text)s
',
- error_row='
%s
',
- row_ender='',
- help_text_html=' %s',
- errors_on_separate_row=False,
- )
-
- def as_p(self):
- "Return this form rendered as HTML
s."
- return self._html_output(
- normal_row='
%(label)s %(field)s%(help_text)s
',
- error_row='%s',
- row_ender='
',
- help_text_html=' %s',
- 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]
diff --git a/django/forms/formsets.py b/django/forms/formsets.py
index 25f8378354..383ad6f6af 100644
--- a/django/forms/formsets.py
+++ b/django/forms/formsets.py
@@ -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
s -- excluding the
."
- # 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
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
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)
diff --git a/django/forms/jinja2/django/forms/attrs.html b/django/forms/jinja2/django/forms/attrs.html
new file mode 100644
index 0000000000..b7e3b8e018
--- /dev/null
+++ b/django/forms/jinja2/django/forms/attrs.html
@@ -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 %}
diff --git a/django/forms/jinja2/django/forms/default.html b/django/forms/jinja2/django/forms/default.html
new file mode 100644
index 0000000000..d034b60d57
--- /dev/null
+++ b/django/forms/jinja2/django/forms/default.html
@@ -0,0 +1 @@
+{% include "django/forms/table.html" %}
diff --git a/django/forms/jinja2/django/forms/errors/dict/default.html b/django/forms/jinja2/django/forms/errors/dict/default.html
new file mode 100644
index 0000000000..19e4fba33e
--- /dev/null
+++ b/django/forms/jinja2/django/forms/errors/dict/default.html
@@ -0,0 +1 @@
+{% include "django/forms/errors/dict/ul.html" %}
diff --git a/django/forms/jinja2/django/forms/errors/dict/text.txt b/django/forms/jinja2/django/forms/errors/dict/text.txt
new file mode 100644
index 0000000000..dc9fd80c99
--- /dev/null
+++ b/django/forms/jinja2/django/forms/errors/dict/text.txt
@@ -0,0 +1,3 @@
+{% for field, errors in errors %}* {{ field }}
+{% for error in errors %} * {{ error }}
+{% endfor %}{% endfor %}
diff --git a/django/forms/jinja2/django/forms/errors/dict/ul.html b/django/forms/jinja2/django/forms/errors/dict/ul.html
new file mode 100644
index 0000000000..c16fd65914
--- /dev/null
+++ b/django/forms/jinja2/django/forms/errors/dict/ul.html
@@ -0,0 +1 @@
+{% if errors %}
{% for field, error in errors %}
{{ field }}{{ error }}
{% endfor %}
{% endif %}
diff --git a/django/forms/jinja2/django/forms/errors/list/default.html b/django/forms/jinja2/django/forms/errors/list/default.html
new file mode 100644
index 0000000000..fccc328188
--- /dev/null
+++ b/django/forms/jinja2/django/forms/errors/list/default.html
@@ -0,0 +1 @@
+{% include "django/forms/errors/list/ul.html" %}
diff --git a/django/forms/jinja2/django/forms/errors/list/text.txt b/django/forms/jinja2/django/forms/errors/list/text.txt
new file mode 100644
index 0000000000..aa7f870b47
--- /dev/null
+++ b/django/forms/jinja2/django/forms/errors/list/text.txt
@@ -0,0 +1,2 @@
+{% for error in errors %}* {{ error }}
+{% endfor %}
diff --git a/django/forms/jinja2/django/forms/errors/list/ul.html b/django/forms/jinja2/django/forms/errors/list/ul.html
new file mode 100644
index 0000000000..752f7c2c8b
--- /dev/null
+++ b/django/forms/jinja2/django/forms/errors/list/ul.html
@@ -0,0 +1 @@
+{% if errors %}
{% for error in errors %}
{{ error }}
{% endfor %}
{% endif %}
diff --git a/django/forms/jinja2/django/forms/formsets/default.html b/django/forms/jinja2/django/forms/formsets/default.html
new file mode 100644
index 0000000000..d8284c5da1
--- /dev/null
+++ b/django/forms/jinja2/django/forms/formsets/default.html
@@ -0,0 +1 @@
+{{ formset.management_form }}{% for form in formset %}{{ form }}{% endfor %}
diff --git a/django/forms/jinja2/django/forms/formsets/p.html b/django/forms/jinja2/django/forms/formsets/p.html
new file mode 100644
index 0000000000..3ed889e6df
--- /dev/null
+++ b/django/forms/jinja2/django/forms/formsets/p.html
@@ -0,0 +1 @@
+{{ formset.management_form }}{% for form in formset %}{{ form.as_p() }}{% endfor %}
diff --git a/django/forms/jinja2/django/forms/formsets/table.html b/django/forms/jinja2/django/forms/formsets/table.html
new file mode 100644
index 0000000000..25033775b0
--- /dev/null
+++ b/django/forms/jinja2/django/forms/formsets/table.html
@@ -0,0 +1 @@
+{{ formset.management_form }}{% for form in formset %}{{ form.as_table() }}{% endfor %}
diff --git a/django/forms/jinja2/django/forms/formsets/ul.html b/django/forms/jinja2/django/forms/formsets/ul.html
new file mode 100644
index 0000000000..335e91e0e6
--- /dev/null
+++ b/django/forms/jinja2/django/forms/formsets/ul.html
@@ -0,0 +1 @@
+{{ formset.management_form }}{% for form in formset %}{{ form.as_ul() }}{% endfor %}
diff --git a/django/forms/jinja2/django/forms/label.html b/django/forms/jinja2/django/forms/label.html
new file mode 100644
index 0000000000..7ad5257a71
--- /dev/null
+++ b/django/forms/jinja2/django/forms/label.html
@@ -0,0 +1 @@
+{% if use_tag %}{% else %}{{ label }}{% endif %}
diff --git a/django/forms/jinja2/django/forms/p.html b/django/forms/jinja2/django/forms/p.html
new file mode 100644
index 0000000000..999c4d963a
--- /dev/null
+++ b/django/forms/jinja2/django/forms/p.html
@@ -0,0 +1,20 @@
+{{ errors }}
+{% if errors and not fields %}
+
{% for field in hidden_fields %}{{ field }}{% endfor %}
+{% endif %}
+{% for field, errors in fields %}
+ {{ errors }}
+
+ {% if field.label %}{{ field.label_tag() }}{% endif %}
+ {{ field }}
+ {% if field.help_text %}
+ {{ field.help_text }}
+ {% endif %}
+ {% if loop.last %}
+ {% for field in hidden_fields %}{{ field }}{% endfor %}
+ {% endif %}
+
+{% endfor %}
+{% if not fields and not errors %}
+ {% for field in hidden_fields %}{{ field }}{% endfor %}
+{% endif %}
diff --git a/django/forms/jinja2/django/forms/table.html b/django/forms/jinja2/django/forms/table.html
new file mode 100644
index 0000000000..92cd746a49
--- /dev/null
+++ b/django/forms/jinja2/django/forms/table.html
@@ -0,0 +1,29 @@
+{% if errors %}
+
+
+ {{ errors }}
+ {% if not fields %}
+ {% for field in hidden_fields %}{{ field }}{% endfor %}
+ {% endif %}
+
+
+{% endif %}
+{% for field, errors in fields %}
+
+
{% if field.label %}{{ field.label_tag() }}{% endif %}
+
+ {{ errors }}
+ {{ field }}
+ {% if field.help_text %}
+
+ {{ field.help_text }}
+ {% endif %}
+ {% if loop.last %}
+ {% for field in hidden_fields %}{{ field }}{% endfor %}
+ {% endif %}
+
+
+{% endfor %}
+{% if not fields and not errors %}
+ {% for field in hidden_fields %}{{ field }}{% endfor %}
+{% endif %}
diff --git a/django/forms/jinja2/django/forms/ul.html b/django/forms/jinja2/django/forms/ul.html
new file mode 100644
index 0000000000..116a9b0808
--- /dev/null
+++ b/django/forms/jinja2/django/forms/ul.html
@@ -0,0 +1,24 @@
+{% if errors %}
+
+ {{ errors }}
+ {% if not fields %}
+ {% for field in hidden_fields %}{{ field }}{% endfor %}
+ {% endif %}
+
+{% endif %}
+{% for field, errors in fields %}
+
+ {{ errors }}
+ {% if field.label %}{{ field.label_tag() }}{% endif %}
+ {{ field }}
+ {% if field.help_text %}
+ {{ field.help_text }}
+ {% endif %}
+ {% if loop.last %}
+ {% for field in hidden_fields %}{{ field }}{% endfor %}
+ {% endif %}
+
+{% endfor %}
+{% if not fields and not errors %}
+ {% for field in hidden_fields %}{{ field }}{% endfor %}
+{% endif %}
diff --git a/django/forms/models.py b/django/forms/models.py
index 16681ba80b..5dcf923c12 100644
--- a/django/forms/models.py
+++ b/django/forms/models.py
@@ -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
diff --git a/django/forms/templates/django/forms/attrs.html b/django/forms/templates/django/forms/attrs.html
new file mode 100644
index 0000000000..50de36bae0
--- /dev/null
+++ b/django/forms/templates/django/forms/attrs.html
@@ -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 %}
\ No newline at end of file
diff --git a/django/forms/templates/django/forms/default.html b/django/forms/templates/django/forms/default.html
new file mode 100644
index 0000000000..d034b60d57
--- /dev/null
+++ b/django/forms/templates/django/forms/default.html
@@ -0,0 +1 @@
+{% include "django/forms/table.html" %}
diff --git a/django/forms/templates/django/forms/errors/dict/default.html b/django/forms/templates/django/forms/errors/dict/default.html
new file mode 100644
index 0000000000..8a833c658d
--- /dev/null
+++ b/django/forms/templates/django/forms/errors/dict/default.html
@@ -0,0 +1 @@
+{% include "django/forms/errors/dict/ul.html" %}
\ No newline at end of file
diff --git a/django/forms/templates/django/forms/errors/dict/text.txt b/django/forms/templates/django/forms/errors/dict/text.txt
new file mode 100644
index 0000000000..dc9fd80c99
--- /dev/null
+++ b/django/forms/templates/django/forms/errors/dict/text.txt
@@ -0,0 +1,3 @@
+{% for field, errors in errors %}* {{ field }}
+{% for error in errors %} * {{ error }}
+{% endfor %}{% endfor %}
diff --git a/django/forms/templates/django/forms/errors/dict/ul.html b/django/forms/templates/django/forms/errors/dict/ul.html
new file mode 100644
index 0000000000..c16fd65914
--- /dev/null
+++ b/django/forms/templates/django/forms/errors/dict/ul.html
@@ -0,0 +1 @@
+{% if errors %}
{% for field, error in errors %}
{{ field }}{{ error }}
{% endfor %}
{% endif %}
diff --git a/django/forms/templates/django/forms/errors/list/default.html b/django/forms/templates/django/forms/errors/list/default.html
new file mode 100644
index 0000000000..b174f26f4f
--- /dev/null
+++ b/django/forms/templates/django/forms/errors/list/default.html
@@ -0,0 +1 @@
+{% include "django/forms/errors/list/ul.html" %}
\ No newline at end of file
diff --git a/django/forms/templates/django/forms/errors/list/text.txt b/django/forms/templates/django/forms/errors/list/text.txt
new file mode 100644
index 0000000000..aa7f870b47
--- /dev/null
+++ b/django/forms/templates/django/forms/errors/list/text.txt
@@ -0,0 +1,2 @@
+{% for error in errors %}* {{ error }}
+{% endfor %}
diff --git a/django/forms/templates/django/forms/errors/list/ul.html b/django/forms/templates/django/forms/errors/list/ul.html
new file mode 100644
index 0000000000..57b34ccb88
--- /dev/null
+++ b/django/forms/templates/django/forms/errors/list/ul.html
@@ -0,0 +1 @@
+{% if errors %}
{% for error in errors %}
{{ error }}
{% endfor %}
{% endif %}
\ No newline at end of file
diff --git a/django/forms/templates/django/forms/formsets/default.html b/django/forms/templates/django/forms/formsets/default.html
new file mode 100644
index 0000000000..d8284c5da1
--- /dev/null
+++ b/django/forms/templates/django/forms/formsets/default.html
@@ -0,0 +1 @@
+{{ formset.management_form }}{% for form in formset %}{{ form }}{% endfor %}
diff --git a/django/forms/templates/django/forms/formsets/p.html b/django/forms/templates/django/forms/formsets/p.html
new file mode 100644
index 0000000000..00c2df6b3e
--- /dev/null
+++ b/django/forms/templates/django/forms/formsets/p.html
@@ -0,0 +1 @@
+{{ formset.management_form }}{% for form in formset %}{{ form.as_p }}{% endfor %}
diff --git a/django/forms/templates/django/forms/formsets/table.html b/django/forms/templates/django/forms/formsets/table.html
new file mode 100644
index 0000000000..4fa5e42548
--- /dev/null
+++ b/django/forms/templates/django/forms/formsets/table.html
@@ -0,0 +1 @@
+{{ formset.management_form }}{% for form in formset %}{{ form.as_table }}{% endfor %}
diff --git a/django/forms/templates/django/forms/formsets/ul.html b/django/forms/templates/django/forms/formsets/ul.html
new file mode 100644
index 0000000000..272e1290ee
--- /dev/null
+++ b/django/forms/templates/django/forms/formsets/ul.html
@@ -0,0 +1 @@
+{{ formset.management_form }}{% for form in formset %}{{ form.as_ul }}{% endfor %}
diff --git a/django/forms/templates/django/forms/label.html b/django/forms/templates/django/forms/label.html
new file mode 100644
index 0000000000..eb2a9f7973
--- /dev/null
+++ b/django/forms/templates/django/forms/label.html
@@ -0,0 +1 @@
+{% if use_tag %}{% else %}{{ label }}{% endif %}
diff --git a/django/forms/templates/django/forms/p.html b/django/forms/templates/django/forms/p.html
new file mode 100644
index 0000000000..1835b7a461
--- /dev/null
+++ b/django/forms/templates/django/forms/p.html
@@ -0,0 +1,20 @@
+{{ errors }}
+{% if errors and not fields %}
+
{% for field in hidden_fields %}{{ field }}{% endfor %}
+{% endif %}
+{% for field, errors in fields %}
+ {{ errors }}
+
+ {% if field.label %}{{ field.label_tag }}{% endif %}
+ {{ field }}
+ {% if field.help_text %}
+ {{ field.help_text }}
+ {% endif %}
+ {% if forloop.last %}
+ {% for field in hidden_fields %}{{ field }}{% endfor %}
+ {% endif %}
+
+{% endfor %}
+{% if not fields and not errors %}
+ {% for field in hidden_fields %}{{ field }}{% endfor %}
+{% endif %}
diff --git a/django/forms/templates/django/forms/table.html b/django/forms/templates/django/forms/table.html
new file mode 100644
index 0000000000..a553776f2f
--- /dev/null
+++ b/django/forms/templates/django/forms/table.html
@@ -0,0 +1,29 @@
+{% if errors %}
+
+
+ {{ errors }}
+ {% if not fields %}
+ {% for field in hidden_fields %}{{ field }}{% endfor %}
+ {% endif %}
+
+
+{% endif %}
+{% for field, errors in fields %}
+
+
{% if field.label %}{{ field.label_tag }}{% endif %}
+
+ {{ errors }}
+ {{ field }}
+ {% if field.help_text %}
+
+ {{ field.help_text }}
+ {% endif %}
+ {% if forloop.last %}
+ {% for field in hidden_fields %}{{ field }}{% endfor %}
+ {% endif %}
+
+
+{% endfor %}
+{% if not fields and not errors %}
+ {% for field in hidden_fields %}{{ field }}{% endfor %}
+{% endif %}
diff --git a/django/forms/templates/django/forms/ul.html b/django/forms/templates/django/forms/ul.html
new file mode 100644
index 0000000000..9ce6a49f07
--- /dev/null
+++ b/django/forms/templates/django/forms/ul.html
@@ -0,0 +1,24 @@
+{% if errors %}
+
+ {{ errors }}
+ {% if not fields %}
+ {% for field in hidden_fields %}{{ field }}{% endfor %}
+ {% endif %}
+
+{% endif %}
+{% for field, errors in fields %}
+
+ {{ errors }}
+ {% if field.label %}{{ field.label_tag }}{% endif %}
+ {{ field }}
+ {% if field.help_text %}
+ {{ field.help_text }}
+ {% endif %}
+ {% if forloop.last %}
+ {% for field in hidden_fields %}{{ field }}{% endfor %}
+ {% endif %}
+
+{% endfor %}
+{% if not fields and not errors %}
+ {% for field in hidden_fields %}{{ field }}{% endfor %}
+{% endif %}
diff --git a/django/forms/utils.py b/django/forms/utils.py
index 50412f414b..44447b5cf5 100644
--- a/django/forms/utils.py
+++ b/django/forms/utils.py
@@ -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
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(
- '
{}
',
- format_html_join('', '
{}{}
', 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(
- '
{}
',
- self.error_class,
- format_html_join('', '
{}
', ((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))
diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt
index 23af7315dd..29af8cc7e2 100644
--- a/docs/internals/deprecation.txt
+++ b/docs/internals/deprecation.txt
@@ -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
diff --git a/docs/ref/forms/api.txt b/docs/ref/forms/api.txt
index 9cdbc21800..9dcfbcfb09 100644
--- a/docs/ref/forms/api.txt
+++ b/docs/ref/forms/api.txt
@@ -520,13 +520,41 @@ Although ``
`` 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 ``