Update request form (#45)
* Implement OrganizationInfo form, add it to the template * Format request_new * Update "Details of Use" section * Refactor request_new * Added some new fields, but form is still WIP * Add details of use fields * Add org info fields * Add some comments * Add Financial Verification and more Details of Use fields * Update some textarea fields to single text field * WIP * Implement OrganizationInfo form, add it to the template * Format request_new * Update "Details of Use" section * Refactor request_new * Added some new fields, but form is still WIP * Add details of use fields * Add org info fields * Add some comments * Add Financial Verification and more Details of Use fields * Update some textarea fields to single text field * Format * Update fields with the correct fieldtypes * Begin updating sidenav changes * Split form into each section * adjust and skip some outdated form validation tests * break request form into multiple form objects * need to send user ID to requests-queue * use DateForm for start date in request * alter request_new handler to pass raw form data to template * change review form * Add KO and COR section titles * Update date input class name * Add a special case for the summary form. We should refactor this * Add read-only fields for review and submit section * Add minimum number validators to request form * Fix formatting * Use html5 datepicker for dates * Fix request form validators * Finish org info form * Finish POC form * Finish financial verification form * Move PE and UII to financial form * Un-skip form validation test
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import tornado.gen
|
||||
from tornado.httpclient import AsyncHTTPClient
|
||||
from json import dumps, loads
|
||||
from json import dumps, loads, decoder
|
||||
|
||||
|
||||
class ApiClient(object):
|
||||
@@ -49,7 +49,12 @@ class ApiClient(object):
|
||||
|
||||
def adapt_response(self, response):
|
||||
if "application/json" in response.headers["Content-Type"]:
|
||||
json = loads(response.body)
|
||||
setattr(response, "json", json)
|
||||
try:
|
||||
json = loads(response.body)
|
||||
setattr(response, "json", json)
|
||||
except decoder.JSONDecodeError:
|
||||
setattr(response, "json", {})
|
||||
else:
|
||||
setattr(response, "json", {})
|
||||
setattr(response, "ok", 200 <= response.code < 300)
|
||||
return response
|
||||
|
@@ -1,9 +0,0 @@
|
||||
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()])
|
34
atst/forms/fields.py
Normal file
34
atst/forms/fields.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from wtforms.fields.html5 import DateField
|
||||
from wtforms.fields import Field
|
||||
from wtforms.widgets import TextArea
|
||||
import pendulum
|
||||
|
||||
|
||||
class DateField(DateField):
|
||||
def _value(self):
|
||||
if self.data:
|
||||
return pendulum.parse(self.data).date()
|
||||
else:
|
||||
return None
|
||||
|
||||
def process_formdata(self, values):
|
||||
if values:
|
||||
self.data = values[0]
|
||||
else:
|
||||
self.data = []
|
||||
|
||||
|
||||
class NewlineListField(Field):
|
||||
widget = TextArea()
|
||||
|
||||
def _value(self):
|
||||
if self.data:
|
||||
return "\n".join(self.data)
|
||||
else:
|
||||
return ""
|
||||
|
||||
def process_formdata(self, valuelist):
|
||||
if valuelist:
|
||||
self.data = [l.strip() for l in valuelist[0].split("\n")]
|
||||
else:
|
||||
self.data = []
|
87
atst/forms/financial.py
Normal file
87
atst/forms/financial.py
Normal file
@@ -0,0 +1,87 @@
|
||||
from wtforms.fields.html5 import EmailField
|
||||
from wtforms.fields import StringField, SelectField
|
||||
from wtforms.validators import Required, Email
|
||||
from wtforms_tornado import Form
|
||||
|
||||
from .fields import NewlineListField
|
||||
|
||||
|
||||
class FinancialForm(Form):
|
||||
task_order_id = StringField(
|
||||
"Task Order Number associated with this request.", validators=[Required()]
|
||||
)
|
||||
|
||||
uii_ids = NewlineListField(
|
||||
"Please enter the Unique Item Identifier (UII)s related to your application(s) if you already have them."
|
||||
)
|
||||
|
||||
pe_id = NewlineListField(
|
||||
"Please provide the Program Element (PE) Numbers related to your request"
|
||||
)
|
||||
|
||||
fname_co = StringField("Contracting Officer First Name", validators=[Required()])
|
||||
lname_co = StringField("Contracting Officer Last Name", validators=[Required()])
|
||||
|
||||
email_co = EmailField("Contracting Officer Email", validators=[Required(), Email()])
|
||||
|
||||
office_co = StringField("Contracting Office Office", validators=[Required()])
|
||||
|
||||
fname_cor = StringField(
|
||||
"Contracting Officer Representative (COR) First Name", validators=[Required()]
|
||||
)
|
||||
|
||||
lname_cor = StringField(
|
||||
"Contracting Officer Representative (COR) Last Name", validators=[Required()]
|
||||
)
|
||||
|
||||
email_cor = EmailField(
|
||||
"Contracting Officer Representative (COR) Email",
|
||||
validators=[Required(), Email()],
|
||||
)
|
||||
|
||||
office_cor = StringField(
|
||||
"Contracting Officer Representative (COR) Office", validators=[Required()]
|
||||
)
|
||||
|
||||
funding_type = SelectField(
|
||||
validators=[Required()],
|
||||
choices=[
|
||||
("", "- Select -"),
|
||||
("RDTE", "Research, Development, Testing & Evaluation (RDT&E)"),
|
||||
("OM", "Operations & Maintenance (O&M)"),
|
||||
("PROC", "Procurement (PROC)"),
|
||||
("OTHER", "Other"),
|
||||
],
|
||||
)
|
||||
|
||||
funding_type_other = StringField(
|
||||
"If other, please specify", validators=[Required()]
|
||||
)
|
||||
|
||||
clin_0001 = StringField(
|
||||
"CLIN 0001 - Unclassified IaaS and PaaS Amount", validators=[Required()]
|
||||
)
|
||||
|
||||
clin_0003 = StringField(
|
||||
"CLIN 0003 - Unclassified Cloud Support Package", validators=[Required()]
|
||||
)
|
||||
|
||||
clin_1001 = StringField(
|
||||
"CLIN 1001 - Unclassified IaaS and PaaS Amount OPTION PERIOD 1",
|
||||
validators=[Required()],
|
||||
)
|
||||
|
||||
clin_1003 = StringField(
|
||||
"CLIN 1003 - Unclassified Cloud Support Package OPTION PERIOD 1",
|
||||
validators=[Required()],
|
||||
)
|
||||
|
||||
clin_2001 = StringField(
|
||||
"CLIN 2001 - Unclassified IaaS and PaaS Amount OPTION PERIOD 2",
|
||||
validators=[Required()],
|
||||
)
|
||||
|
||||
clin_2003 = StringField(
|
||||
"CLIN 2003 - Unclassified Cloud Support Package OPTION PERIOD 2",
|
||||
validators=[Required()],
|
||||
)
|
@@ -1,5 +0,0 @@
|
||||
from wtforms_tornado import Form
|
||||
|
||||
|
||||
class FundingForm(Form):
|
||||
pass
|
34
atst/forms/org.py
Normal file
34
atst/forms/org.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from wtforms.fields.html5 import EmailField, TelField
|
||||
from wtforms.fields import RadioField, StringField
|
||||
from wtforms.validators import Required, Length, Email
|
||||
from wtforms_tornado import Form
|
||||
from .fields import DateField
|
||||
|
||||
|
||||
class OrgForm(Form):
|
||||
fname_request = StringField("First Name", validators=[Required()])
|
||||
lname_request = StringField("Last Name", validators=[Required()])
|
||||
|
||||
email_request = EmailField(
|
||||
"Email (associated with your CAC)", validators=[Required(), Email()]
|
||||
)
|
||||
|
||||
phone_number = TelField("Phone Number", validators=[Required(), Length(min=7)])
|
||||
|
||||
service_branch = StringField("Service Branch or Agency", validators=[Required()])
|
||||
|
||||
citizenship = RadioField(
|
||||
choices=[
|
||||
("United States", "United States"),
|
||||
("Foreign National", "Foreign National"),
|
||||
("Other", "Other"),
|
||||
],
|
||||
validators=[Required()],
|
||||
)
|
||||
|
||||
designation = StringField("Designation of Person", validators=[Required()])
|
||||
|
||||
date_latest_training = DateField(
|
||||
"Latest Information Assurance (IA) Training completion date.",
|
||||
validators=[Required()],
|
||||
)
|
@@ -1,5 +0,0 @@
|
||||
from wtforms_tornado import Form
|
||||
|
||||
|
||||
class OrganizationInfoForm(Form):
|
||||
pass
|
17
atst/forms/poc.py
Normal file
17
atst/forms/poc.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from wtforms.fields import StringField
|
||||
from wtforms.validators import Required, Email, Length
|
||||
from wtforms_tornado import Form
|
||||
from .validators import IsNumber
|
||||
|
||||
|
||||
class POCForm(Form):
|
||||
fname_poc = StringField("POC First Name", validators=[Required()])
|
||||
lname_poc = StringField("POC Last Name", validators=[Required()])
|
||||
|
||||
email_poc = StringField(
|
||||
"POC Email (associated with CAC)", validators=[Required(), Email()]
|
||||
)
|
||||
|
||||
dodid_poc = StringField(
|
||||
"DOD ID", validators=[Required(), Length(min=10), IsNumber()]
|
||||
)
|
@@ -1,5 +0,0 @@
|
||||
from wtforms_tornado import Form
|
||||
|
||||
|
||||
class ReadinessForm(Form):
|
||||
pass
|
@@ -1,73 +1,87 @@
|
||||
from wtforms.fields.html5 import IntegerField
|
||||
from wtforms.fields import (
|
||||
RadioField,
|
||||
StringField,
|
||||
SelectField,
|
||||
TextAreaField,
|
||||
FormField,
|
||||
)
|
||||
from wtforms.validators import Required, ValidationError
|
||||
from wtforms.fields import RadioField, StringField, TextAreaField
|
||||
from wtforms.validators import NumberRange, InputRequired
|
||||
from wtforms_tornado import Form
|
||||
from .date import DateForm
|
||||
from .fields import DateField, NewlineListField
|
||||
from .validators import DateRange
|
||||
import pendulum
|
||||
|
||||
|
||||
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 = FormField(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()])
|
||||
object_storage = IntegerField("Total object storage", validators=[Required()])
|
||||
server_storage = IntegerField("Total server storage", validators=[Required()])
|
||||
total_active_users = IntegerField("Total active users", validators=[Required()])
|
||||
total_peak_users = IntegerField("Total peak users", validators=[Required()])
|
||||
total_requests = IntegerField("Total requests", validators=[Required()])
|
||||
total_environments = IntegerField("Total environments", 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.")
|
||||
# Details of Use: Overall Request Details
|
||||
dollar_value = IntegerField(
|
||||
"What is the total estimated dollar value of the cloud resources you are requesting using the JEDI CSP Calculator? ",
|
||||
validators=[InputRequired(), NumberRange(min=1)],
|
||||
)
|
||||
|
||||
num_applications = IntegerField(
|
||||
"Please estimate the number of applications that might be supported by this request",
|
||||
validators=[InputRequired(), NumberRange(min=1)],
|
||||
)
|
||||
|
||||
date_start = DateField(
|
||||
"Date you expect to start accessing this cloud resource.",
|
||||
validators=[
|
||||
InputRequired(),
|
||||
DateRange(
|
||||
lower_bound=pendulum.duration(days=0),
|
||||
message="Must be no earlier than today.",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
app_description = TextAreaField(
|
||||
"Please briefly describe how your team is expecting to use the JEDI Cloud"
|
||||
)
|
||||
|
||||
supported_organizations = StringField(
|
||||
"What organizations are supported by these applications?",
|
||||
validators=[InputRequired()],
|
||||
)
|
||||
|
||||
# Details of Use: Cloud Resources
|
||||
total_cores = IntegerField(
|
||||
"Total Number of vCPU cores", validators=[InputRequired(), NumberRange(min=0)]
|
||||
)
|
||||
total_ram = IntegerField(
|
||||
"Total RAM", validators=[InputRequired(), NumberRange(min=0)]
|
||||
)
|
||||
total_object_storage = IntegerField(
|
||||
"Total object storage", validators=[InputRequired(), NumberRange(min=0)]
|
||||
)
|
||||
total_database_storage = IntegerField(
|
||||
"Total database storage", validators=[InputRequired(), NumberRange(min=0)]
|
||||
)
|
||||
total_server_storage = IntegerField(
|
||||
"Total server storage", validators=[InputRequired(), NumberRange(min=0)]
|
||||
)
|
||||
|
||||
# Details of Use: Support Staff
|
||||
has_contractor_advisor = RadioField(
|
||||
"Do you have a contractor to advise and assist you with using cloud services?",
|
||||
choices=[("yes", "Yes"), ("no", "No")],
|
||||
validators=[InputRequired()],
|
||||
)
|
||||
|
||||
is_migrating_application = RadioField(
|
||||
"Are you using the JEDI Cloud to migrate existing applications?",
|
||||
choices=[("yes", "Yes"), ("no", "No")],
|
||||
validators=[InputRequired()],
|
||||
)
|
||||
|
||||
supporting_organization = TextAreaField(
|
||||
"Please describe the organizations that are supporting you, include both government and contractor resources",
|
||||
validators=[InputRequired()],
|
||||
)
|
||||
|
||||
has_migration_office = RadioField(
|
||||
"Do you have a migration office that you're working with to migrate to the cloud?",
|
||||
choices=[("yes", "Yes"), ("no", "No")],
|
||||
validators=[InputRequired()],
|
||||
)
|
||||
|
||||
supporting_organization = StringField(
|
||||
"Please describe the organizations that are supporting you, include both government and contractor resources.",
|
||||
validators=[InputRequired()],
|
||||
)
|
||||
|
@@ -1,5 +1,6 @@
|
||||
from wtforms.fields import BooleanField
|
||||
from wtforms_tornado import Form
|
||||
|
||||
|
||||
class ReviewForm(Form):
|
||||
pass
|
||||
reviewed = BooleanField("I have reviewed this data and it is correct.")
|
||||
|
29
atst/forms/validators.py
Normal file
29
atst/forms/validators.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from wtforms.validators import ValidationError
|
||||
import pendulum
|
||||
|
||||
|
||||
def DateRange(lower_bound=None, upper_bound=None, message=None):
|
||||
def _date_range(form, field):
|
||||
now = pendulum.now().date()
|
||||
|
||||
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)
|
||||
|
||||
return _date_range
|
||||
|
||||
|
||||
def IsNumber(message="Please enter a valid number."):
|
||||
def _is_number(form, field):
|
||||
try:
|
||||
int(field.data)
|
||||
except ValueError:
|
||||
raise ValidationError(message)
|
||||
|
||||
return _is_number
|
@@ -1,10 +1,10 @@
|
||||
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.org import OrgForm
|
||||
from atst.forms.poc import POCForm
|
||||
from atst.forms.review import ReviewForm
|
||||
from atst.forms.financial import FinancialForm
|
||||
import tornado.httputil
|
||||
|
||||
|
||||
@@ -12,18 +12,30 @@ class RequestNew(BaseHandler):
|
||||
screens = [
|
||||
{
|
||||
"title": "Details of Use",
|
||||
"section": "details_of_use",
|
||||
"form": RequestForm,
|
||||
"subitems": [
|
||||
{"title": "Application Details", "id": "application-details"},
|
||||
{"title": "Computation", "id": "computation"},
|
||||
{"title": "Storage", "id": "storage"},
|
||||
{"title": "Usage", "id": "usage"},
|
||||
{"title": "Overall request details", "id": "overall-request-details"},
|
||||
{"title": "Cloud Resources", "id": "cloud-resources"},
|
||||
{"title": "Support Staff", "id": "support-staff"},
|
||||
],
|
||||
},
|
||||
{"title": "Organizational Info", "form": OrganizationInfoForm},
|
||||
{"title": "Funding/Contracting", "form": FundingForm},
|
||||
{"title": "Readiness Survey", "form": ReadinessForm},
|
||||
{"title": "Review & Submit", "form": ReviewForm},
|
||||
{
|
||||
"title": "Information About You",
|
||||
"section": "information_about_you",
|
||||
"form": OrgForm,
|
||||
},
|
||||
{
|
||||
"title": "Primary Point of Contact",
|
||||
"section": "primary_poc",
|
||||
"form": POCForm,
|
||||
},
|
||||
{"title": "Review & Submit", "section": "review_submit", "form": ReviewForm},
|
||||
{
|
||||
"title": "Financial Verification",
|
||||
"section": "financial_verification",
|
||||
"form": FinancialForm,
|
||||
},
|
||||
]
|
||||
|
||||
def initialize(self, page, requests_client):
|
||||
@@ -35,9 +47,14 @@ class RequestNew(BaseHandler):
|
||||
def post(self, screen=1, request_id=None):
|
||||
self.check_xsrf_cookie()
|
||||
screen = int(screen)
|
||||
form = self.screens[screen - 1]["form"](self.request.arguments)
|
||||
form_metadata = self.screens[screen - 1]
|
||||
form_section = form_metadata["section"]
|
||||
form = form_metadata["form"](self.request.arguments)
|
||||
|
||||
if form.validate():
|
||||
response = yield self.create_or_update_request(form.data, request_id)
|
||||
response = yield self.create_or_update_request(
|
||||
form_section, form.data, request_id
|
||||
)
|
||||
if response.ok:
|
||||
where = self.application.default_router.reverse_url(
|
||||
"request_form_update",
|
||||
@@ -54,20 +71,29 @@ class RequestNew(BaseHandler):
|
||||
@tornado.gen.coroutine
|
||||
def get(self, screen=1, request_id=None):
|
||||
form = None
|
||||
form_data = None
|
||||
is_review_section = screen == 4
|
||||
|
||||
if request_id:
|
||||
request = yield self.get_request(request_id)
|
||||
if request.ok:
|
||||
form_data = request.json["body"] if request else {}
|
||||
form = self.screens[int(screen) - 1]["form"](data=form_data)
|
||||
if is_review_section:
|
||||
form_data = request.json["body"]
|
||||
else:
|
||||
form_metadata = self.screens[int(screen) - 1]
|
||||
section = form_metadata["section"]
|
||||
form_data = request.json["body"].get(section, request.json["body"])
|
||||
form = form_metadata["form"](data=form_data)
|
||||
|
||||
self.show_form(screen=screen, form=form, request_id=request_id)
|
||||
self.show_form(screen=screen, form=form, request_id=request_id, data=form_data)
|
||||
|
||||
def show_form(self, screen=1, form=None, request_id=None):
|
||||
def show_form(self, screen=1, form=None, request_id=None, data=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,
|
||||
data=data,
|
||||
page=self.page,
|
||||
screens=self.screens,
|
||||
current=int(screen),
|
||||
@@ -78,16 +104,16 @@ class RequestNew(BaseHandler):
|
||||
@tornado.gen.coroutine
|
||||
def get_request(self, request_id):
|
||||
request = yield self.requests_client.get(
|
||||
"/users/{}/requests/{}".format(self.get_current_user(), request_id),
|
||||
"/users/{}/requests/{}".format(self.get_current_user()["id"], request_id),
|
||||
raise_error=False,
|
||||
)
|
||||
return request
|
||||
|
||||
@tornado.gen.coroutine
|
||||
def create_or_update_request(self, form_data, request_id=None):
|
||||
def create_or_update_request(self, form_section, form_data, request_id=None):
|
||||
request_data = {
|
||||
"creator_id": self.get_current_user()["id"],
|
||||
"request": form_data,
|
||||
"request": {form_section: form_data},
|
||||
}
|
||||
if request_id:
|
||||
response = yield self.requests_client.patch(
|
||||
|
Reference in New Issue
Block a user