Merge pull request #147 from dod-ccpo/ui/input-field-frontend-validation
Ui/input field frontend validation
This commit is contained in:
commit
73205e9d90
12
atst/domain/date.py
Normal file
12
atst/domain/date.py
Normal 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))
|
@ -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
|
||||
|
||||
|
@ -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()])
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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.'
|
||||
}
|
||||
}
|
||||
|
@ -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 %}
|
@ -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 %}
|
||||
|
@ -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 %}
|
||||
|
@ -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
21
tests/domain/test_date.py
Normal 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)
|
||||
|
@ -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):
|
||||
|
Loading…
x
Reference in New Issue
Block a user