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