Merge pull request #147 from dod-ccpo/ui/input-field-frontend-validation

Ui/input field frontend validation
This commit is contained in:
andrewdds 2018-08-13 11:19:16 -04:00 committed by GitHub
commit 73205e9d90
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 201 additions and 67 deletions

12
atst/domain/date.py Normal file
View File

@ -0,0 +1,12 @@
import pendulum
def parse_date(data):
date_formats = ["YYYY-MM-DD", "MM/DD/YYYY"]
for _format in date_formats:
try:
return pendulum.from_format(data, _format).date()
except (ValueError, pendulum.parsing.exceptions.ParserError):
pass
raise ValueError("Unable to parse string {}".format(data))

View File

@ -1,20 +1,14 @@
from wtforms.fields.html5 import DateField
from wtforms.fields import Field
from wtforms.widgets import TextArea
import pendulum
from atst.domain.date import parse_date
class DateField(DateField):
def _value(self):
if self.data:
date_formats = ["YYYY-MM-DD", "MM/DD/YYYY"]
for _format in date_formats:
try:
return pendulum.from_format(self.data, _format).date()
except (ValueError, pendulum.parsing.exceptions.ParserError):
pass
raise ValueError("Unable to parse string {}".format(self.data))
return parse_date(self.data)
else:
return None

View File

@ -12,9 +12,11 @@ class OrgForm(ValidatedForm):
lname_request = StringField("Last Name", validators=[Required(), Alphabet()])
email_request = EmailField("Email Address", validators=[Required(), Email()])
email_request = EmailField("E-mail Address", validators=[Required(), Email()])
phone_number = TelField("Phone Number", validators=[Required(), PhoneNumber()])
phone_number = TelField("Phone Number",
description='Enter a 10-digit phone number',
validators=[Required(), PhoneNumber()])
service_branch = StringField("Service Branch or Agency", validators=[Required()])

View File

@ -2,18 +2,19 @@ import re
from wtforms.validators import ValidationError
import pendulum
from atst.domain.date import parse_date
def DateRange(lower_bound=None, upper_bound=None, message=None):
def _date_range(form, field):
now = pendulum.now().date()
date = parse_date(field.data)
if lower_bound is not None:
date = pendulum.parse(field.data).date()
if (now - lower_bound) > date:
raise ValidationError(message)
if upper_bound is not None:
date = pendulum.parse(field.data).date()
if (now + upper_bound) < date:
raise ValidationError(message)

View File

