Commit d14c4fd8 authored by John Kirkwood's avatar John Kirkwood

Refactor field validation and help_text display.

parent aef828d8
Pipeline #2481 passed with stage
in 40 seconds
from datetime import date
from django import forms
from django.urls import reverse
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
# from chp import chp
from chp.components import * #noqa
from chp.pyreact import (
context_middleware, inject_ids, render_element
)
from chp.store import (create_store, Inject_ast_into_DOM, render_app)
# from chp.store import (create_store, Inject_ast_into_DOM, render_app)
from chp.django.components import Csrf
from chp.mdc.components import * # noqa
from chp.mdc.django.factory import Factory as MdcField
from chp.mdc.django.factory import Factory
from .models import Post
......@@ -28,20 +26,28 @@ class PostForm(forms.ModelForm):
"text": _("Input Label"),
"date": _("Type = date"),
}
help_texts = {
"text": _(
"Enter 'Error' to raise a general form error on submit"),
"date": _(
"Enter a date earlier than today"),
"media": _(
"Select a media format from the dropdown list"),
}
def clean_date(self):
data = self.cleaned_data["date"]
max_date = date(2000, 1, 1)
max_date = date.today()
if data >= max_date:
raise forms.ValidationError(
_("Date must be less than %(max_date)s"),
_("Date must be earlier 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":
if self.cleaned_data.get("text", "").lower() == "error":
raise forms.ValidationError(
_("General form error."),
code="invalid_form"
......@@ -52,13 +58,16 @@ class PostForm(forms.ModelForm):
def render(self, *args, **kwargs):
def mdc(f):
return MdcField.render(self[f])
return Factory.render(self[f])
def errors(f):
return MdcField.errors(self[f])
return Factory.errors(self[f])
def help_text(f):
return Factory.help_text(self[f])
def non_field_errors(self):
return MdcField.non_field_errors(self)
return Factory.non_field_errors(self)
form = (
Grid([], [
......@@ -75,20 +84,19 @@ class PostForm(forms.ModelForm):
non_field_errors(self),
mdc("checkbox"),
errors("checkbox"),
help_text("checkbox"),
mdc("text"),
errors("text"),
help_text("text"),
mdc("date"),
errors("date"),
help_text("date"),
mdc("media"),
errors("media"),
help_text("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"]),
help_text("foreignkey"),
], {"display": "grid"}),
Flex([],
[
......
from django.conf import settings
from django.db import models
......@@ -28,7 +29,7 @@ class Post(models.Model):
date = models.DateField()
media = models.CharField(max_length=12,
choices=MEDIA_CHOICES)
foreignkey = models.ForeignKey('auth.User', models.CASCADE)
foreignkey = models.ForeignKey(settings.AUTH_USER_MODEL, models.CASCADE)
class Meta:
ordering = ['text']
......
......@@ -43,21 +43,24 @@ def test_render(postform):
<form id="form-chp" method="POST" chp-id="\d+">
<input type="hidden" name="csrfmiddlewaretoken" value="\w+" chp-id="\d+" />
<div style="display: grid;" chp-id="\d+">
<div class="mdc-form-field mdc-form-field--align-end" data-mdc-auto-init="MDCFormField" chp-id="\d+">
<div class="mdc-form-field mdc-form-field--align-left" data-mdc-auto-init="MDCFormField" chp-id="\d+">
<div class="mdc-checkbox" data-mdc-auto-init="MDCCheckbox" chp-id="\d+"><input name="checkbox" id="id_checkbox" checked class="mdc-checkbox__native-control" type="checkbox" chp-id="\d+" />
<div class="mdc-checkbox__background" chp-id="\d+"></div></div><label for="id_checkbox" chp-id="\d+">This is my checkbox:</label></div>
<div class="mdc-text-field" data-mdc-auto-init="MDCTextField" chp-id="\d+"><input name="text" value="Initial value" maxlength="\d+" required id="id_text" class="mdc-text-field__input" type="text" chp-id="\d+" />
<label for="id_text" class="mdc-floating-label" chp-id="\d+">Input Label:</label>
<div class="mdc-line-ripple" chp-id="\d+"></div></div>
<div class="mdc-text-field" data-mdc-auto-init="MDCTextField" chp-id="\d+"><input name="date" value="2018-10-03" required id="id_date" class="mdc-text-field__input" type="date" chp-id="\d+" />
<div class="mdc-text-field-helper-line" chp-id="\d+"><div class="mdc-text-field-helper-text mdc-text-field-helper-text--persistent" id="text-helper-text" chp-id="\d+">Enter &#39;Error&#39; to raise a general form error on submit</div></div>
<div class="mdc-text-field" data-mdc-auto-init="MDCTextField" chp-id="\d+"><input name="date" value="\d+-\d+-\d+" required id="id_date" class="mdc-text-field__input" type="date" chp-id="\d+" />
<label for="id_date" class="mdc-floating-label" chp-id="\d+">Type = date:</label>
<div class="mdc-line-ripple" chp-id="\d+"></div></div>
<div class="mdc-text-field-helper-line" chp-id="\d+"><div class="mdc-text-field-helper-text mdc-text-field-helper-text--persistent" id="date-helper-text" chp-id="\d+">Enter a date earlier than today</div></div>
<div class="mdc-select" data-mdc-auto-init="MDCSelect" chp-id="\d+">
<select name="media" required id="id_media" class="mdc-select__native-control" chp-id="\d+">
<option value="" disabled chp-id="\d+"></option><optgroup label="Audio" chp-id="\d+"><option value="vinyl" selected chp-id="\d+">Vinyl</option><option value="cd" chp-id="\d+">CD</option><option value="mp3" chp-id="\d+">MP3</option></optgroup><optgroup label="Video" chp-id="\d+"><option value="vhs" chp-id="\d+">VHS tape</option><option value="dvd" chp-id="\d+">DVD</option><option value="blu-ray" chp-id="\d+">Blu-ray</option></optgroup>
</select>
<label for="id_media" class="mdc-floating-label" chp-id="\d+">Media:</label>
<div class="mdc-line-ripple" chp-id="\d+"></div></div>
<div class="mdc-text-field-helper-line" chp-id="\d+"><div class="mdc-text-field-helper-text mdc-text-field-helper-text--persistent" id="media-helper-text" chp-id="\d+">Select a media format from the dropdown list</div></div>
<div class="mdc-select" data-mdc-auto-init="MDCSelect" chp-id="\d+">
<select name="foreignkey" required id="id_foreignkey" class="mdc-select__native-control" chp-id="\d+">
<option value="" disabled chp-id="\d+"></option>
......@@ -77,7 +80,7 @@ def test_render_checkbox(postform):
fld = MdcField.render(postform["checkbox"])
html = render_element(fld)
test_str = """
<div class="mdc-form-field mdc-form-field--align-end" data-mdc-auto-init="MDCFormField">
<div class="mdc-form-field mdc-form-field--align-left" data-mdc-auto-init="MDCFormField">
<div class="mdc-checkbox" data-mdc-auto-init="MDCCheckbox">
<input name="checkbox" id="id_checkbox" checked class="mdc-checkbox__native-control" type="checkbox" />
<div class="mdc-checkbox__background"></div>
......@@ -145,8 +148,9 @@ def test_render_media(postform):
# TODO: needs a fixture to create database entries
def test_render_foreignkey(postform):
from django.contrib.auth.models import User
User.objects.create(username=user_name)
from django.contrib.auth import get_user_model
user_model = get_user_model()
user_model.objects.create(username=user_name)
fld = MdcField.render(postform["foreignkey"])
html = render_element(fld)
......@@ -187,7 +191,7 @@ def test_render_form_errors():
<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 class="mdc-text-field-helper-line"><div class="mdc-text-field-helper-text--validation-msg mdc-text-field-helper-text mdc-text-field-helper-text--persistent" class="mdc-text-field-helper-text--validation-msg" id="{test_field}-helper-text">{err_msg}</div></div>
</div>
""" # noqa
test_str = re.sub(r"\n\s*", "", test_str)
......
from datetime import date
from django.urls.base import reverse_lazy
from django.views import generic
......@@ -10,10 +12,11 @@ class PostCreateView(generic.CreateView):
model = Post
def get_initial(self):
initial = super(PostCreateView, self).get_initial()
initial = super().get_initial()
initial.update({
'checkbox': True,
'text': 'Initial value',
'date': date.today(),
'media': Post.VHS,
})
return initial
......
......@@ -3,7 +3,7 @@ from django.views import generic
from django.urls import include, path
urlpatterns = [
path('', generic.RedirectView.as_view(url='/todos')),
path('', generic.RedirectView.as_view(url='/blog/post/create')),
path('blog/', include('chp.django.example.blog.urls',
namespace='blog')),
path('todos/', include('chp.django.example.todos.urls',
......
......@@ -77,7 +77,7 @@ def FormField(children=[]):
Only required for checkbox and radio button fields
"""
props = [
cp("class", "mdc-form-field mdc-form-field--align-end"),
cp("class", "mdc-form-field mdc-form-field--align-left"),
cp("data-mdc-auto-init", "MDCFormField"),
]
return Div(props, children)
......@@ -218,7 +218,7 @@ def InputField(props=[], children=[], context={}):
mdc_type = MDC_TYPE_MAP[el_type]
mdc_class = mdc_type["class"]
# if there are erorrs, add --invalid class.
# if there are errors, add --invalid class.
if context["errors"]:
mdc_invalid = "mdc-text-field--invalid"
mdc_class = f"{mdc_class} {mdc_invalid}"
......@@ -272,3 +272,56 @@ def SelectField(props=[], children=[], context={}):
children_field.append(ast_label)
children_field.append(LineRipple())
return InputField([], children_field, context)
def HelperText(props=[], children=[], context={}):
"""Add MDC classes to help text for an input field.
Use 'name' from 'context' to link the text to an input field.
<div class="mdc-text-field-helper-line">
<div id="{field_name}-helper-text"
class="mdc-text-field-helper-text
mdc-text-field-helper-text--persistent
mdc-text-field-helper-text--validation-msg"
aria-hidden="true">
{message}
</div>
</div>
"""
field_name = context.get("name", "")
classes = chp.get_prop(props, "class")
if classes is None:
classes = []
else:
classes = classes["value"]
if isinstance(classes, (str,)):
classes = [classes]
classes.extend([
"mdc-text-field-helper-text",
"mdc-text-field-helper-text--persistent",
])
# prepend new 'class' prop to supercede original entry in list.
# TODO: add set_prop() functionality?
props = [cp("class", " ".join(classes))] + props
sep = "-"
props.append(
cp("id",
f"{(field_name + sep) if field_name else 'non-field-'}helper-text")
)
children_line = [chp.Div(props, children)]
props_line = [cp("class", "mdc-text-field-helper-line"),]
return chp.Div(props_line, children_line)
def ValidationText(props=[], children=[], context={}):
"""Add MDC classes to validation text for an input field.
Use 'name' from 'context' to link to an input field and display 'errors'."""
props = [
cp("class", "mdc-text-field-helper-text--validation-msg"),
]
children = "<br />".join(context.get("errors", []))
return HelperText(props, children, context)
......@@ -84,32 +84,39 @@ class Factory:
props.extend([
cp("class", "errorlist nonfield"),
])
error_msg = "<br />".join(errs)
return chp.Para(props, error_msg)
return mdc.ValidationText(props, [],
{"errors": errs,})
# 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)
"""Render a field validation message."""
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)
return mdc.ValidationText([], [],
{"name": field.name,
"errors": errs,})
@staticmethod
def help_text(field):
"""Render a field help_text."""
# MDC design: don't show help if there is an error to display.
errs = field.form.errors.get(field.name, field.form.error_class())
if errs:
return {}
help_text = field.help_text
if help_text == "":
return {}
# cast any lazy translation strings
if isinstance(help_text, Promise):
help_text = conditional_escape(help_text)
return mdc.HelperText([], help_text,
{"name": field.name})
@staticmethod
def mdc_checkboxinput(context):
......
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