Commit 4a80e6a7 authored by John Kirkwood's avatar John Kirkwood

Display form validation errors.

parent 6010061e
......@@ -5,6 +5,10 @@ def Div(props=[], children=[]):
return ce('div', props, children)
def Para(props=[], children=[]):
return ce('p', props, children)
def Button(props=[], children=[]):
props.append(
cp('type', 'button'))
......@@ -80,7 +84,7 @@ def Select(props=[], children=[]):
try:
props_option = children[0]["props"]
option_value = get_prop(props_option, "value")
if (option_value is None
if (option_value is None
or option_value["value"] == ""):
props_option.append(
cp("disabled", True))
......
from datetime import date
from django import forms
from django.urls import reverse
from django.utils.safestring import mark_safe
......@@ -27,10 +29,37 @@ class PostForm(forms.ModelForm):
"date": _("Type = date"),
}
def clean_date(self):
data = self.cleaned_data["date"]
max_date = date(2000, 1, 1)
if data >= max_date:
raise forms.ValidationError(
_("Date must be less than %(max_date)s"),
code="max_date",
params={"max_date": max_date.strftime("%d/%m/%Y")}
)
return data
def clean(self):
if self.cleaned_data.get("text", None) == "Error":
raise forms.ValidationError(
_("General form error."),
code="invalid_form"
)
def FormSchema(self, *args, **kwargs):
def render(self, *args, **kwargs):
def mdc(f):
return MdcField.render(self[f])
def errors(f):
return MdcField.errors(self[f])
def non_field_errors(self):
return MdcField.non_field_errors(self)
form = (
Grid([], [
Row([], [
......@@ -43,22 +72,33 @@ class PostForm(forms.ModelForm):
Csrf(),
Flex([],
[
# Div([], str(self.errors)),
MdcField.render(self["checkbox"]),
MdcField.render(self["text"]),
MdcField.render(self["date"]),
MdcField.render(self["media"]),
MdcField.render(self["foreignkey"]),
]),
non_field_errors(self),
mdc("checkbox"),
errors("checkbox"),
mdc("text"),
errors("text"),
mdc("date"),
errors("date"),
mdc("media"),
errors("media"),
mdc("foreignkey"),
errors("foreignkey"),
# MdcField.render(self["checkbox"]),
# MdcField.render(self["text"]),
# MdcField.render(self["date"]),
# MdcField.render(self["media"]),
# MdcField.render(self["foreignkey"]),
], {"display": "grid"}),
Flex([],
[
SubmitButton([cp("form", "form-chp")]),
]),
])
]) # Cell
], {"span": 6}) # Cell
]) # Row
]) # Grid
)
) # noqa
return form
......
import pytest
import re
from django.core.exceptions import ValidationError
from django.test import TestCase
from chp.components import Div
from chp.django.example.blog.forms import PostForm
from chp.mdc.django.factory import Factory as MdcField
from chp.pyreact import render_element
......@@ -28,10 +32,11 @@ def postform_html(postform):
# return postform.render()
pass
def test_render(postform):
"""Regression test."""
regex = """
regex = r"""
<div class="mdc-layout-grid" chp-id="\d+">
<div class="mdc-layout-grid__inner" chp-id="\d+">
<div class="mdc-layout-grid__cell--span-12" chp-id="\d+">
......@@ -62,7 +67,7 @@ def test_render(postform):
<div style="display: flex;" chp-id="\d+">
<button form="form-chp" class="mdc-button" type="submit" chp-id="\d+">Submit</button></div>
</form></div></div></div>
"""
""" # noqa
regex = regex.replace("\n", "")
regexc = re.compile(regex)
assert re.match(regexc, postform.render()) is not None
......@@ -79,8 +84,8 @@ def test_render_checkbox(postform):
</div>
<label for="id_checkbox">This is my checkbox:</label>
</div>
"""
test_str = re.sub("\n\s*", "", test_str)
""" # noqa
test_str = re.sub(r"\n\s*", "", test_str)
assert html == test_str
......@@ -93,8 +98,8 @@ def test_render_text(postform):
<label for="id_text" class="mdc-floating-label">Input Label:</label>
<div class="mdc-line-ripple"></div>
</div>
"""
test_str = re.sub("\n\s*", "", test_str)
""" # noqa
test_str = re.sub(r"\n\s*", "", test_str)
assert html == test_str
......@@ -107,8 +112,8 @@ def test_render_date(postform):
<label for="id_date" class="mdc-floating-label">Type = date:</label>
<div class="mdc-line-ripple"></div>
</div>
"""
test_str = re.sub("\n\s*", "", test_str)
""" # noqa
test_str = re.sub(r"\n\s*", "", test_str)
assert html == test_str
......@@ -133,8 +138,8 @@ def test_render_media(postform):
<label for="id_media" class="mdc-floating-label">Media:</label>
<div class="mdc-line-ripple"></div>
</div>
"""
test_str = re.sub("\n\s*", "", test_str)
""" # noqa
test_str = re.sub(r"\n\s*", "", test_str)
assert html == test_str
......@@ -154,6 +159,43 @@ def test_render_foreignkey(postform):
<label for="id_foreignkey" class="mdc-floating-label">Foreignkey:</label>
<div class="mdc-line-ripple"></div>
</div>
"""
test_str = re.sub("\n\s*", "", test_str)
""" # noqa
test_str = re.sub(r"\n\s*", "", test_str)
assert html == test_str
def test_render_form_errors():
# Receive form with initial values
# POST form to create view
# Compare form.errors with MdcField(form.errors)
postform = PostForm({}) # should yield an invalid form
test_field = "date"
postform.is_valid() # ignore result
invalid_msg = postform[test_field].field.error_messages["invalid"]
postform.add_error(test_field,
ValidationError(invalid_msg, code="invalid"))
err_msg = "<br />".join(postform.errors[test_field])
ast_fld = MdcField.render(postform[test_field])
ast_err = MdcField.errors(postform[test_field])
ast = Div([], [ast_fld, ast_err])
html = render_element(ast)
test_str = f"""
<div>
<div class="mdc-text-field mdc-text-field--invalid" data-mdc-auto-init="MDCTextField">
<input name="date" required id="id_date" class="mdc-text-field__input" type="date" />
<label for="id_date" class="mdc-floating-label">Type = date:</label>
<div class="mdc-line-ripple"></div>
</div>
<p class="mdc-text-field-helper-text mdc-text-field-helper-text--persistent mdc-text-field-helper-text--validation-msg" id="{test_field}-validation-msg">{err_msg}</p>
</div>
""" # noqa
test_str = re.sub(r"\n\s*", "", test_str)
assert html == test_str
class FormTest(TestCase):
def test_uses_post_form_template(self):
response = self.client.get("/blog/post/create")
self.assertTemplateUsed(response, "blog/post_form.html")
......@@ -43,12 +43,13 @@ def Row(props=[], children=[]):
return Div(props, children)
def Cell(props=[], children=[]):
def Cell(props=[], children=[], context={}):
"""Start the MDC grid cell.
Set the span to 12 columns as a sensible default."""
span = context.get("span", 12)
props.append(
cp('class', 'mdc-layout-grid__cell--span-12')
cp('class', f'mdc-layout-grid__cell--span-{span}')
)
return Div(props, children)
......@@ -215,10 +216,18 @@ def InputField(props=[], children=[], context={}):
Requires context["type"] to lookup MDC classes."""
el_type = context.get("type", "text")
mdc_type = MDC_TYPE_MAP[el_type]
mdc_class = mdc_type["class"]
# if there are erorrs, add --invalid class.
if context["errors"]:
mdc_invalid = "mdc-text-field--invalid"
mdc_class = f"{mdc_class} {mdc_invalid}"
props_field = [
cp("class", mdc_type["class"]),
cp("class", mdc_class),
cp("data-mdc-auto-init", mdc_type["init"]),
]
return Div(props_field, children)
......
......@@ -70,10 +70,47 @@ class Factory:
'type':
context['widget'].get('type',
field.field.widget.input_type),
'errors': getattr(field, "errors", None),
})
return mdc_render(context)
@staticmethod
def non_field_errors(form):
errs = form.errors.get("__all__", form.error_class())
if errs == []:
return {}
props = []
props.extend([
cp("class", "errorlist nonfield"),
])
error_msg = "<br />".join(errs)
return chp.Para(props, error_msg)
@staticmethod
def errors(field):
"""<p class="mdc-text-field-helper-text
mdc-text-field-helper-text--persistent
mdc-text-field-helper-text--validation-msg"
id="{test_field}-validation-msg"
role="alert">
{invalid_msg}</p>
"""
# errs = getattr(field, "errors", None)
errs = field.form.errors.get(field.name, field.form.error_class())
if errs == []:
return {}
props = []
props.extend([
cp("class",
" ".join(["mdc-text-field-helper-text",
"mdc-text-field-helper-text--persistent",
"mdc-text-field-helper-text--validation-msg"])),
cp("id", f"{field.name}-validation-msg"),
])
error_msg = "<br />".join(errs)
return chp.Para(props, error_msg)
@staticmethod
def mdc_checkboxinput(context):
"""Render a BooleanField/CheckBoxInput field/widget."""
......@@ -147,6 +184,8 @@ class Factory:
for option in group_choices:
props_option = []
option_label = option["label"]
# override any 'empty_label' as MDC floating label will be
# displayed instead.
if option["value"] == "":
option_label = ""
props_option.append(
......@@ -156,9 +195,11 @@ class Factory:
children_group.append(
chp.Option(props_option, option_label))
# build list of optgroups
if group_name:
children.append(
chp.Optgroup(props_group, children_group))
# or simple list of options
else:
children.extend(children_group)
......
......@@ -186,6 +186,8 @@ def id_middleware(ast, *args):
def render_ast(ast, ast_middleware, render_middleware):
ast = ast_middleware(ast)
if ast == {}:
return ""
props = ast["props"]
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment