diff --git a/atst/forms/data.py b/atst/forms/data.py new file mode 100644 index 00000000..8e55ff33 --- /dev/null +++ b/atst/forms/data.py @@ -0,0 +1,48 @@ +SERVICE_BRANCHES = [ + (None, "Select an option"), + ("Air Force, Department of the", "Air Force, Department of the"), + ("Army and Air Force Exchange Service", "Army and Air Force Exchange Service"), + ("Army, Department of the", "Army, Department of the"), + ("Defense Advanced Research Projects Agency", "Defense Advanced Research Projects Agency"), + ("Defense Commissary Agency", "Defense Commissary Agency"), + ("Defense Contract Audit Agency", "Defense Contract Audit Agency"), + ("Defense Contract Management Agency", "Defense Contract Management Agency"), + ("Defense Finance & Accounting Service", "Defense Finance & Accounting Service"), + ("Defense Health Agency", "Defense Health Agency"), + ("Defense Information System Agency", "Defense Information System Agency"), + ("Defense Intelligence Agency", "Defense Intelligence Agency"), + ("Defense Legal Services Agency", "Defense Legal Services Agency"), + ("Defense Logistics Agency", "Defense Logistics Agency"), + ("Defense Media Activity", "Defense Media Activity"), + ("Defense Micro Electronics Activity", "Defense Micro Electronics Activity"), + ("Defense POW-MIA Accounting Agency", "Defense POW-MIA Accounting Agency"), + ("Defense Security Cooperation Agency", "Defense Security Cooperation Agency"), + ("Defense Security Service", "Defense Security Service"), + ("Defense Technical Information Center", "Defense Technical Information Center"), + ("Defense Technology Security Administration", "Defense Technology Security Administration"), + ("Defense Threat Reduction Agency", "Defense Threat Reduction Agency"), + ("DoD Education Activity", "DoD Education Activity"), + ("DoD Human Recourses Activity", "DoD Human Recourses Activity"), + ("DoD Inspector General", "DoD Inspector General"), + ("DoD Test Resource Management Center", "DoD Test Resource Management Center"), + ("Headquarters Defense Human Resource Activity ", "Headquarters Defense Human Resource Activity "), + ("Joint Staff", "Joint Staff"), + ("Missile Defense Agency", "Missile Defense Agency"), + ("National Defense University", "National Defense University"), + ("National Geospatial Intelligence Agency (NGA)", "National Geospatial Intelligence Agency (NGA)"), + ("National Oceanic and Atmospheric Administration (NOAA)", "National Oceanic and Atmospheric Administration (NOAA)"), + ("National Reconnaissance Office", "National Reconnaissance Office"), + ("National Reconnaissance Office (NRO)", "National Reconnaissance Office (NRO)"), + ("National Security Agency (NSA)", "National Security Agency (NSA)"), + ("National Security Agency-Central Security Service", "National Security Agency-Central Security Service"), + ("Navy, Department of the", "Navy, Department of the"), + ("Office of Economic Adjustment", "Office of Economic Adjustment"), + ("Office of the Secretary of Defense", "Office of the Secretary of Defense"), + ("Pentagon Force Protection Agency", "Pentagon Force Protection Agency"), + ("Uniform Services University of the Health Sciences", "Uniform Services University of the Health Sciences"), + ("US Cyber Command (USCYBERCOM)", "US Cyber Command (USCYBERCOM)"), + ("US Special Operations Command (USSOCOM)", "US Special Operations Command (USSOCOM)"), + ("US Strategic Command (USSTRATCOM)", "US Strategic Command (USSTRATCOM)"), + ("US Transportation Command (USTRANSCOM)", "US Transportation Command (USTRANSCOM)"), + ("Washington Headquarters Services", "Washington Headquarters Services"), +] diff --git a/atst/forms/fields.py b/atst/forms/fields.py index 2c06154a..9ac4371b 100644 --- a/atst/forms/fields.py +++ b/atst/forms/fields.py @@ -1,5 +1,5 @@ from wtforms.fields.html5 import DateField -from wtforms.fields import Field +from wtforms.fields import Field, SelectField as SelectField_ from wtforms.widgets import TextArea from atst.domain.date import parse_date @@ -32,6 +32,22 @@ class NewlineListField(Field): def process_formdata(self, valuelist): if valuelist: - self.data = [l.strip() for l in valuelist[0].split("\n")] + self.data = [l.strip() for l in valuelist[0].split("\n") if l] else: self.data = [] + + def process_data(self, value): + if isinstance(value, list): + self.data = "\n".join(value) + else: + self.data = value + + +class SelectField(SelectField_): + def __init__(self, *args, **kwargs): + render_kw = kwargs.get("render_kw", {}) + kwargs["render_kw"] = { + **render_kw, + "required": False + } + super().__init__(*args, **kwargs) diff --git a/atst/forms/financial.py b/atst/forms/financial.py index 994e84bf..42a29d39 100644 --- a/atst/forms/financial.py +++ b/atst/forms/financial.py @@ -1,12 +1,12 @@ import re from wtforms.fields.html5 import EmailField -from wtforms.fields import StringField, SelectField -from wtforms.validators import Required, Email +from wtforms.fields import StringField +from wtforms.validators import Required, Email, Regexp from atst.domain.exceptions import NotFoundError from atst.domain.pe_numbers import PENumbers -from .fields import NewlineListField +from .fields import NewlineListField, SelectField from .forms import ValidatedForm @@ -21,6 +21,9 @@ PE_REGEX = re.compile( re.X, ) +TREASURY_CODE_REGEX = re.compile(r"^0*([1-9]{4}|[1-9]{6})$") + +BA_CODE_REGEX = re.compile(r"^0*[1-9]{2}\w?$") def suggest_pe_id(pe_id): suggestion = pe_id @@ -55,6 +58,18 @@ def validate_pe_id(field, existing_request): class FinancialForm(ValidatedForm): + def validate(self, *args, **kwargs): + if self.funding_type.data == "OTHER": + self.funding_type_other.validators.append(Required()) + return super().validate(*args, **kwargs) + + def reset(self): + """ + Reset UII info so that it can be de-parsed rendered properly. + This is a stupid workaround, and there's probably a better way. + """ + self.uii_ids.process_data(self.uii_ids.data) + def perform_extra_validation(self, existing_request): valid = True if not existing_request or existing_request.get("pe_id") != self.pe_id.data: @@ -62,25 +77,28 @@ class FinancialForm(ValidatedForm): return valid task_order_id = StringField( - "Task Order Number associated with this request.", validators=[Required()] + "Task Order Number associated with this request", + description="Include the original Task Order number (including the 000X at the end). Do not include any modification numbers. Note that there may be a lag between approving a task order and when it becomes available in our system.", + validators=[Required()] ) uii_ids = NewlineListField( - "Unique Item Identifier (UII)s related to your application(s) if you already have them." + "Unique Item Identifier (UII)s related to your application(s) if you already have them", + validators=[Required()] ) pe_id = StringField("Program Element (PE) Number related to your request", validators=[Required()]) - treasury_code = StringField("Program Treasury Code") + treasury_code = StringField("Program Treasury Code", validators=[Required(), Regexp(TREASURY_CODE_REGEX)]) - ba_code = StringField("Program BA Code") + ba_code = StringField("Program Budget Activity (BA) Code", validators=[Required(), Regexp(BA_CODE_REGEX)]) 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()]) + office_co = StringField("Contracting Officer Office", validators=[Required()]) fname_cor = StringField( "Contracting Officer Representative (COR) First Name", validators=[Required()] @@ -100,7 +118,7 @@ class FinancialForm(ValidatedForm): ) funding_type = SelectField( - validators=[Required()], + description="What is the source of funding?", choices=[ ("", "- Select -"), ("RDTE", "Research, Development, Testing & Evaluation (RDT&E)"), @@ -108,38 +126,44 @@ class FinancialForm(ValidatedForm): ("PROC", "Procurement (PROC)"), ("OTHER", "Other"), ], + validators=[Required()], + render_kw={"required": False} ) - funding_type_other = StringField( - "If other, please specify", validators=[Required()] - ) + funding_type_other = StringField("If other, please specify") clin_0001 = StringField( "
CLIN 0001
-
Unclassified IaaS and PaaS Amount
", validators=[Required()], + description="Review your task order document, the amounts for each CLIN must match exactly here" ) clin_0003 = StringField( "
CLIN 0003
-
Unclassified Cloud Support Package
", validators=[Required()], + description="Review your task order document, the amounts for each CLIN must match exactly here" ) clin_1001 = StringField( "
CLIN 1001
-
Unclassified IaaS and PaaS Amount
OPTION PERIOD 1
", validators=[Required()], + description="Review your task order document, the amounts for each CLIN must match exactly here" ) clin_1003 = StringField( "
CLIN 1003
-
Unclassified Cloud Support Package
OPTION PERIOD 1
", validators=[Required()], + description="Review your task order document, the amounts for each CLIN must match exactly here" ) clin_2001 = StringField( "
CLIN 2001
-
Unclassified IaaS and PaaS Amount
OPTION PERIOD 2
", validators=[Required()], + description="Review your task order document, the amounts for each CLIN must match exactly here" ) clin_2003 = StringField( "
CLIN 2003
-
Unclassified Cloud Support Package
OPTION PERIOD 2
", validators=[Required()], + description="Review your task order document, the amounts for each CLIN must match exactly here" ) diff --git a/atst/forms/org.py b/atst/forms/org.py index a812ce4e..ff531d9a 100644 --- a/atst/forms/org.py +++ b/atst/forms/org.py @@ -1,10 +1,12 @@ from wtforms.fields.html5 import EmailField, TelField -from wtforms.fields import RadioField, StringField, SelectField +from wtforms.fields import RadioField, StringField from wtforms.validators import Required, Email import pendulum -from .fields import DateField + +from .fields import DateField, SelectField from .forms import ValidatedForm from .validators import DateRange, PhoneNumber, Alphabet +from .data import SERVICE_BRANCHES class OrgForm(ValidatedForm): @@ -21,54 +23,7 @@ class OrgForm(ValidatedForm): service_branch = SelectField( "Service Branch or Agency", description="Which services and organizations do you belong to within the DoD?", - choices=[ - ("null", "Select an option"), - ("Air Force, Department of the", "Air Force, Department of the"), - ("Army and Air Force Exchange Service", "Army and Air Force Exchange Service"), - ("Army, Department of the", "Army, Department of the"), - ("Defense Advanced Research Projects Agency", "Defense Advanced Research Projects Agency"), - ("Defense Commissary Agency", "Defense Commissary Agency"), - ("Defense Contract Audit Agency", "Defense Contract Audit Agency"), - ("Defense Contract Management Agency", "Defense Contract Management Agency"), - ("Defense Finance & Accounting Service", "Defense Finance & Accounting Service"), - ("Defense Health Agency", "Defense Health Agency"), - ("Defense Information System Agency", "Defense Information System Agency"), - ("Defense Intelligence Agency", "Defense Intelligence Agency"), - ("Defense Legal Services Agency", "Defense Legal Services Agency"), - ("Defense Logistics Agency", "Defense Logistics Agency"), - ("Defense Media Activity", "Defense Media Activity"), - ("Defense Micro Electronics Activity", "Defense Micro Electronics Activity"), - ("Defense POW-MIA Accounting Agency", "Defense POW-MIA Accounting Agency"), - ("Defense Security Cooperation Agency", "Defense Security Cooperation Agency"), - ("Defense Security Service", "Defense Security Service"), - ("Defense Technical Information Center", "Defense Technical Information Center"), - ("Defense Technology Security Administration", "Defense Technology Security Administration"), - ("Defense Threat Reduction Agency", "Defense Threat Reduction Agency"), - ("DoD Education Activity", "DoD Education Activity"), - ("DoD Human Recourses Activity", "DoD Human Recourses Activity"), - ("DoD Inspector General", "DoD Inspector General"), - ("DoD Test Resource Management Center", "DoD Test Resource Management Center"), - ("Headquarters Defense Human Resource Activity ", "Headquarters Defense Human Resource Activity "), - ("Joint Staff", "Joint Staff"), - ("Missile Defense Agency", "Missile Defense Agency"), - ("National Defense University", "National Defense University"), - ("National Geospatial Intelligence Agency (NGA)", "National Geospatial Intelligence Agency (NGA)"), - ("National Oceanic and Atmospheric Administration (NOAA)", "National Oceanic and Atmospheric Administration (NOAA)"), - ("National Reconnaissance Office", "National Reconnaissance Office"), - ("National Reconnaissance Office (NRO)", "National Reconnaissance Office (NRO)"), - ("National Security Agency (NSA)", "National Security Agency (NSA)"), - ("National Security Agency-Central Security Service", "National Security Agency-Central Security Service"), - ("Navy, Department of the", "Navy, Department of the"), - ("Office of Economic Adjustment", "Office of Economic Adjustment"), - ("Office of the Secretary of Defense", "Office of the Secretary of Defense"), - ("Pentagon Force Protection Agency", "Pentagon Force Protection Agency"), - ("Uniform Services University of the Health Sciences", "Uniform Services University of the Health Sciences"), - ("US Cyber Command (USCYBERCOM)", "US Cyber Command (USCYBERCOM)"), - ("US Special Operations Command (USSOCOM)", "US Special Operations Command (USSOCOM)"), - ("US Strategic Command (USSTRATCOM)", "US Strategic Command (USSTRATCOM)"), - ("US Transportation Command (USTRANSCOM)", "US Transportation Command (USTRANSCOM)"), - ("Washington Headquarters Services", "Washington Headquarters Services"), - ], + choices=SERVICE_BRANCHES, ) citizenship = RadioField( @@ -93,7 +48,7 @@ class OrgForm(ValidatedForm): ) date_latest_training = DateField( - "Latest Information Assurance (IA) Training completion date", + "Latest Information Assurance (IA) Training Completion Date", description="To complete the training, you can find it in Information Assurance Cyber Awareness Challange website.", validators=[ Required(), diff --git a/atst/forms/poc.py b/atst/forms/poc.py index 827bf1ae..fc0d8459 100644 --- a/atst/forms/poc.py +++ b/atst/forms/poc.py @@ -20,7 +20,7 @@ class POCForm(ValidatedForm): am_poc = BooleanField( - "I am the Workspace Owner.", + "I am the Workspace Owner", default=False, false_values=(False, "false", "False", "no", "") ) diff --git a/atst/forms/request.py b/atst/forms/request.py index 31b1967d..a938142f 100644 --- a/atst/forms/request.py +++ b/atst/forms/request.py @@ -1,9 +1,10 @@ from wtforms.fields.html5 import IntegerField -from wtforms.fields import RadioField, TextAreaField, SelectField +from wtforms.fields import RadioField, TextAreaField from wtforms.validators import Optional, Required -from .fields import DateField +from .fields import DateField, SelectField from .forms import ValidatedForm +from .data import SERVICE_BRANCHES from atst.domain.requests import Requests @@ -37,65 +38,20 @@ class RequestForm(ValidatedForm): dod_component = SelectField( "DoD Component", description="Identify the DoD component that is requesting access to the JEDI Cloud", - choices=[ - ("null", "Select an option"), - ("Air Force, Department of the", "Air Force, Department of the"), - ("Army and Air Force Exchange Service", "Army and Air Force Exchange Service"), - ("Army, Department of the", "Army, Department of the"), - ("Defense Advanced Research Projects Agency", "Defense Advanced Research Projects Agency"), - ("Defense Commissary Agency", "Defense Commissary Agency"), - ("Defense Contract Audit Agency", "Defense Contract Audit Agency"), - ("Defense Contract Management Agency", "Defense Contract Management Agency"), - ("Defense Finance & Accounting Service", "Defense Finance & Accounting Service"), - ("Defense Health Agency", "Defense Health Agency"), - ("Defense Information System Agency", "Defense Information System Agency"), - ("Defense Intelligence Agency", "Defense Intelligence Agency"), - ("Defense Legal Services Agency", "Defense Legal Services Agency"), - ("Defense Logistics Agency", "Defense Logistics Agency"), - ("Defense Media Activity", "Defense Media Activity"), - ("Defense Micro Electronics Activity", "Defense Micro Electronics Activity"), - ("Defense POW-MIA Accounting Agency", "Defense POW-MIA Accounting Agency"), - ("Defense Security Cooperation Agency", "Defense Security Cooperation Agency"), - ("Defense Security Service", "Defense Security Service"), - ("Defense Technical Information Center", "Defense Technical Information Center"), - ("Defense Technology Security Administration", "Defense Technology Security Administration"), - ("Defense Threat Reduction Agency", "Defense Threat Reduction Agency"), - ("DoD Education Activity", "DoD Education Activity"), - ("DoD Human Recourses Activity", "DoD Human Recourses Activity"), - ("DoD Inspector General", "DoD Inspector General"), - ("DoD Test Resource Management Center", "DoD Test Resource Management Center"), - ("Headquarters Defense Human Resource Activity ", "Headquarters Defense Human Resource Activity "), - ("Joint Staff", "Joint Staff"), - ("Missile Defense Agency", "Missile Defense Agency"), - ("National Defense University", "National Defense University"), - ("National Geospatial Intelligence Agency (NGA)", "National Geospatial Intelligence Agency (NGA)"), - ("National Oceanic and Atmospheric Administration (NOAA)", "National Oceanic and Atmospheric Administration (NOAA)"), - ("National Reconnaissance Office", "National Reconnaissance Office"), - ("National Reconnaissance Office (NRO)", "National Reconnaissance Office (NRO)"), - ("National Security Agency (NSA)", "National Security Agency (NSA)"), - ("National Security Agency-Central Security Service", "National Security Agency-Central Security Service"), - ("Navy, Department of the", "Navy, Department of the"), - ("Office of Economic Adjustment", "Office of Economic Adjustment"), - ("Office of the Secretary of Defense", "Office of the Secretary of Defense"), - ("Pentagon Force Protection Agency", "Pentagon Force Protection Agency"), - ("Uniform Services University of the Health Sciences", "Uniform Services University of the Health Sciences"), - ("US Cyber Command (USCYBERCOM)", "US Cyber Command (USCYBERCOM)"), - ("US Special Operations Command (USSOCOM)", "US Special Operations Command (USSOCOM)"), - ("US Strategic Command (USSTRATCOM)", "US Strategic Command (USSTRATCOM)"), - ("US Transportation Command (USTRANSCOM)", "US Transportation Command (USTRANSCOM)"), - ("Washington Headquarters Services", "Washington Headquarters Services"), - ], + choices=SERVICE_BRANCHES, + validators=[Required()] ) jedi_usage = TextAreaField( "JEDI Usage", description="Your answer will help us provide tangible examples to DoD leadership how and why commercial cloud resources are accelerating the Department's missions", + validators=[Required()] ) # Details of Use: Cloud Readiness num_software_systems = IntegerField( - "Number of Software System", + "Number of Software Systems", description="Estimate the number of software systems that will be supported by this JEDI Cloud access request", ) @@ -108,7 +64,7 @@ class RequestForm(ValidatedForm): rationalization_software_systems = RadioField( description="Have you completed a “rationalization” of your software systems to move to the cloud?", - choices=[("yes", "Yes"), ("no", "No"), ("in_progress", "In Progress")], + choices=[("yes", "Yes"), ("no", "No"), ("In Progress", "In Progress")], default="", ) @@ -121,45 +77,45 @@ class RequestForm(ValidatedForm): organization_providing_assistance = RadioField( # this needs to be updated to use checkboxes instead of radio description="If you are receiving migration assistance, what is the type of organization providing assistance?", choices=[ - ("in_house_staff", "In-house staff"), - ("contractor", "Contractor"), - ("other_dod_organization", "Other DoD organization"), - ("none", "None"), + ("In-house staff", "In-house staff"), + ("Contractor", "Contractor"), + ("Other DoD Organization", "Other DoD Organization"), + ("None", "None"), ], default="", ) engineering_assessment = RadioField( description="Have you completed an engineering assessment of your systems for cloud readiness?", - choices=[("yes", "Yes"), ("no", "No"), ("in_progress", "In Progress")], + choices=[("yes", "Yes"), ("no", "No"), ("In Progress", "In Progress")], default="", ) data_transfers = SelectField( description="How much data is being transferred to the cloud?", choices=[ - ("null", "Select an option"), - ("less_than_100gb", "Less than 100GB"), - ("100gb-500gb", "100GB-500GB"), - ("500gb-1tb", "500GB-1TB"), - ("1tb-50tb", "1TB-50TB"), - ("50tb-100tb", "50TB-100TB"), - ("100tb-500tb", "100TB-500TB"), - ("500tb-1pb", "500TB-1PB"), - ("1pb-5pb", "1PB-5PB"), - ("5pb-10pb", "5PB-10PB"), - ("above_10pb", "Above 10PB"), + ("", "Select an option"), + ("Less than 100GB", "Less than 100GB"), + ("100GB-500GB", "100GB-500GB"), + ("500GB-1TB", "500GB-1TB"), + ("1TB-50TB", "1TB-50TB"), + ("50TB-100TB", "50TB-100TB"), + ("100TB-500TB", "100TB-500TB"), + ("500TB-1PB", "500TB-1PB"), + ("1PB-5PB", "1PB-5PB"), + ("5PB-10PB", "5PB-10PB"), + ("Above 10PB", "Above 10PB"), ], ) expected_completion_date = SelectField( description="When do you expect to complete your migration to the JEDI Cloud?", choices=[ - ("null", "Select an option"), - ("less_than_1_month", "Less than 1 month"), - ("1_to_3_months", "1-3 months"), - ("3_to_6_months", "3-6 months"), - ("above_12_months", "Above 12 months"), + ("", "Select an option"), + ("Less than 1 month", "Less than 1 month"), + ("1-3 months", "1-3 months"), + ("3-6 months", "3-6 months"), + ("Above 12 months", "Above 12 months"), ], ) @@ -171,7 +127,7 @@ class RequestForm(ValidatedForm): # Details of Use: Financial Usage estimated_monthly_spend = IntegerField( - "Estimated monthly spend", + "Estimated Monthly Spend", description='Use the JEDI CSP Calculator to estimate your monthly cloud resource usage and enter the dollar amount below. Note these estimates are for initial approval only. After the request is approved, you will be asked to provide a valid Task Order number with specific CLIN amounts for cloud services.', ) @@ -195,5 +151,7 @@ class RequestForm(ValidatedForm): ) start_date = DateField( - description="When do you expect to start using the JEDI Cloud (not for billing purposes)?" + description="When do you expect to start using the JEDI Cloud (not for billing purposes)?", + validators=[ + Required()] ) diff --git a/atst/routes/requests/financial_verification.py b/atst/routes/requests/financial_verification.py index f6b8cfda..966a9680 100644 --- a/atst/routes/requests/financial_verification.py +++ b/atst/routes/requests/financial_verification.py @@ -24,7 +24,7 @@ def update_financial_verification(request_id): rerender_args = dict(request_id=request_id, f=form) if form.validate(): - request_data = {"financial_verification": post_data} + request_data = {"financial_verification": form.data} valid = form.perform_extra_validation( existing_request.body.get("financial_verification") ) @@ -32,10 +32,12 @@ def update_financial_verification(request_id): if valid: return redirect(url_for("requests.financial_verification_submitted")) else: + form.reset() return render_template( "requests/financial_verification.html", **rerender_args ) else: + form.reset() return render_template("requests/financial_verification.html", **rerender_args) diff --git a/js/components/forms/financial.js b/js/components/forms/financial.js new file mode 100644 index 00000000..2a1fb43b --- /dev/null +++ b/js/components/forms/financial.js @@ -0,0 +1,41 @@ +import optionsinput from '../options_input' +import textinput from '../text_input' + +export default { + name: 'financial', + + components: { + optionsinput, + textinput, + }, + + props: { + initialData: { + type: Object, + default: () => ({}) + } + }, + + data: function () { + const { + funding_type = "" + } = this.initialData + + return { + funding_type + } + }, + + mounted: function () { + this.$root.$on('field-change', this.handleFieldChange) + }, + + methods: { + handleFieldChange: function (event) { + const { value, name } = event + if (typeof this[name] !== undefined) { + this[name] = value + } + }, + } +} diff --git a/js/index.js b/js/index.js index afe6961d..4ecd6aa7 100644 --- a/js/index.js +++ b/js/index.js @@ -7,6 +7,7 @@ import textinput from './components/text_input' import checkboxinput from './components/checkbox_input' import DetailsOfUse from './components/forms/details_of_use' import poc from './components/forms/poc' +import financial from './components/forms/financial' Vue.use(VTooltip) @@ -19,6 +20,7 @@ const app = new Vue({ checkboxinput, DetailsOfUse, poc, + financial, }, methods: { closeModal: function(name) { diff --git a/styles/components/_forms.scss b/styles/components/_forms.scss index 83ab5b83..6bfd17d2 100644 --- a/styles/components/_forms.scss +++ b/styles/components/_forms.scss @@ -47,6 +47,7 @@ .form__sub-fields { @include alert; @include alert-level('default'); + display: block; .usa-input { &:first-child { @@ -73,5 +74,15 @@ } } } + + &--warning { + @include alert-level('warning'); + display: block; + } + + &--error { + @include alert-level('error'); + display: block; + } } diff --git a/styles/elements/_inputs.scss b/styles/elements/_inputs.scss index 35640fbe..14b508ab 100644 --- a/styles/elements/_inputs.scss +++ b/styles/elements/_inputs.scss @@ -110,6 +110,7 @@ margin: 0; box-sizing: border-box; max-width: 32em; + resize: none; &:hover, &:focus { @@ -133,6 +134,10 @@ } + select { + max-width: 30em; + } + ul { list-style: none; margin: 0; @@ -181,16 +186,19 @@ &--anything, &--email { input { - max-width: 26em; + max-width: 30em; } .icon-validation { - left: 26em; + left: 30em; } } &--paragraph { + textarea { + max-width: 30em; + } .icon-validation { - left: 32em; + left: 30em; } } diff --git a/templates/requests/financial_verification.html b/templates/requests/financial_verification.html index e8236ece..71057795 100644 --- a/templates/requests/financial_verification.html +++ b/templates/requests/financial_verification.html @@ -6,6 +6,7 @@ {% block content %} +
@@ -34,36 +35,99 @@

In order to get you access to the JEDI Cloud, we will need you to enter the details below that will help us verify and account for your Task Order.

- {{ TextInput(f.task_order_id,placeholder="e.g.: 1234567899C0001",tooltip="Note that there may be a lag between the time you have created and approved the task order to the time it is searchable within the electronic.
A Contracting Officer will likely be the best source for this number.") }} - {{ TextInput(f.uii_ids,paragraph=True,placeholder="e.g.: DI 0CVA5786950 \nUN1945326361234786950",tooltip="A Unique Item Identifer is a unique code that helps the Department of Defense track and report on where and how digital assets are stored.
Not all applications have an existing UII number assigned.") }} - {{ TextInput(f.pe_id,placeholder="e.g.: 0203752A",tooltip="Program Element numbers helps the Department of Defense identify which offices\\' budgets are contributing towards this resource use.") }} - {{ TextInput(f.treasury_code,placeholder="e.g.: 1200") }} - {{ TextInput(f.ba_code,placeholder="e.g.: 02") }} + {{ TextInput( + f.task_order_id, + placeholder="e.g.: 1234567899C0001", + tooltip="A Contracting Officer will likely be the best source for this number.", + validation="anything" + ) }} + + {{ TextInput(f.uii_ids, + paragraph=True, + placeholder="e.g.: DI 0CVA5786950 \nUN1945326361234786950", + tooltip="A Unique Item Identifer is a unique code that helps the Department of Defense track and report on where and how digital assets are stored.
Not all applications have an existing UII number assigned." + ) }} + + {{ TextInput(f.pe_id, + placeholder="e.g.: 0302400A", + tooltip="Program Element numbers helps the Department of Defense identify which offices\\' budgets are contributing towards this resource use." + ) }} + + {{ TextInput(f.treasury_code,placeholder="e.g.: 00123456") }} + + {{ TextInput(f.ba_code,placeholder="e.g.: 02A") }}

Contracting Officer (KO) Information

{{ TextInput(f.fname_co,placeholder="Contracting Officer First Name") }} + {{ TextInput(f.lname_co,placeholder="Contracting Officer Last Name") }} - {{ TextInput(f.email_co,validation='email',placeholder="jane@mail.mil") }} - {{ TextInput(f.office_co,placeholder="e.g.: WHS") }} + + {{ TextInput( + f.email_co,validation='email', + placeholder="jane@mail.mil" + ) }} + + {{ TextInput( + f.office_co, + placeholder="e.g.: WHS" + ) }} +

Contracting Officer Representative (COR) Information

{{ TextInput(f.fname_cor,placeholder="Contracting Officer Representative First Name") }} + {{ TextInput(f.lname_cor,placeholder="Contracting Officer Representative Last Name") }} + {{ TextInput(f.email_cor,validation='email',placeholder="jane@mail.mil") }} + {{ TextInput(f.office_cor,placeholder="e.g.: WHS") }} -

- ↓ FIELDS NEEDED FOR MANUAL ENTRY OF TASK ORDER INFORMATION (only necessary if EDA info not available) +
+ + {{ Alert('Task Order not found in EDA', + message="Since the Task Order (TO) number was not found in our system of record, EDA, please populate the additional fields in the form below.", + level='warning' + ) }} + +
+ {{ OptionsInput(f.funding_type) }} + + + + {{ TextInput( + f.clin_0001,placeholder="50,000", + validation='integer' + ) }} + + {{ TextInput( + f.clin_0003,placeholder="13,000", + validation='integer' + ) }} + + {{ TextInput( + f.clin_1001,placeholder="30,000", + validation='integer' + ) }} + + {{ TextInput( + f.clin_1003,placeholder="7,000", + validation='integer' + ) }} + + {{ TextInput( + f.clin_2001,placeholder="30,000", + validation='integer' + ) }} + + {{ TextInput( + f.clin_2003,placeholder="7,000", + validation='integer' + ) }} +
- {{ OptionsInput(f.funding_type) }} - {{ TextInput(f.funding_type_other) }} - {{ TextInput(f.clin_0001,placeholder="50,000", validation='integer', tooltip="Review your task order document, the amounts for each CLIN must match exactly here.") }} - {{ TextInput(f.clin_0003,placeholder="13,000", validation='integer', tooltip="Review your task order document, the amounts for each CLIN must match exactly here.") }} - {{ TextInput(f.clin_1001,placeholder="30,000", validation='integer', tooltip="Review your task order document, the amounts for each CLIN must match exactly here.") }} - {{ TextInput(f.clin_1003,placeholder="7,000", validation='integer', tooltip="Review your task order document, the amounts for each CLIN must match exactly here.") }} - {{ TextInput(f.clin_2001,placeholder="30,000", validation='integer', tooltip="Review your task order document, the amounts for each CLIN must match exactly here.") }} - {{ TextInput(f.clin_2003,placeholder="7,000", validation='integer', tooltip="Review your task order document, the amounts for each CLIN must match exactly here.") }} {% endautoescape %} {% endblock form %} @@ -71,9 +135,9 @@ {% endblock %} -
+
-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/requests/screen-1.html b/templates/requests/screen-1.html index c6ea9a05..cc3c9c25 100644 --- a/templates/requests/screen-1.html +++ b/templates/requests/screen-1.html @@ -25,7 +25,7 @@

General

{{ OptionsInput(f.dod_component) }} - {{ TextInput(f.jedi_usage, paragraph=True, placeholder="Briefly describe how you are expecting to use the JEDI Cloud. \n e.g. We are migrating XYZ application to the cloud so that...") }} + {{ TextInput(f.jedi_usage, paragraph=True, placeholder="Briefly describe how you are expecting to use the JEDI Cloud. e.g. We are migrating XYZ application to the cloud so that...") }}

Cloud Readiness

{{ TextInput(f.num_software_systems,validation="integer",tooltip="A software system can be any code that you plan to host on cloud infrastructure. For example, it could be a custom-developed web application, or a large ERP system.",placeholder="0") }} @@ -75,8 +75,8 @@ - {{ TextInput(f.dollar_value, validation='dollars', placeholder="$0") }} - {{ TextInput(f.start_date, validation='date', placeholder='MM / DD / YYYY') }} + {{ TextInput(f.dollar_value, validation='dollars', placeholder='$0') }} + {{ TextInput(f.start_date, placeholder='MM / DD / YYYY', validation='date') }} diff --git a/tests/factories.py b/tests/factories.py index 07073424..a6370060 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -3,6 +3,7 @@ import string import factory from uuid import uuid4 +from atst.forms.data import SERVICE_BRANCHES from atst.models.request import Request from atst.models.request_status_event import RequestStatusEvent, RequestStatus from atst.models.pe_number import PENumber @@ -13,7 +14,6 @@ from atst.models.request_status_event import RequestStatusEvent from atst.domain.roles import Roles - class RoleFactory(factory.alchemy.SQLAlchemyModelFactory): class Meta: model = Role @@ -34,7 +34,6 @@ class UserFactory(factory.alchemy.SQLAlchemyModelFactory): class RequestStatusEventFactory(factory.alchemy.SQLAlchemyModelFactory): - class Meta: model = RequestStatusEvent @@ -61,15 +60,16 @@ class RequestFactory(factory.alchemy.SQLAlchemyModelFactory): "dodid_poc": user.dod_id, "email_poc": user.email, "fname_poc": user.first_name, - "lname_poc": user.last_name + "lname_poc": user.last_name, }, "details_of_use": { "jedi_usage": "adf", "start_date": "2018-08-08", "cloud_native": "yes", "dollar_value": dollar_value, - "dod_component": "Army and Air Force Exchange Service", - "data_transfers": "less_than_100gb", + "dod_component": SERVICE_BRANCHES[2][1], + "data_transfers": "Less than 100GB", + "expected_completion_date": "Less than 1 month", "jedi_migration": "yes", "num_software_systems": 1, "number_user_sessions": 2, @@ -78,9 +78,8 @@ class RequestFactory(factory.alchemy.SQLAlchemyModelFactory): "technical_support_team": "yes", "estimated_monthly_spend": 100, "average_daily_traffic_gb": 4, - "expected_completion_date": "less_than_1_month", "rationalization_software_systems": "yes", - "organization_providing_assistance": "in_house_staff" + "organization_providing_assistance": "In-house staff", }, "information_about_you": { "citizenship": "United States", @@ -89,9 +88,9 @@ class RequestFactory(factory.alchemy.SQLAlchemyModelFactory): "email_request": user.email, "fname_request": user.first_name, "lname_request": user.last_name, - "service_branch": "Air Force, Department of the", - "date_latest_training": "2018-08-06" - } + "service_branch": SERVICE_BRANCHES[1][1], + "date_latest_training": "2018-08-06", + }, } diff --git a/tests/forms/test_fields.py b/tests/forms/test_fields.py index ceece7c5..04dd88c1 100644 --- a/tests/forms/test_fields.py +++ b/tests/forms/test_fields.py @@ -1,25 +1,58 @@ import pytest from wtforms import Form import pendulum +from werkzeug.datastructures import ImmutableMultiDict -from atst.forms.fields import DateField +from atst.forms.fields import DateField, NewlineListField -class MyForm(Form): +class DateForm(Form): date = DateField() +class NewlineListForm(Form): + newline_list = NewlineListField() + + def test_date_ie_format(): - form = MyForm(data={"date": "12/24/2018"}) + form = DateForm(data={"date": "12/24/2018"}) assert form.date._value() == pendulum.date(2018, 12, 24) def test_date_sane_format(): - form = MyForm(data={"date": "2018-12-24"}) + form = DateForm(data={"date": "2018-12-24"}) assert form.date._value() == pendulum.date(2018, 12, 24) def test_date_insane_format(): - form = MyForm(data={"date": "hello"}) + form = DateForm(data={"date": "hello"}) with pytest.raises(ValueError): form.date._value() + + +@pytest.mark.parametrize("input_,expected", [ + ("", []), + ("hello", ["hello"]), + ("hello\n", ["hello"]), + ("hello\nworld", ["hello", "world"]), + ("hello\nworld\n", ["hello", "world"]) +]) +def test_newline_list_process(input_, expected): + form_data = ImmutableMultiDict({"newline_list": input_}) + form = NewlineListForm(form_data) + + assert form.validate() + assert form.data == {"newline_list": expected} + + +@pytest.mark.parametrize("input_,expected", [ + ([], ""), + (["hello"], "hello"), + (["hello", "world"], "hello\nworld") +]) +def test_newline_list_value(input_, expected): + form_data = {"newline_list": input_} + form = NewlineListForm(data=form_data) + + assert form.validate() + assert form.newline_list._value() == expected diff --git a/tests/forms/test_financial.py b/tests/forms/test_financial.py index d58aa61c..9bc82421 100644 --- a/tests/forms/test_financial.py +++ b/tests/forms/test_financial.py @@ -1,14 +1,69 @@ import pytest -from atst.forms.financial import suggest_pe_id +from atst.forms.financial import suggest_pe_id, FinancialForm -@pytest.mark.parametrize("input,expected", [ +@pytest.mark.parametrize("input_,expected", [ ('0603502N', None), ('0603502NZ', None), ('603502N', '0603502N'), ('063502N', '0603502N'), ('63502N', '0603502N'), ]) -def test_suggest_pe_id(input, expected): - assert suggest_pe_id(input) == expected +def test_suggest_pe_id(input_, expected): + assert suggest_pe_id(input_) == expected + + +def test_funding_type_other_not_required_if_funding_type_is_not_other(): + form_data = { + "funding_type": "PROC" + } + form = FinancialForm(data=form_data) + form.validate() + assert "funding_type_other" not in form.errors + + +def test_funding_type_other_required_if_funding_type_is_other(): + form_data = { + "funding_type": "OTHER" + } + form = FinancialForm(data=form_data) + form.validate() + assert "funding_type_other" in form.errors + + +@pytest.mark.parametrize("input_,expected", [ + ("1234", True), + ("123456", True), + ("0001234", True), + ("000123456", True), + ("12345", False), + ("00012345", False), + ("0001234567", False), + ("000000", False), +]) +def test_treasury_code_validation(input_, expected): + form_data = {"treasury_code": input_} + form = FinancialForm(data=form_data) + form.validate() + is_valid = "treasury_code" not in form.errors + + assert is_valid == expected + + +@pytest.mark.parametrize("input_,expected", [ + ("12", True), + ("00012", True), + ("12A", True), + ("000123", True), + ("00012A", True), + ("0001", False), + ("00012AB", False), +]) +def test_ba_code_validation(input_, expected): + form_data = {"ba_code": input_} + form = FinancialForm(data=form_data) + form.validate() + is_valid = "ba_code" not in form.errors + + assert is_valid == expected diff --git a/tests/forms/test_request.py b/tests/forms/test_request.py index 9d5e4201..5f55a561 100644 --- a/tests/forms/test_request.py +++ b/tests/forms/test_request.py @@ -6,80 +6,80 @@ from atst.forms.request import RequestForm class TestRequestForm: form_data = { - 'dod_component': 'Army and Air Force Exchange Service', - 'jedi_usage': 'cloud-ify all the things', - 'num_software_systems': '12', - 'estimated_monthly_spend': '1000000', - 'dollar_value': '42', - 'number_user_sessions': '6', - 'average_daily_traffic': '0', - 'start_date': '12/12/2012', + "dod_component": "Army and Air Force Exchange Service", + "jedi_usage": "cloud-ify all the things", + "num_software_systems": "12", + "estimated_monthly_spend": "1000000", + "dollar_value": "42", + "number_user_sessions": "6", + "average_daily_traffic": "0", + "start_date": "12/12/2012", } migration_data = { - 'jedi_migration': 'yes', - 'rationalization_software_systems': 'yes', - 'technical_support_team': 'yes', - 'organization_providing_assistance': 'in_house_staff', - 'engineering_assessment': 'yes', - 'data_transfers': 'less_than_100gb', - 'expected_completion_date': 'less_than_1_month' + "jedi_migration": "yes", + "rationalization_software_systems": "yes", + "technical_support_team": "yes", + "organization_providing_assistance": "In-house staff", + "engineering_assessment": "yes", + "data_transfers": "Less than 100GB", + "expected_completion_date": "Less than 1 month", } def test_require_cloud_native_when_not_migrating(self): - extra_data = { 'jedi_migration': 'no' } - request_form = RequestForm(data={ **self.form_data, **extra_data }) + extra_data = {"jedi_migration": "no"} + request_form = RequestForm(data={**self.form_data, **extra_data}) assert not request_form.validate() - assert request_form.errors == { 'cloud_native': ['Not a valid choice'] } + assert request_form.errors == {"cloud_native": ["Not a valid choice"]} def test_require_migration_questions_when_migrating(self): - extra_data = { 'jedi_migration': 'yes' } - request_form = RequestForm(data={ **self.form_data, **extra_data }) + extra_data = {"jedi_migration": "yes"} + request_form = RequestForm(data={**self.form_data, **extra_data}) assert not request_form.validate() assert request_form.errors == { - 'rationalization_software_systems': ['Not a valid choice'], - 'technical_support_team': ['Not a valid choice'], - 'organization_providing_assistance': ['Not a valid choice'], - 'engineering_assessment': ['Not a valid choice'], - 'data_transfers': ['Not a valid choice'], - 'expected_completion_date': ['Not a valid choice'] + "rationalization_software_systems": ["Not a valid choice"], + "technical_support_team": ["Not a valid choice"], + "organization_providing_assistance": ["Not a valid choice"], + "engineering_assessment": ["Not a valid choice"], + "data_transfers": ["Not a valid choice"], + "expected_completion_date": ["Not a valid choice"], } def test_require_organization_when_technical_support_team(self): - data = { **self.form_data, **self.migration_data } - del data['organization_providing_assistance'] + data = {**self.form_data, **self.migration_data} + del data["organization_providing_assistance"] request_form = RequestForm(data=data) assert not request_form.validate() assert request_form.errors == { - 'organization_providing_assistance': ['Not a valid choice'], + "organization_providing_assistance": ["Not a valid choice"] } def test_valid_form_data(self): - data = { **self.form_data, **self.migration_data } - data['technical_support_team'] = 'no' - del data['organization_providing_assistance'] + data = {**self.form_data, **self.migration_data} + data["technical_support_team"] = "no" + del data["organization_providing_assistance"] request_form = RequestForm(data=data) assert request_form.validate() def test_sessions_required_for_large_projects(self): - data = { **self.form_data, **self.migration_data } - data['estimated_monthly_spend'] = '9999999' - del data['number_user_sessions'] - del data['average_daily_traffic'] + data = {**self.form_data, **self.migration_data} + data["estimated_monthly_spend"] = "9999999" + del data["number_user_sessions"] + del data["average_daily_traffic"] request_form = RequestForm(data=data) assert not request_form.validate() assert request_form.errors == { - 'number_user_sessions': ['This field is required.'], - 'average_daily_traffic': ['This field is required.'], + "number_user_sessions": ["This field is required."], + "average_daily_traffic": ["This field is required."], } def test_sessions_not_required_invalid_monthly_spend(self): - data = { **self.form_data, **self.migration_data } - data['estimated_monthly_spend'] = 'foo' - del data['number_user_sessions'] - del data['average_daily_traffic'] + data = {**self.form_data, **self.migration_data} + data["estimated_monthly_spend"] = "foo" + del data["number_user_sessions"] + del data["average_daily_traffic"] request_form = RequestForm(data=data) assert request_form.validate()