diff options
| author | David Smith <smithdc@gmail.com> | 2021-09-10 08:06:01 +0100 |
|---|---|---|
| committer | Mariusz Felisiak <felisiak.mariusz@gmail.com> | 2021-09-20 15:50:18 +0200 |
| commit | 456466d932830b096d39806e291fe23ec5ed38d5 (patch) | |
| tree | 9320cc645ef43eb920630cff02c1387b34f21906 /django/forms | |
| parent | 5353e7c2505c0d0ab8232ad9c131b3c99c833988 (diff) | |
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>
Diffstat (limited to 'django/forms')
37 files changed, 357 insertions, 135 deletions
diff --git a/django/forms/boundfield.py b/django/forms/boundfield.py index 3e92cba549..d1e98719d2 100644 --- a/django/forms/boundfield.py +++ b/django/forms/boundfield.py @@ -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): """ diff --git a/django/forms/forms.py b/django/forms/forms.py index 2bf268ae76..589b4693fd 100644 --- a/django/forms/forms.py +++ b/django/forms/forms.py @@ -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] 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 <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) 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 %}<ul class="{{ error_class }}">{% for field, error in errors %}<li>{{ field }}{{ error }}</li>{% endfor %}</ul>{% 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 %}<ul class="{{ error_class }}">{% for error in errors %}<li>{{ error }}</li>{% endfor %}</ul>{% 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 %}<label{% if attrs %}{% include 'django/forms/attrs.html' %}{% endif %}>{{ label }}</label>{% 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 %} + <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 %} 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 %} + <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 %} 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 %} + <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 %} 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 %}<ul class="{{ error_class }}">{% for field, error in errors %}<li>{{ field }}{{ error }}</li>{% endfor %}</ul>{% 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 %}<ul class="{{ error_class }}">{% for error in errors %}<li>{{ error }}</li>{% endfor %}</ul>{% 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 %}<label{% include 'django/forms/attrs.html' %}>{{ label }}</label>{% 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 %} + <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 %} 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 %} + <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 %} 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 %} + <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 %} 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 <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 get_context(self): + return { + 'errors': self.items(), + 'error_class': 'errorlist', + } - def __str__(self): - return self.as_ul() - -@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)) |