@ -14,32 +14,54 @@ export default {
type: String,
default: () => 'anything'
},
value: {
initialValue: {
type: String,
default: () => ''
}
},
initialErrors: Array
},
data: function () {
return {
showError: false,
showError: (this.initialErrors && this.initialErrors.length) || false,
showValid: false,
mask: inputValidations[this.validation].mask,
renderedValue: this.value
pipe: inputValidations[this.validation].pipe || undefined,
keepCharPositions: inputValidations[this.validation].keepCharPositions || false,
validationError: inputValidations[this.validation].validationError || '',
value: this.initialValue,
modified: false
}
},
computed:{
rawValue: function () {
return this._rawValue(this.value)
}
},
mounted: function () {
const value = this.$refs.input.value
if (value) {
this._checkIfValid({ value, invalidate: true })
this.renderedValue = conformToMask(value, this.mask).conformedValue
if (this.value) {
this._checkIfValid({ value: this.value, invalidate: true })
if (this.mask && this.validation !== 'email') {
const mask = typeof this.mask.mask !== 'function'
? this.mask
: mask.mask(this.value).filter((val) => val !== '[]')
this.value = conformToMask(this.value, mask).conformedValue
}
}
},
methods: {
// When user types a character
onInput: function (value) {
onInput: function (e) {
// When we use the native textarea element, we receive an event object
// When we use the masked-input component, we receive the value directly
const value = typeof e === 'object' ? e.target.value : e
this.value = value
this.modified = true
this._checkIfValid({ value })
},
@ -52,7 +74,11 @@ export default {
//
_checkIfValid: function ({ value, invalidate = false}) {
// Validate the value
const valid = this._validate(value)
let valid = this._validate(value)
if (!this.modified && this.initialErrors && this.initialErrors.length) {
valid = false
}
// Show error messages or not
if (valid) {
@ -70,13 +96,14 @@ export default {
})
},
_validate: function (value) {
// Strip out all the mask characters
let rawValue = inputValidations[this.validation].unmask.reduce((currentValue, character) => {
_rawValue: function (value) {
return inputValidations[this.validation].unmask.reduce((currentValue, character) => {
return currentValue.split(character).join('')
}, value)
},
return inputValidations[this.validation].match.test(rawValue)
_validate: function (value) {
return inputValidations[this.validation].match.test(this._rawValue(value))
}
}
}

View File

@ -1,20 +1,56 @@
import createNumberMask from 'text-mask-addons/dist/createNumberMask'
import emailMask from 'text-mask-addons/dist/emailMask'
import createAutoCorrectedDatePipe from 'text-mask-addons/dist/createAutoCorrectedDatePipe'
export default {
anything: {
mask: false,
match: /^(?!\s*$).+/,
unmask: [],
validationError: 'Please enter a response.'
},
integer: {
mask: createNumberMask({ prefix: '', allowDecimal: false }),
match: /^[1-9]\d*$/,
unmask: [','],
validationError: 'Please enter a number.'
},
dollars: {
mask: createNumberMask({ prefix: '$', allowDecimal: true }),
match: /^-?\d+\.?\d*$/,
unmask: ['$',',']
unmask: ['$',','],
validationError: 'Please enter a dollar amount.'
},
gigabytes: {
mask: createNumberMask({ prefix: '', suffix:'GB', allowDecimal: false }),
match: /^[1-9]\d*$/,
unmask: [',','GB'],
validationError: 'Please enter an amount of data in gigabytes.'
},
email: {
mask: emailMask,
match: /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/,
unmask: [],
validationError: 'Please enter a valid e-mail address.'
},
date: {
mask: [/\d/,/\d/,'/',/\d/,/\d/,'/',/\d/,/\d/,/\d/,/\d/],
match: /(0[1-9]|1[012])[- \/.](0[1-9]|[12][0-9]|3[01])[- \/.](19|20)\d\d/,
unmask: [],
pipe: createAutoCorrectedDatePipe('mm/dd/yyyy HH:MM'),
keepCharPositions: true,
validationError: 'Please enter a valid date in the form MM/DD/YYYY.'
},
usPhone: {
mask: ['(', /[1-9]/, /\d/, /\d/, ')', ' ', /\d/, /\d/, /\d/, '-', /\d/, /\d/, /\d/, /\d/],
match: /^\d{10}$/,
unmask: ['(',')','-',' '],
validationError: 'Please enter a 10-digit phone number.'
},
dodId: {
mask: createNumberMask({ prefix: '', allowDecimal: false, includeThousandsSeparator: false }),
match: /^\d{10}$/,
unmask: [],
validationError: 'Please enter a 10-digit D.O.D. ID number.'
}
}

View File

@ -1,23 +1,64 @@
{% from "components/icon.html" import Icon %}
{% macro TextInput(field, placeholder='') -%}
<div class='usa-input {% if field.errors %}usa-input--error{% endif %}'>
{% macro TextInput(field, placeholder='', validation='anything', paragraph=False) -%}
<textinput
name='{{ field.name }}'
validation='{{ validation }}'
{% if field.data %}initial-value='{{ field.data }}'{% endif %}
{% if field.errors %}v-bind:initial-errors='{{ field.errors }}'{% endif %}
inline-template>
<div
v-bind:class="['usa-input usa-input--validation--' + validation, { 'usa-input--error': showError, 'usa-input--success': showValid }]">
<label for={{field.name}}>
{{ field.label }}
{{ field.label | striptags }}
{% if field.description %}
<span class='usa-input__help'>{{ field.description | safe }}</span>
{% endif %}
{% if field.errors %}{{ Icon('alert') }}{% endif %}
<span v-show='showError'>{{ Icon('alert') }}</span>
<span v-show='showValid'>{{ Icon('ok') }}</span>
</label>
{{ field(placeholder=placeholder) | safe }}
{% if paragraph %}
<textarea
v-on:input='onInput'
v-on:change='onChange'
v-bind:value='value'
id='{{ field.name }}'
ref='input'
placeholder='{{ placeholder }}'>
</textarea>
{% else %}
<masked-input
v-on:input='onInput'
v-on:change='onChange'
v-bind:value='value'
v-bind:mask='mask'
v-bind:pipe='pipe'
v-bind:keep-char-positions='keepCharPositions'
v-bind:aria-invalid='showError'
id='{{ field.name }}'
type='text'
ref='input'
placeholder='{{ placeholder }}'>
</masked-input>
{% if field.errors %}
{% for error in field.errors %}
<span class='usa-input__message'>{{ error }}</span>
{% endfor %}
{% endif %}
<input type='hidden' v-bind:value='rawValue' name='{{ field.name }}' />
<template v-if='showError'>
<span v-if='initialErrors' v-for='error in initialErrors' class='usa-input__message' v-html='error'></span>
<span v-if='!initialErrors' class='usa-input__message' v-html='validationError'></span>
</template>
</div>
</textinput>
{%- endmacro %}

View File

@ -22,27 +22,27 @@
<p><em>All fields are required, unless specified optional.</em></p>
<h2>General</h2>
{{ TextInput(f.dod_component) }}
{{ TextInput(f.jedi_usage,placeholder="e.g. We are migrating XYZ application to the cloud so that...") }}
{{ OptionsInput(f.dod_component) }}
{{ TextInput(f.jedi_usage, paragraph=True, placeholder="e.g. We are migrating XYZ application to the cloud so that...") }}
<h2>Cloud Readiness</h2>
{{ TextInput(f.num_software_systems,placeholder="Number of systems") }}
{{ TextInput(f.num_software_systems, validation='integer') }}
{{ OptionsInput(f.jedi_migration) }}
{{ OptionsInput(f.rationalization_software_systems) }}
{{ OptionsInput(f.technical_support_team) }}
{{ OptionsInput(f.organization_providing_assistance) }}
{{ OptionsInput(f.engineering_assessment) }}
{{ TextInput(f.data_transfers) }}
{{ TextInput(f.expected_completion_date) }}
{{ OptionsInput(f.data_transfers) }}
{{ OptionsInput(f.expected_completion_date) }}
{{ OptionsInput(f.cloud_native) }}
<h2>Financial Usage</h2>
{{ TextInput(f.estimated_monthly_spend) }}
{{ TextInput(f.estimated_monthly_spend, validation='dollars') }}
<p>So this means you are spending approximately <b>$X</b> annually</p>
{{ TextInput(f.dollar_value) }}
{{ TextInput(f.number_user_sessions) }}
{{ TextInput(f.average_daily_traffic) }}
{{ TextInput(f.start_date) }}
{{ TextInput(f.dollar_value, validation='dollars') }}
{{ TextInput(f.number_user_sessions, validation='integer') }}
{{ TextInput(f.average_daily_traffic, placeholder='Gigabytes per day', validation='gigabytes') }}
{{ TextInput(f.start_date, validation='date', placeholder='MM / DD / YYYY') }}
{% endblock %}

View File

@ -19,16 +19,16 @@
<p>Please tell us more about you.</p>
{{ TextInput(f.fname_request,placeholder='First Name') }}
{{ TextInput(f.lname_request,placeholder='Last Name') }}
{{ TextInput(f.email_request,placeholder='jane@mail.mil') }}
{{ TextInput(f.phone_number,placeholder='(123) 456-7890') }}
{{ TextInput(f.fname_request, placeholder='First Name') }}
{{ TextInput(f.lname_request, placeholder='Last Name') }}
{{ TextInput(f.email_request, placeholder='jane@mail.mil', validation='email') }}
{{ TextInput(f.phone_number, placeholder='e.g. (123) 456-7890', validation='usPhone') }}
<p>We want to collect the following information from you for security auditing and determining priviledged user access</p>
{{ TextInput(f.service_branch,placeholder='e.g. US Air Force, US Army, US Navy, Marine Corps, Defense Media Agency') }}
{{ OptionsInput(f.citizenship) }}
{{ OptionsInput(f.designation) }}
{{ TextInput(f.date_latest_training) }}
{{ TextInput(f.date_latest_training, placeholder='MM / DD / YYYY', validation='date') }}
{% endblock %}

View File

@ -30,7 +30,7 @@
{{ TextInput(f.fname_poc,placeholder='First Name') }}
{{ TextInput(f.lname_poc,placeholder='Last Name') }}
{{ TextInput(f.email_poc,placeholder='jane@mail.mil') }}
{{ TextInput(f.dodid_poc,placeholder='10-digit number on the back of the CAC') }}
{{ TextInput(f.email_poc,placeholder='jane@mail.mil', validation='email') }}
{{ TextInput(f.dodid_poc,placeholder='10-digit number on the back of the CAC', validation='dodId') }}
{% endblock %}

21
tests/domain/test_date.py Normal file
View File

@ -0,0 +1,21 @@
import pytest
import pendulum
from atst.domain.date import parse_date
def test_date_with_slashes():
date_str = "1/2/2020"
assert parse_date(date_str) == pendulum.date(2020, 1, 2)
def test_date_with_dashes():
date_str = "2020-1-2"
assert parse_date(date_str) == pendulum.date(2020, 1, 2)
def test_invalid_date():
date_str = "This is not a valid data"
with pytest.raises(ValueError):
parse_date(date_str)

View File

@ -75,9 +75,9 @@ def test_creator_info_is_autopopulated(monkeypatch, client, user_session):
response = client.get("/requests/new/2/{}".format(request.id))
body = response.data.decode()
assert 'value="{}"'.format(user.first_name) in body
assert 'value="{}"'.format(user.last_name) in body
assert 'value="{}"'.format(user.email) in body
assert "initial-value='{}'".format(user.first_name) in body
assert "initial-value='{}'".format(user.last_name) in body
assert "initial-value='{}'".format(user.email) in body
def test_creator_info_is_autopopulated_for_new_request(monkeypatch, client, user_session):
@ -86,9 +86,9 @@ def test_creator_info_is_autopopulated_for_new_request(monkeypatch, client, user
response = client.get("/requests/new/2")
body = response.data.decode()
assert 'value="{}"'.format(user.first_name) in body
assert 'value="{}"'.format(user.last_name) in body
assert 'value="{}"'.format(user.email) in body
assert "initial-value='{}'".format(user.first_name) in body
assert "initial-value='{}'".format(user.last_name) in body
assert "initial-value='{}'".format(user.email) in body
def test_non_creator_info_is_not_autopopulated(monkeypatch, client, user_session):