Implement form objects for request forms

This commit is contained in:
Brian Duggan 2018-06-08 11:32:19 -04:00 committed by Jason Garber
parent 4dd4fbf201
commit 9152ffe91e
21 changed files with 1673 additions and 1542 deletions

1
.gitignore vendored
View File

@ -17,7 +17,6 @@ __pycache__
# Compiled python bytecode files
*.pyc
.cache/
atst.ini
# static/fonts for now, since it is just symlink
static/fonts

View File

@ -7,6 +7,7 @@ name = "pypi"
tornado = "==5.0.2"
webassets = "==0.12.1"
Unipath = "==1.1"
wtforms-tornado = "*"
[dev-packages]
pytest = "==3.6.0"

16
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "391e254ddb902877afca9c07aa2306710ce6d1e207b029c1a8b5dc0115ee99a5"
"sha256": "2299d6143992989417d01bd11d7d99a8a239a388321dc1546815d8b49c8151b8"
},
"pipfile-spec": 6,
"requires": {
@ -41,6 +41,20 @@
],
"index": "pypi",
"version": "==0.12.1"
},
"wtforms": {
"hashes": [
"sha256:0cdbac3e7f6878086c334aa25dc5a33869a3954e9d1e015130d65a69309b3b61",
"sha256:e3ee092c827582c50877cdbd49e9ce6d2c5c1f6561f849b3b068c1b8029626f1"
],
"version": "==2.2.1"
},
"wtforms-tornado": {
"hashes": [
"sha256:dadb5e504d01f14bf75900f592888bb402ada6b8f8235fe583359f562d351a3a"
],
"index": "pypi",
"version": "==0.0.2"
}
},
"develop": {

View File

@ -15,16 +15,6 @@ To enter the virtualenv manually (a la `source .venv/bin/activate`):
If you want to automatically load the virtual environment whenever you enter the project directory, take a look at [direnv](https://direnv.net/). An `.envrc` file is included in this repository. direnv will activate and deactivate virtualenvs for you when you enter and leave the directory.
## Configuration
A sample configuration is included in atst.ini.example.
cp atst.ini.example atst.ini
Be sure to modify it and change the 'secret' key.
`script/config` (called by script/setup) will provide a default configuration.
## Running (development)
To start the app and watch for changes:

8
atst/forms/date.py Normal file
View File

@ -0,0 +1,8 @@
from wtforms.fields.html5 import IntegerField
from wtforms.validators import Required, ValidationError
from wtforms_tornado import Form
class DateForm(Form):
month = IntegerField('Month', validators=[Required()])
day = IntegerField('Day', validators=[Required()])
year = IntegerField('Year', validators=[Required()])

4
atst/forms/funding.py Normal file
View File

@ -0,0 +1,4 @@
from wtforms_tornado import Form
class FundingForm(Form):
pass

View File

@ -0,0 +1,4 @@
from wtforms_tornado import Form
class OrganizationInfoForm(Form):
pass

4
atst/forms/readiness.py Normal file
View File

@ -0,0 +1,4 @@
from wtforms_tornado import Form
class ReadinessForm(Form):
pass

38
atst/forms/request.py Normal file
View File

@ -0,0 +1,38 @@
from wtforms.fields.html5 import IntegerField
from wtforms.fields import RadioField, StringField, SelectField, TextAreaField
from wtforms.validators import Required, ValidationError
from wtforms_tornado import Form
from .date import DateForm
class RequestForm(Form):
application_name = StringField('Application name', validators=[Required()])
application_description = TextAreaField('Application description', validators=[Required()])
dollar_value = IntegerField('Estimated dollar value of use', validators=[Required()])
input_estimate = SelectField('How did you arrive at this estimate?', validators=[Required()],
choices=[('','- Select -'),
('calculator','CSP usage calculator'),
('B','Option B'),
('C','Option C') ])
# no way to apply a label to a whole nested form like this
date_start = DateForm()
period_of_performance = SelectField('Desired period of performance', validators=[Required()],
choices=[('','- Select -'),
('value1','30 days'),
('value2','60 days'),
('value3','90 days') ])
classification_level = RadioField('Classification level', validators=[Required()],
choices=[('unclassified', 'Unclassified'),
('secret', 'Secret'),
('top-secret', 'Top Secret') ])
primary_service_branch = StringField('Primary service branch usage', validators=[Required()])
cloud_model = RadioField('Cloud model service', validators=[Required()],
choices=[('iaas', 'IaaS'),
('paas', 'PaaS'),
('both', 'Both') ])
number_of_cores = IntegerField('Number of cores', validators=[Required()])
total_ram = IntegerField('Total RAM', validators=[Required()])
# this is just an example validation; obviously this is wrong.
def validate_total_ram(self,field):
if (field.data % 2) != 0:
raise ValidationError("RAM must be in increments of 2.")

4
atst/forms/review.py Normal file
View File

@ -0,0 +1,4 @@
from wtforms_tornado import Form
class ReviewForm(Form):
pass

View File

@ -14,7 +14,7 @@ css = Bundle(
assets.register( 'css', css )
helpers = {
'assets': assets
'assets': assets,
}

View File

@ -1,10 +1,16 @@
import tornado
from atst.handler import BaseHandler
from atst.forms.request import RequestForm
from atst.forms.organization_info import OrganizationInfoForm
from atst.forms.funding import FundingForm
from atst.forms.readiness import ReadinessForm
from atst.forms.review import ReviewForm
import tornado.httputil
class RequestNew(BaseHandler):
screens = [
{ 'title' : 'Details of Use',
'form' : RequestForm,
'subitems' : [
{'title' : 'Application Details',
'id' : 'application-details'},
@ -15,10 +21,22 @@ class RequestNew(BaseHandler):
{'title' : 'Usage',
'id' : 'usage' },
]},
{ 'title' : 'Organizational Info', },
{ 'title' : 'Funding/Contracting', },
{ 'title' : 'Readiness Survey', },
{ 'title' : 'Review & Submit', }
{
'title' : 'Organizational Info',
'form' : OrganizationInfoForm,
},
{
'title' : 'Funding/Contracting',
'form' : FundingForm,
},
{
'title' : 'Readiness Survey',
'form' : ReadinessForm,
},
{
'title' : 'Review & Submit',
'form' : ReviewForm,
}
]
def initialize(self, page):
@ -27,18 +45,25 @@ class RequestNew(BaseHandler):
@tornado.web.authenticated
def post(self, screen = 1):
self.check_xsrf_cookie()
all = {
arg: self.get_argument(arg)
for arg in self.request.arguments
if not arg.startswith('_')
}
print( all )
import json
self.write( json.dumps( all ) )
screen = int(screen)
form = self.screens[ screen - 1 ]['form'](self.request.arguments)
print( 'data---------' )
print( form.data )
if form.validate():
where=self.application.default_router.reverse_url('request_form', str(screen + 1))
self.redirect(where)
else:
self.show_form(screen, form)
@tornado.web.authenticated
def get(self, screen = 1):
self.show_form(screen=screen)
def show_form(self, screen = 1, form = None):
if not form:
form = self.screens[ int(screen) - 1 ]['form'](self.request.arguments)
self.render( 'requests/screen-%d.html.to' % int(screen),
f = form,
page = self.page,
screens = self.screens,
current = int(screen),

View File

@ -5,3 +5,5 @@ DEBUG = true
AUTHZ_BASE_URL = http://localhost
AUTHNID_BASE_URL= http://localhost
COOKIE_SECRET = some-secret-please-replace
SECRET = change_me_into_something_secret
CAC_URL = http://localhost:8888/home

2878
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +0,0 @@
cp atst.ini.example atst.ini
rand=`head -c 400 /dev/random | tr -dc A-Za-z0-9_=,@-`
perl -p -i -e "s/change_me_into_something_secret/$rand/" atst.ini

View File

@ -24,8 +24,6 @@ fi
# Install application dependencies
script/bootstrap
# Generate default configuration
script/config
# Symlink uswds fonts into the /static directory
rm -f ./static/fonts
ln -s ../node_modules/uswds/src/fonts ./static/fonts

View File

@ -17,7 +17,7 @@
<h1 class="usa-display">JEDI</h1>
<a class="usa-button" href='{{ config.get('cac_url','https://cac.atat.codes') }}'><span>Sign In with CAC</span></a>
<a class="usa-button" href='{{ config['default'].get('cac_url','https://cac.atat.codes') }}'><span>Sign In with CAC</span></a>
<button class="usa-button" disabled>Sign In via MFA</button>
</main>

View File

@ -0,0 +1,24 @@
{% extends '../requests_new.html.to' %}
{% block form %}
form goes here
<br>
{% if f.errors %}
<b>There were some errors</b>
{% end %}
<hr>
{% autoescape None %}
{% for e in f.a.errors %}
<b>{{ e }}</b>
{% end %}
<br>
a: {{ f.a }}
<hr>
b: {{ f.b }}
<br>
{% end %}

View File

@ -2,102 +2,70 @@
{% block form %}
<h2>Details of Use</h2>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Doloremque placeat distinctio accusamus quo temporibus facilis, dicta delectus asperiores. Nihil aut quod quibusdam id fugit, officia dolorum laudantium! Quidem tempora, aliquam.</p>
{% autoescape None %}
{% if f.errors %}
<b>There were some errors, see below.</b>
{% end %}
<h3 id="application-details">Application Details</h3>
<p>These headings introduce, respectively, sections and subsections within your body copy. As you create these headings, follow the same guidelines that you use when writing section headings: Be succinct, descriptive, and precise.</p>
<button class="usa-button-secondary usa-button-active">New Application</button>
<button class="usa-button-secondary" disabled>Existing Application</button>
<button class="usa-button-secondary" disabled>Sandbox Application</button>
<label for="input-application-name">Application name</label>
<input id="input-application-name" name="input-application-name" type="text" placeholder="What is the application name?">
{{ f.application_name.label }}
{{ f.application_name(placeholder="What is the application name?") }}
<label for="input-application-description">Application description</label>
<textarea id="input-application-description" name="input-application-description" placeholder="Describe the application"></textarea>
{{ f.application_description.label }}
{{ f.application_description(placeholder="Describe the application") }}
<label for="input-type-textarea">Estimated dollar value of use</label>
<input id="dollar-value" name="dollar-value" type="text" placeholder="$">
{{ f.dollar_value.label }}
{{ f.dollar_value(placeholder="$") }}
<label for="input-estimate">How did you arrive at this estimate?</label>
<select name="input-estimate" id="input-estimate">
<option value="">- Select -</option>
<option value="value1">CSP usage calculator</option>
<option value="value2">Option B</option>
<option value="value3">Option C</option>
</select>
{{ f.input_estimate.label }}
{{ f.input_estimate }}
<b>NEW</b>
<hr>
<fieldset>
<label for="input-start-date">Expected start date</label>
<div class="usa-date-of-birth">
<div class="usa-form-group usa-form-group-month">
<label for="date_start_1">Month</label>
<input class="usa-input-inline" aria-describedby="dobHint" id="date_start_1" name="date_start_1" type="number" min="1" max="12" value="">
{{ f.date_start.month.label }}
{{ f.date_start.month(min="1", max="12") }}
</div>
<div class="usa-form-group usa-form-group-day">
<label for="date_start_2">Day</label>
<input class="usa-input-inline" aria-describedby="dobHint" id="date_start_2" name="date_start_2" type="number" min="1" max="31" value="">
{{ f.date_start.day.label }}
{{ f.date_start.day(min="1", max="31") }}
</div>
<div class="usa-form-group usa-form-group-year">
<label for="date_start_3">Year</label>
<input class="usa-input-inline" aria-describedby="dobHint" id="date_start_3" name="date_start_3" type="number" min="1900" max="2000" value="">
{{ f.date_start.year.label }}
{{ f.date_start.year(min="2000", max="2040") }}
</div>
</div>
</fieldset>
<label for="input-period-performance">Desired period of performance</label>
<select name="input-period-performance" id="input-period-performance">
<option value="">- Select -</option>
<option value="value1">30 days</option>
<option value="value2">60 days</option>
<option value="value3">90 days</option>
</select>
{{ f.period_of_performance.label }}
{{ f.period_of_performance }}
<br>
<fieldset class="usa-fieldset-inputs usa-sans">
<label for="input-period-performance">Classification level</label>
<ul class="usa-unstyled-list">
<li>
<input id="unclassified" type="radio" checked name="classification-level" value="unclassified">
<label for="unclassified">Unclassified</label>
</li>
<li>
<input id="secret" type="radio" name="classification-level" value="secret">
<label for="secret">Secret</label>
</li>
<li>
<input id="top-secret" type="radio" name="classification-level" value="top-secret">
<label for="top-secret">Top Secret</label>
</li>
</ul>
{{ f.classification_level.label }}
{{ f.classification_level(class_="usa-unstyled-list") }}
</fieldset>
<label for="input-service-branch">Primary service branch usage</label>
<input id="service-branch" name="service-branch" type="text" placeholder="Add tags associated with service branches">
{{ f.primary_service_branch.label }}
{{ f.primary_service_branch(placeholder="Add tags associated with service branches") }}
<br>
<fieldset class="usa-fieldset-inputs usa-sans">
<label for="input-cloud-model">Cloud model service</label>
<ul class="usa-unstyled-list">
<li>
<input id="iaas" type="radio" name="cloud-level" value="iaas">
<label for="iaas">IaaS</label>
</li>
<li>
<input id="paas" type="radio" name="cloud-level" value="paas">
<label for="paas">PaaS</label>
</li>
<li>
<input id="iass-pass" type="radio" checked name="cloud-level" value="iass-pass">
<label for="iass-pass">Both</label>
</li>
</ul>
{{ f.cloud_model.label }}
{{ f.cloud_model(class_="usa-unstyled-list") }}
</fieldset>
@ -106,12 +74,18 @@
<h4 id="application-details">Computation</h4>
<p>These headings introduce, respectively, sections and subsections within your body copy. As you create these headings, follow the same guidelines that you use when writing section headings: Be succinct, descriptive, and precise.</p>
<label for="">Number of cores</label>
<input id="" name="" type="text" placeholder="Total cores">
<label for="">Total RAM</label>
<input id="" name="" type="text" placeholder="Amount of RAM">
{{ f.number_of_cores.label }}
{{ f.number_of_cores(placeholder="Total cores", min="1", max="32") }}
<!-- example field with custom validation -->
{{ f.total_ram.label }}
{{ f.total_ram(placeholder="Total RAM", min="1", max="32") }}
<!-- example validation errors -->
{% for e in f.total_ram.errors %}
<div class="usa-input-error-message">
{{ e }}
</div>
{% end %}
<!-- Storage -->

View File

@ -0,0 +1,14 @@
import wtforms
import pytest
from atst.forms.request import RequestForm
form = RequestForm()
def test_form_has_expected_fields():
label = form.application_name.label
assert label.text == 'Application name'
def test_form_can_validate_total_ram():
form.application_name.data = 5
with pytest.raises(wtforms.validators.ValidationError):
form.validate_total_ram(form.application_name)

View File

@ -0,0 +1,31 @@
import re
import pytest
ERROR_CLASS = 'usa-input-error-message'
@pytest.mark.gen_test
def test_submit_invalid_request_form(monkeypatch, http_client, base_url):
monkeypatch.setattr('atst.handlers.request_new.RequestNew.check_xsrf_cookie', lambda s: True)
# this just needs to send a known invalid form value
response = yield http_client.fetch(
base_url + "/requests/new",
method="POST",
headers={"Content-Type": "application/x-www-form-urlencoded"},
body="total_ram=5",
)
assert response.effective_url == base_url + '/requests/new'
assert re.search(ERROR_CLASS, response.body.decode())
@pytest.mark.gen_test
def test_submit_valid_request_form(monkeypatch, http_client, base_url):
monkeypatch.setattr('atst.handlers.request_new.RequestNew.check_xsrf_cookie', lambda s: True)
monkeypatch.setattr('atst.forms.request.RequestForm.validate', lambda s: True)
# this just needs to send a known invalid form value
response = yield http_client.fetch(
base_url + "/requests/new",
method="POST",
headers={"Content-Type": "application/x-www-form-urlencoded"},
body="meaning=42",
)
assert response.effective_url == base_url + '/requests/new/2'