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 # Compiled python bytecode files
*.pyc *.pyc
.cache/ .cache/
atst.ini
# static/fonts for now, since it is just symlink # static/fonts for now, since it is just symlink
static/fonts static/fonts

View File

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

16
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "391e254ddb902877afca9c07aa2306710ce6d1e207b029c1a8b5dc0115ee99a5" "sha256": "2299d6143992989417d01bd11d7d99a8a239a388321dc1546815d8b49c8151b8"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@ -41,6 +41,20 @@
], ],
"index": "pypi", "index": "pypi",
"version": "==0.12.1" "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": { "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. 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) ## Running (development)
To start the app and watch for changes: 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 ) assets.register( 'css', css )
helpers = { helpers = {
'assets': assets 'assets': assets,
} }

View File

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

View File

@ -5,3 +5,5 @@ DEBUG = true
AUTHZ_BASE_URL = http://localhost AUTHZ_BASE_URL = http://localhost
AUTHNID_BASE_URL= http://localhost AUTHNID_BASE_URL= http://localhost
COOKIE_SECRET = some-secret-please-replace 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 # Install application dependencies
script/bootstrap script/bootstrap
# Generate default configuration
script/config
# Symlink uswds fonts into the /static directory # Symlink uswds fonts into the /static directory
rm -f ./static/fonts
ln -s ../node_modules/uswds/src/fonts ./static/fonts ln -s ../node_modules/uswds/src/fonts ./static/fonts

View File

@ -17,7 +17,7 @@
<h1 class="usa-display">JEDI</h1> <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> <button class="usa-button" disabled>Sign In via MFA</button>
</main> </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 %} {% block form %}
<h2>Details of Use</h2> <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> <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 usa-button-active">New Application</button>
<button class="usa-button-secondary" disabled>Existing Application</button> <button class="usa-button-secondary" disabled>Existing Application</button>
<button class="usa-button-secondary" disabled>Sandbox Application</button> <button class="usa-button-secondary" disabled>Sandbox Application</button>
<label for="input-application-name">Application name</label> {{ f.application_name.label }}
<input id="input-application-name" name="input-application-name" type="text" placeholder="What is the application name?"> {{ f.application_name(placeholder="What is the application name?") }}
<label for="input-application-description">Application description</label> {{ f.application_description.label }}
<textarea id="input-application-description" name="input-application-description" placeholder="Describe the application"></textarea> {{ f.application_description(placeholder="Describe the application") }}
<label for="input-type-textarea">Estimated dollar value of use</label> {{ f.dollar_value.label }}
<input id="dollar-value" name="dollar-value" type="text" placeholder="$"> {{ f.dollar_value(placeholder="$") }}
<label for="input-estimate">How did you arrive at this estimate?</label> {{ f.input_estimate.label }}
<select name="input-estimate" id="input-estimate"> {{ f.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>
<b>NEW</b>
<hr>
<fieldset> <fieldset>
<label for="input-start-date">Expected start date</label> <label for="input-start-date">Expected start date</label>
<div class="usa-date-of-birth"> <div class="usa-date-of-birth">
<div class="usa-form-group usa-form-group-month"> <div class="usa-form-group usa-form-group-month">
<label for="date_start_1">Month</label> {{ f.date_start.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(min="1", max="12") }}
</div> </div>
<div class="usa-form-group usa-form-group-day"> <div class="usa-form-group usa-form-group-day">
<label for="date_start_2">Day</label> {{ f.date_start.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(min="1", max="31") }}
</div> </div>
<div class="usa-form-group usa-form-group-year"> <div class="usa-form-group usa-form-group-year">
<label for="date_start_3">Year</label> {{ f.date_start.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(min="2000", max="2040") }}
</div> </div>
</div> </div>
</fieldset> </fieldset>
{{ f.period_of_performance.label }}
<label for="input-period-performance">Desired period of performance</label> {{ f.period_of_performance }}
<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>
<br> <br>
<fieldset class="usa-fieldset-inputs usa-sans"> <fieldset class="usa-fieldset-inputs usa-sans">
<label for="input-period-performance">Classification level</label> {{ f.classification_level.label }}
<ul class="usa-unstyled-list"> {{ f.classification_level(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>
</fieldset> </fieldset>
{{ f.primary_service_branch.label }}
<label for="input-service-branch">Primary service branch usage</label> {{ f.primary_service_branch(placeholder="Add tags associated with service branches") }}
<input id="service-branch" name="service-branch" type="text" placeholder="Add tags associated with service branches">
<br> <br>
<fieldset class="usa-fieldset-inputs usa-sans"> <fieldset class="usa-fieldset-inputs usa-sans">
<label for="input-cloud-model">Cloud model service</label> {{ f.cloud_model.label }}
<ul class="usa-unstyled-list"> {{ f.cloud_model(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>
</fieldset> </fieldset>
@ -106,12 +74,18 @@
<h4 id="application-details">Computation</h4> <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> <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> {{ f.number_of_cores.label }}
<input id="" name="" type="text" placeholder="Total cores"> {{ f.number_of_cores(placeholder="Total cores", min="1", max="32") }}
<label for="">Total RAM</label>
<input id="" name="" type="text" placeholder="Amount of RAM">
<!-- 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 --> <!-- 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'