summaryrefslogtreecommitdiff
path: root/tests/forms_tests
diff options
context:
space:
mode:
authorDavid Smith <smithdc@gmail.com>2021-09-10 08:06:01 +0100
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2021-09-20 15:50:18 +0200
commit456466d932830b096d39806e291fe23ec5ed38d5 (patch)
tree9320cc645ef43eb920630cff02c1387b34f21906 /tests/forms_tests
parent5353e7c2505c0d0ab8232ad9c131b3c99c833988 (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 'tests/forms_tests')
-rw-r--r--tests/forms_tests/templates/forms_tests/error.html1
-rw-r--r--tests/forms_tests/templates/forms_tests/form_snippet.html6
-rw-r--r--tests/forms_tests/tests/__init__.py30
-rw-r--r--tests/forms_tests/tests/test_deprecation_forms.py183
-rw-r--r--tests/forms_tests/tests/test_forms.py174
-rw-r--r--tests/forms_tests/tests/test_formsets.py32
-rw-r--r--tests/forms_tests/tests/test_i18n.py2
-rw-r--r--tests/forms_tests/tests/tests.py2
8 files changed, 293 insertions, 137 deletions
diff --git a/tests/forms_tests/templates/forms_tests/error.html b/tests/forms_tests/templates/forms_tests/error.html
new file mode 100644
index 0000000000..4981d32b70
--- /dev/null
+++ b/tests/forms_tests/templates/forms_tests/error.html
@@ -0,0 +1 @@
+{% if errors %}<div class="errorlist">{% for error in errors %}<div class="error">{{ error }}</div>{% endfor %}</div>{% endif %}
diff --git a/tests/forms_tests/templates/forms_tests/form_snippet.html b/tests/forms_tests/templates/forms_tests/form_snippet.html
new file mode 100644
index 0000000000..90ab4f2981
--- /dev/null
+++ b/tests/forms_tests/templates/forms_tests/form_snippet.html
@@ -0,0 +1,6 @@
+{% for field in form %}
+ <div class="fieldWrapper">
+ {{ field.errors }}
+ {{ field.label_tag }} {{ field }}
+ </div>
+{% endfor %}
diff --git a/tests/forms_tests/tests/__init__.py b/tests/forms_tests/tests/__init__.py
index e69de29bb2..7fc0269ec5 100644
--- a/tests/forms_tests/tests/__init__.py
+++ b/tests/forms_tests/tests/__init__.py
@@ -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
diff --git a/tests/forms_tests/tests/test_deprecation_forms.py b/tests/forms_tests/tests/test_deprecation_forms.py
new file mode 100644
index 0000000000..6bdb595602
--- /dev/null
+++ b/tests/forms_tests/tests/test_deprecation_forms.py
@@ -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>'
+ )
diff --git a/tests/forms_tests/tests/test_forms.py b/tests/forms_tests/tests/test_forms.py
index b713cb1321..8c08cd5ad4 100644
--- a/tests/forms_tests/tests/test_forms.py
+++ b/tests/forms_tests/tests/test_forms.py
@@ -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>',
+ )
diff --git a/tests/forms_tests/tests/test_formsets.py b/tests/forms_tests/tests/test_formsets.py
index 06b61306fe..0d7d452d71 100644
--- a/tests/forms_tests/tests/test_formsets.py
+++ b/tests/forms_tests/tests/test_formsets.py
@@ -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">'
diff --git a/tests/forms_tests/tests/test_i18n.py b/tests/forms_tests/tests/test_i18n.py
index d941902af2..fc5d59c480 100644
--- a/tests/forms_tests/tests/test_i18n.py
+++ b/tests/forms_tests/tests/test_i18n.py
@@ -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):
diff --git a/tests/forms_tests/tests/tests.py b/tests/forms_tests/tests/tests.py
index 18b4c58b5e..ec08a5f9ef 100644
--- a/tests/forms_tests/tests/tests.py
+++ b/tests/forms_tests/tests/tests.py
@@ -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()