Merge branch 'master' into continuous-deployment
This commit is contained in:
commit
23e5c04597
@ -63,7 +63,7 @@ def make_flask_callbacks(app):
|
||||
def _set_globals():
|
||||
g.navigationContext = (
|
||||
"workspace"
|
||||
if re.match("\/workspaces\/[A-Za-z0-9]*", request.url)
|
||||
if re.match("\/workspaces\/[A-Za-z0-9]*", request.path)
|
||||
else "global"
|
||||
)
|
||||
g.dev = os.getenv("FLASK_ENV", "dev") == "dev"
|
||||
|
@ -1,10 +1,13 @@
|
||||
import requests
|
||||
import re
|
||||
import os
|
||||
import pendulum
|
||||
from html.parser import HTMLParser
|
||||
|
||||
_DISA_CRLS = "https://iasecontent.disa.mil/pki-pke/data/crls/dod_crldps.htm"
|
||||
|
||||
MODIFIED_TIME_BUFFER = 15 * 60
|
||||
|
||||
|
||||
def fetch_disa():
|
||||
response = requests.get(_DISA_CRLS)
|
||||
@ -29,29 +32,67 @@ def crl_list_from_disa_html(html):
|
||||
return parser.crl_list
|
||||
|
||||
|
||||
def write_crl(out_dir, crl_location):
|
||||
def crl_local_path(out_dir, crl_location):
|
||||
name = re.split("/", crl_location)[-1]
|
||||
crl = os.path.join(out_dir, name)
|
||||
with requests.get(crl_location, stream=True) as r:
|
||||
return crl
|
||||
|
||||
|
||||
def existing_crl_modification_time(crl):
|
||||
if os.path.exists(crl):
|
||||
prev_time = os.path.getmtime(crl)
|
||||
buffered = prev_time + MODIFIED_TIME_BUFFER
|
||||
mod_time = prev_time if pendulum.now().timestamp() < buffered else buffered
|
||||
dt = pendulum.from_timestamp(mod_time, tz="GMT")
|
||||
return dt.format("ddd, DD MMM YYYY HH:mm:ss zz")
|
||||
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def write_crl(out_dir, target_dir, crl_location):
|
||||
crl = crl_local_path(out_dir, crl_location)
|
||||
existing = crl_local_path(target_dir, crl_location)
|
||||
options = {"stream": True}
|
||||
mod_time = existing_crl_modification_time(existing)
|
||||
if mod_time:
|
||||
options["headers"] = {"If-Modified-Since": mod_time}
|
||||
|
||||
with requests.get(crl_location, **options) as response:
|
||||
if response.status_code == 304:
|
||||
return False
|
||||
|
||||
with open(crl, "wb") as crl_file:
|
||||
for chunk in r.iter_content(chunk_size=1024):
|
||||
for chunk in response.iter_content(chunk_size=1024):
|
||||
if chunk:
|
||||
crl_file.write(chunk)
|
||||
|
||||
return True
|
||||
|
||||
def refresh_crls(out_dir, logger=None):
|
||||
|
||||
def remove_bad_crl(out_dir, crl_location):
|
||||
crl = crl_local_path(out_dir, crl_location)
|
||||
os.remove(crl)
|
||||
|
||||
|
||||
def refresh_crls(out_dir, target_dir, logger):
|
||||
disa_html = fetch_disa()
|
||||
crl_list = crl_list_from_disa_html(disa_html)
|
||||
for crl_location in crl_list:
|
||||
if logger:
|
||||
logger.info("updating CRL from {}".format(crl_location))
|
||||
logger.info("updating CRL from {}".format(crl_location))
|
||||
try:
|
||||
write_crl(out_dir, crl_location)
|
||||
if write_crl(out_dir, target_dir, crl_location):
|
||||
logger.info("successfully synced CRL from {}".format(crl_location))
|
||||
else:
|
||||
logger.info("no updates for CRL from {}".format(crl_location))
|
||||
except requests.exceptions.ChunkedEncodingError:
|
||||
if logger:
|
||||
logger.error(
|
||||
"Error downloading {}, continuing anyway".format(crl_location)
|
||||
"Error downloading {}, removing file and continuing anyway".format(
|
||||
crl_location
|
||||
)
|
||||
)
|
||||
remove_bad_crl(out_dir, crl_location)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
@ -64,7 +105,7 @@ if __name__ == "__main__":
|
||||
logger = logging.getLogger()
|
||||
logger.info("Updating CRLs")
|
||||
try:
|
||||
refresh_crls(sys.argv[1], logger=logger)
|
||||
refresh_crls(sys.argv[1], sys.argv[2], logger)
|
||||
except Exception as err:
|
||||
logger.exception("Fatal error encountered, stopping")
|
||||
sys.exit(1)
|
||||
|
12
atst/domain/date.py
Normal file
12
atst/domain/date.py
Normal file
@ -0,0 +1,12 @@
|
||||
import pendulum
|
||||
|
||||
|
||||
def parse_date(data):
|
||||
date_formats = ["YYYY-MM-DD", "MM/DD/YYYY"]
|
||||
for _format in date_formats:
|
||||
try:
|
||||
return pendulum.from_format(data, _format).date()
|
||||
except (ValueError, pendulum.parsing.exceptions.ParserError):
|
||||
pass
|
||||
|
||||
raise ValueError("Unable to parse string {}".format(data))
|
@ -1,4 +1,6 @@
|
||||
from enum import Enum
|
||||
from sqlalchemy import exists, and_, exc
|
||||
from sqlalchemy.sql import text
|
||||
from sqlalchemy.orm.exc import NoResultFound
|
||||
from sqlalchemy.orm.attributes import flag_modified
|
||||
|
||||
@ -150,3 +152,47 @@ class Requests(object):
|
||||
@classmethod
|
||||
def is_pending_financial_verification(cls, request):
|
||||
return request.status == RequestStatus.PENDING_FINANCIAL_VERIFICATION
|
||||
|
||||
@classmethod
|
||||
def is_pending_ccpo_approval(cls, request):
|
||||
return request.status == RequestStatus.PENDING_CCPO_APPROVAL
|
||||
|
||||
@classmethod
|
||||
def status_count(cls, status, creator=None):
|
||||
if isinstance(status, Enum):
|
||||
status = status.name
|
||||
bindings = {"status": status}
|
||||
raw = """
|
||||
SELECT count(requests_with_status.id)
|
||||
FROM (
|
||||
SELECT DISTINCT ON (rse.request_id) r.*, rse.new_status as status
|
||||
FROM request_status_events rse JOIN requests r ON r.id = rse.request_id
|
||||
ORDER BY rse.request_id, rse.sequence DESC
|
||||
) as requests_with_status
|
||||
WHERE requests_with_status.status = :status
|
||||
"""
|
||||
|
||||
if creator:
|
||||
raw += " AND requests_with_status.user_id = :user_id"
|
||||
bindings["user_id"] = creator.id
|
||||
|
||||
results = db.session.execute(text(raw), bindings).fetchone()
|
||||
(count,) = results
|
||||
return count
|
||||
|
||||
@classmethod
|
||||
def in_progress_count(cls):
|
||||
return sum([
|
||||
Requests.status_count(RequestStatus.STARTED),
|
||||
Requests.status_count(RequestStatus.PENDING_FINANCIAL_VERIFICATION),
|
||||
Requests.status_count(RequestStatus.CHANGES_REQUESTED),
|
||||
])
|
||||
|
||||
@classmethod
|
||||
def pending_ccpo_count(cls):
|
||||
return Requests.status_count(RequestStatus.PENDING_CCPO_APPROVAL)
|
||||
|
||||
@classmethod
|
||||
def completed_count(cls):
|
||||
return Requests.status_count(RequestStatus.APPROVED)
|
||||
|
||||
|
@ -1,20 +1,14 @@
|
||||
from wtforms.fields.html5 import DateField
|
||||
from wtforms.fields import Field
|
||||
from wtforms.widgets import TextArea
|
||||
import pendulum
|
||||
|
||||
from atst.domain.date import parse_date
|
||||
|
||||
|
||||
class DateField(DateField):
|
||||
def _value(self):
|
||||
if self.data:
|
||||
date_formats = ["YYYY-MM-DD", "MM/DD/YYYY"]
|
||||
for _format in date_formats:
|
||||
try:
|
||||
return pendulum.from_format(self.data, _format).date()
|
||||
except (ValueError, pendulum.parsing.exceptions.ParserError):
|
||||
pass
|
||||
|
||||
raise ValueError("Unable to parse string {}".format(self.data))
|
||||
return parse_date(self.data)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
import re
|
||||
from wtforms.fields.html5 import EmailField
|
||||
from wtforms.fields import StringField, SelectField
|
||||
from wtforms.form import Form
|
||||
from wtforms.validators import Required, Email
|
||||
|
||||
from atst.domain.exceptions import NotFoundError
|
||||
@ -41,7 +40,7 @@ def suggest_pe_id(pe_id):
|
||||
|
||||
def validate_pe_id(field, existing_request):
|
||||
try:
|
||||
pe_number = PENumbers.get(field.data)
|
||||
PENumbers.get(field.data)
|
||||
except NotFoundError:
|
||||
suggestion = suggest_pe_id(field.data)
|
||||
error_str = (
|
||||
|
@ -6,3 +6,9 @@ class ValidatedForm(FlaskForm):
|
||||
"""Performs any applicable extra validation. Must
|
||||
return True if the form is valid or False otherwise."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
_data = super().data
|
||||
_data.pop("csrf_token", None)
|
||||
return _data
|
||||
|
@ -1,5 +1,5 @@
|
||||
from wtforms.fields.html5 import EmailField, TelField
|
||||
from wtforms.fields import RadioField, StringField
|
||||
from wtforms.fields import RadioField, StringField, SelectField
|
||||
from wtforms.validators import Required, Email
|
||||
import pendulum
|
||||
from .fields import DateField
|
||||
@ -12,13 +12,67 @@ class OrgForm(ValidatedForm):
|
||||
|
||||
lname_request = StringField("Last Name", validators=[Required(), Alphabet()])
|
||||
|
||||
email_request = EmailField("Email Address", validators=[Required(), Email()])
|
||||
email_request = EmailField("E-mail Address", validators=[Required(), Email()])
|
||||
|
||||
phone_number = TelField("Phone Number", validators=[Required(), PhoneNumber()])
|
||||
phone_number = TelField("Phone Number",
|
||||
description='Enter a 10-digit phone number',
|
||||
validators=[Required(), PhoneNumber()])
|
||||
|
||||
service_branch = StringField("Service Branch or Agency", validators=[Required()])
|
||||
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"),
|
||||
],
|
||||
)
|
||||
|
||||
citizenship = RadioField(
|
||||
description="What is your citizenship status?",
|
||||
choices=[
|
||||
("United States", "United States"),
|
||||
("Foreign National", "Foreign National"),
|
||||
@ -29,6 +83,7 @@ class OrgForm(ValidatedForm):
|
||||
|
||||
designation = RadioField(
|
||||
"Designation of Person",
|
||||
description="What is your designation within the DoD?",
|
||||
choices=[
|
||||
("military", "Military"),
|
||||
("civilian", "Civilian"),
|
||||
@ -39,6 +94,7 @@ class OrgForm(ValidatedForm):
|
||||
|
||||
date_latest_training = DateField(
|
||||
"Latest Information Assurance (IA) Training completion date",
|
||||
description="To complete the training, you can find it in <a class=\"icon-link\" href=\"https://iatraining.disa.mil/eta/disa_cac2018/launchPage.htm\" target=\"_blank\">Information Assurance Cyber Awareness Challange</a> website.",
|
||||
validators=[
|
||||
Required(),
|
||||
DateRange(
|
||||
|
@ -1,16 +1,35 @@
|
||||
from wtforms.fields import StringField
|
||||
from wtforms.fields import StringField, BooleanField
|
||||
from wtforms.fields.html5 import EmailField
|
||||
from wtforms.validators import Required, Email, Length
|
||||
from wtforms.validators import Required, Email, Length, Optional
|
||||
from .forms import ValidatedForm
|
||||
from .validators import IsNumber, Alphabet
|
||||
from .validators import IsNumber
|
||||
|
||||
|
||||
class POCForm(ValidatedForm):
|
||||
fname_poc = StringField("POC First Name", validators=[Required()])
|
||||
|
||||
lname_poc = StringField("POC Last Name", validators=[Required()])
|
||||
def validate(self, *args, **kwargs):
|
||||
if self.am_poc.data:
|
||||
# Prepend Optional validators so that the validation chain
|
||||
# halts if no data exists.
|
||||
self.fname_poc.validators.insert(0, Optional())
|
||||
self.lname_poc.validators.insert(0, Optional())
|
||||
self.email_poc.validators.insert(0, Optional())
|
||||
self.dodid_poc.validators.insert(0, Optional())
|
||||
|
||||
email_poc = EmailField("POC Email Address", validators=[Required(), Email()])
|
||||
return super().validate(*args, **kwargs)
|
||||
|
||||
|
||||
am_poc = BooleanField(
|
||||
"I am the Workspace Owner.",
|
||||
default=False,
|
||||
false_values=(False, "false", "False", "no", "")
|
||||
)
|
||||
|
||||
fname_poc = StringField("First Name", validators=[Required()])
|
||||
|
||||
lname_poc = StringField("Last Name", validators=[Required()])
|
||||
|
||||
email_poc = EmailField("Email Address", validators=[Required(), Email()])
|
||||
|
||||
dodid_poc = StringField(
|
||||
"DOD ID", validators=[Required(), Length(min=10), IsNumber()]
|
||||
|
@ -1,36 +1,98 @@
|
||||
from wtforms.fields.html5 import IntegerField
|
||||
from wtforms.fields import RadioField, StringField, TextAreaField, SelectField
|
||||
from wtforms.validators import NumberRange, InputRequired
|
||||
from wtforms.fields import RadioField, TextAreaField, SelectField
|
||||
from wtforms.validators import Optional, Required
|
||||
|
||||
from .fields import DateField
|
||||
from .forms import ValidatedForm
|
||||
from .validators import DateRange
|
||||
import pendulum
|
||||
from atst.domain.requests import Requests
|
||||
|
||||
|
||||
class RequestForm(ValidatedForm):
|
||||
|
||||
def validate(self, *args, **kwargs):
|
||||
if self.jedi_migration.data == 'no':
|
||||
self.rationalization_software_systems.validators.append(Optional())
|
||||
self.technical_support_team.validators.append(Optional())
|
||||
self.organization_providing_assistance.validators.append(Optional())
|
||||
self.engineering_assessment.validators.append(Optional())
|
||||
self.data_transfers.validators.append(Optional())
|
||||
self.expected_completion_date.validators.append(Optional())
|
||||
elif self.jedi_migration.data == 'yes':
|
||||
if self.technical_support_team.data == 'no':
|
||||
self.organization_providing_assistance.validators.append(Optional())
|
||||
self.cloud_native.validators.append(Optional())
|
||||
|
||||
try:
|
||||
annual_spend = int(self.estimated_monthly_spend.data or 0) * 12
|
||||
except ValueError:
|
||||
annual_spend = 0
|
||||
|
||||
if annual_spend > Requests.AUTO_APPROVE_THRESHOLD:
|
||||
self.number_user_sessions.validators.append(Required())
|
||||
self.average_daily_traffic.validators.append(Required())
|
||||
|
||||
return super(RequestForm, self).validate(*args, **kwargs)
|
||||
|
||||
# Details of Use: General
|
||||
dod_component = SelectField(
|
||||
"DoD Component",
|
||||
description="Identify the DoD component that is requesting access to the JEDI Cloud",
|
||||
choices=[
|
||||
("null", "Select an option"),
|
||||
("us_air_force", "US Air Force"),
|
||||
("us_army", "US Army"),
|
||||
("us_navy", "US Navy"),
|
||||
("us_marine_corps", "US Marine Corps"),
|
||||
("joint_chiefs_of_staff", "Joint Chiefs of Staff"),
|
||||
("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"),
|
||||
],
|
||||
)
|
||||
|
||||
jedi_usage = TextAreaField(
|
||||
"JEDI Usage",
|
||||
description="Briefly describe how you are expecting to use the JEDI Cloud",
|
||||
render_kw={
|
||||
"placeholder": "e.g. We are migrating XYZ application to the cloud so that..."
|
||||
},
|
||||
description="Your answer will help us provide tangible examples to DoD leadership how and why commercial cloud resources are accelerating the Department's missions",
|
||||
)
|
||||
|
||||
|
||||
# Details of Use: Cloud Readiness
|
||||
num_software_systems = IntegerField(
|
||||
"Number of Software System",
|
||||
@ -38,32 +100,39 @@ class RequestForm(ValidatedForm):
|
||||
)
|
||||
|
||||
jedi_migration = RadioField(
|
||||
"Are you using the JEDI Cloud to migrate existing systems?",
|
||||
"JEDI Migration",
|
||||
description="Are you using the JEDI Cloud to migrate existing systems?",
|
||||
choices=[("yes", "Yes"), ("no", "No")],
|
||||
default="",
|
||||
)
|
||||
|
||||
rationalization_software_systems = RadioField(
|
||||
"Have you completed a “rationalization” of your software systems to move to the cloud?",
|
||||
description="Have you completed a “rationalization” of your software systems to move to the cloud?",
|
||||
choices=[("yes", "Yes"), ("no", "No"), ("in_progress", "In Progress")],
|
||||
default="",
|
||||
)
|
||||
|
||||
technical_support_team = RadioField(
|
||||
"Are you working with a technical support team experienced in cloud migrations?",
|
||||
description="Are you working with a technical support team experienced in cloud migrations?",
|
||||
choices=[("yes", "Yes"), ("no", "No")],
|
||||
default="",
|
||||
)
|
||||
|
||||
organization_providing_assistance = RadioField( # this needs to be updated to use checkboxes instead of radio
|
||||
"If you are receiving migration assistance, indicate the type of organization providing assistance below:",
|
||||
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"),
|
||||
],
|
||||
default="",
|
||||
)
|
||||
|
||||
engineering_assessment = RadioField(
|
||||
"Have you completed an engineering assessment of your software systems for cloud readiness?",
|
||||
description="Have you completed an engineering assessment of your systems for cloud readiness?",
|
||||
choices=[("yes", "Yes"), ("no", "No"), ("in_progress", "In Progress")],
|
||||
default="",
|
||||
)
|
||||
|
||||
data_transfers = SelectField(
|
||||
@ -95,14 +164,15 @@ class RequestForm(ValidatedForm):
|
||||
)
|
||||
|
||||
cloud_native = RadioField(
|
||||
"Are your software systems being developed cloud native?",
|
||||
description="Are your software systems being developed cloud native?",
|
||||
choices=[("yes", "Yes"), ("no", "No")],
|
||||
default="",
|
||||
)
|
||||
|
||||
# Details of Use: Financial Usage
|
||||
estimated_monthly_spend = IntegerField(
|
||||
"Estimated monthly spend",
|
||||
description='Use the <a href="#">JEDI CSP Calculator</a> 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.',
|
||||
description='Use the <a href="#" target="_blank" class="icon-link">JEDI CSP Calculator</a> to estimate your <b>monthly</b> 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.',
|
||||
)
|
||||
|
||||
dollar_value = IntegerField(
|
||||
@ -115,6 +185,12 @@ class RequestForm(ValidatedForm):
|
||||
)
|
||||
|
||||
average_daily_traffic = IntegerField(
|
||||
"Average Daily Traffic (Number of Requests)",
|
||||
description="What is the average daily traffic you expect the systems under this cloud contract to use?"
|
||||
)
|
||||
|
||||
average_daily_traffic_gb = IntegerField(
|
||||
"Average Daily Traffic (GB)",
|
||||
description="What is the average daily traffic you expect the systems under this cloud contract to use?"
|
||||
)
|
||||
|
||||
|
@ -2,18 +2,19 @@ import re
|
||||
from wtforms.validators import ValidationError
|
||||
import pendulum
|
||||
|
||||
from atst.domain.date import parse_date
|
||||
|
||||
|
||||
def DateRange(lower_bound=None, upper_bound=None, message=None):
|
||||
def _date_range(form, field):
|
||||
now = pendulum.now().date()
|
||||
date = parse_date(field.data)
|
||||
|
||||
if lower_bound is not None:
|
||||
date = pendulum.parse(field.data).date()
|
||||
if (now - lower_bound) > date:
|
||||
raise ValidationError(message)
|
||||
|
||||
if upper_bound is not None:
|
||||
date = pendulum.parse(field.data).date()
|
||||
if (now + upper_bound) < date:
|
||||
raise ValidationError(message)
|
||||
|
||||
|
@ -12,9 +12,10 @@ class RequestStatus(Enum):
|
||||
STARTED = "Started"
|
||||
PENDING_FINANCIAL_VERIFICATION = "Pending Financial Verification"
|
||||
PENDING_CCPO_APPROVAL = "Pending CCPO Approval"
|
||||
CHANGES_REQUESTED = "Changes Requested"
|
||||
APPROVED = "Approved"
|
||||
EXPIRED = "Expired"
|
||||
DELETED = "Deleted"
|
||||
CANCELED = "Canceled"
|
||||
|
||||
|
||||
class RequestStatusEvent(Base):
|
||||
|
@ -3,14 +3,18 @@ from flask import render_template, g, url_for
|
||||
|
||||
from . import requests_bp
|
||||
from atst.domain.requests import Requests
|
||||
from atst.models.permissions import Permissions
|
||||
|
||||
|
||||
def map_request(request):
|
||||
time_created = pendulum.instance(request.time_created)
|
||||
is_new = time_created.add(days=1) > pendulum.now()
|
||||
app_count = request.body.get("details_of_use", {}).get("num_software_systems", 0)
|
||||
update_url = url_for('requests.requests_form_update', screen=1, request_id=request.id)
|
||||
verify_url = url_for('requests.financial_verification', request_id=request.id)
|
||||
annual_usage = request.body.get("details_of_use", {}).get("dollar_value", 0)
|
||||
update_url = url_for(
|
||||
"requests.requests_form_update", screen=1, request_id=request.id
|
||||
)
|
||||
verify_url = url_for("requests.financial_verification", request_id=request.id)
|
||||
|
||||
return {
|
||||
"order_id": request.id,
|
||||
@ -19,20 +23,49 @@ def map_request(request):
|
||||
"app_count": app_count,
|
||||
"date": time_created.format("M/DD/YYYY"),
|
||||
"full_name": request.creator.full_name,
|
||||
"edit_link": verify_url if Requests.is_pending_financial_verification(request) else update_url
|
||||
"annual_usage": annual_usage,
|
||||
"edit_link": verify_url if Requests.is_pending_financial_verification(
|
||||
request
|
||||
) else update_url,
|
||||
}
|
||||
|
||||
|
||||
@requests_bp.route("/requests", methods=["GET"])
|
||||
def requests_index():
|
||||
requests = []
|
||||
if "review_and_approve_jedi_workspace_request" in g.current_user.atat_permissions:
|
||||
requests = Requests.get_many()
|
||||
else:
|
||||
requests = Requests.get_many(creator=g.current_user)
|
||||
if Permissions.REVIEW_AND_APPROVE_JEDI_WORKSPACE_REQUEST in g.current_user.atat_permissions:
|
||||
return _ccpo_view()
|
||||
|
||||
else:
|
||||
return _non_ccpo_view()
|
||||
|
||||
|
||||
def _ccpo_view():
|
||||
requests = Requests.get_many()
|
||||
mapped_requests = [map_request(r) for r in requests]
|
||||
|
||||
return render_template(
|
||||
"requests.html",
|
||||
requests=mapped_requests,
|
||||
pending_financial_verification=False,
|
||||
pending_ccpo_approval=False,
|
||||
extended_view=True,
|
||||
kpi_inprogress=Requests.in_progress_count(),
|
||||
kpi_pending=Requests.pending_ccpo_count(),
|
||||
kpi_completed=Requests.completed_count(),
|
||||
)
|
||||
|
||||
|
||||
def _non_ccpo_view():
|
||||
requests = Requests.get_many(creator=g.current_user)
|
||||
mapped_requests = [map_request(r) for r in requests]
|
||||
|
||||
pending_fv = any(Requests.is_pending_financial_verification(r) for r in requests)
|
||||
pending_ccpo = any(Requests.is_pending_ccpo_approval(r) for r in requests)
|
||||
|
||||
return render_template("requests.html", requests=mapped_requests, pending_financial_verification=pending_fv)
|
||||
return render_template(
|
||||
"requests.html",
|
||||
requests=mapped_requests,
|
||||
pending_financial_verification=pending_fv,
|
||||
pending_ccpo_approval=pending_ccpo,
|
||||
extended_view=False,
|
||||
)
|
||||
|
@ -65,7 +65,7 @@ class JEDIRequestFlow(object):
|
||||
return {
|
||||
"fname_request": user.first_name,
|
||||
"lname_request": user.last_name,
|
||||
"email_request": user.email
|
||||
"email_request": user.email,
|
||||
}
|
||||
|
||||
@property
|
||||
@ -78,15 +78,15 @@ class JEDIRequestFlow(object):
|
||||
if self.request:
|
||||
if self.form_section == "review_submit":
|
||||
data = self.request.body
|
||||
if self.form_section == "information_about_you":
|
||||
elif self.form_section == "information_about_you":
|
||||
form_data = self.request.body.get(self.form_section, {})
|
||||
data = { **self.map_user_data(self.request.creator), **form_data }
|
||||
data = {**self.map_user_data(self.request.creator), **form_data}
|
||||
else:
|
||||
data = self.request.body.get(self.form_section, {})
|
||||
elif self.form_section == "information_about_you":
|
||||
data = self.map_user_data(self.current_user)
|
||||
|
||||
return defaultdict(lambda: defaultdict(lambda: "Input required"), data)
|
||||
return defaultdict(lambda: defaultdict(lambda: None), data)
|
||||
|
||||
@property
|
||||
def can_submit(self):
|
||||
@ -103,40 +103,36 @@ class JEDIRequestFlow(object):
|
||||
"title": "Details of Use",
|
||||
"section": "details_of_use",
|
||||
"form": RequestForm,
|
||||
"subitems": [
|
||||
{
|
||||
"title": "Overall request details",
|
||||
"id": "overall-request-details",
|
||||
},
|
||||
{"title": "Cloud Resources", "id": "cloud-resources"},
|
||||
{"title": "Support Staff", "id": "support-staff"},
|
||||
],
|
||||
"show": True,
|
||||
},
|
||||
{
|
||||
"title": "Information About You",
|
||||
"section": "information_about_you",
|
||||
"form": OrgForm,
|
||||
"show": True,
|
||||
},
|
||||
{
|
||||
"title": "Primary Point of Contact",
|
||||
"section": "primary_poc",
|
||||
"form": POCForm,
|
||||
"show": True,
|
||||
},
|
||||
{"title": "Workspace Owner", "section": "primary_poc", "form": POCForm},
|
||||
{
|
||||
"title": "Review & Submit",
|
||||
"section": "review_submit",
|
||||
"form": ReviewForm,
|
||||
"show": True,
|
||||
},
|
||||
]
|
||||
|
||||
def create_or_update_request(self):
|
||||
request_data = {self.form_section: self.form.data}
|
||||
request_data = self.map_request_data(self.form_section, self.form.data)
|
||||
if self.request_id:
|
||||
Requests.update(self.request_id, request_data)
|
||||
else:
|
||||
request = Requests.create(self.current_user, request_data)
|
||||
self.request_id = request.id
|
||||
|
||||
def map_request_data(self, section, data):
|
||||
if section == "primary_poc":
|
||||
if data.get("am_poc", False):
|
||||
data = {
|
||||
**data,
|
||||
"dodid_poc": self.current_user.dod_id,
|
||||
"fname_poc": self.current_user.first_name,
|
||||
"lname_poc": self.current_user.last_name,
|
||||
"email_poc": self.current_user.email,
|
||||
}
|
||||
return {section: data}
|
||||
|
@ -42,6 +42,7 @@ def requests_form_update(screen=1, request_id=None):
|
||||
current=screen,
|
||||
next_screen=screen + 1,
|
||||
request_id=request_id,
|
||||
jedi_request=jedi_flow.request,
|
||||
can_submit=jedi_flow.can_submit,
|
||||
)
|
||||
|
||||
@ -63,35 +64,30 @@ def requests_update(screen=1, request_id=None):
|
||||
existing_request=existing_request,
|
||||
)
|
||||
|
||||
rerender_args = dict(
|
||||
f=jedi_flow.form,
|
||||
data=post_data,
|
||||
screens=jedi_flow.screens,
|
||||
current=screen,
|
||||
next_screen=jedi_flow.next_screen,
|
||||
request_id=jedi_flow.request_id,
|
||||
)
|
||||
has_next_screen = jedi_flow.next_screen <= len(jedi_flow.screens)
|
||||
valid = jedi_flow.validate() and jedi_flow.validate_warnings()
|
||||
|
||||
if jedi_flow.validate():
|
||||
if valid:
|
||||
jedi_flow.create_or_update_request()
|
||||
valid = jedi_flow.validate_warnings()
|
||||
if valid:
|
||||
if jedi_flow.next_screen > len(jedi_flow.screens):
|
||||
where = "/requests"
|
||||
else:
|
||||
where = url_for(
|
||||
"requests.requests_form_update",
|
||||
screen=jedi_flow.next_screen,
|
||||
request_id=jedi_flow.request_id,
|
||||
)
|
||||
return redirect(where)
|
||||
|
||||
else:
|
||||
return render_template(
|
||||
"requests/screen-%d.html" % int(screen), **rerender_args
|
||||
if has_next_screen:
|
||||
where = url_for(
|
||||
"requests.requests_form_update",
|
||||
screen=jedi_flow.next_screen,
|
||||
request_id=jedi_flow.request_id,
|
||||
)
|
||||
|
||||
else:
|
||||
where = "/requests"
|
||||
return redirect(where)
|
||||
else:
|
||||
rerender_args = dict(
|
||||
f=jedi_flow.form,
|
||||
data=post_data,
|
||||
screens=jedi_flow.screens,
|
||||
current=screen,
|
||||
next_screen=jedi_flow.next_screen,
|
||||
request_id=jedi_flow.request_id,
|
||||
)
|
||||
return render_template("requests/screen-%d.html" % int(screen), **rerender_args)
|
||||
|
||||
|
||||
@ -104,7 +100,7 @@ def requests_submit(request_id=None):
|
||||
return redirect("/requests?modal=pendingFinancialVerification")
|
||||
|
||||
else:
|
||||
return redirect("/requests")
|
||||
return redirect("/requests?modal=pendingCCPOApproval")
|
||||
|
||||
|
||||
# TODO: generalize this, along with other authorizations, into a policy-pattern
|
||||
|
16
js/components/checkbox_input.js
Normal file
16
js/components/checkbox_input.js
Normal file
@ -0,0 +1,16 @@
|
||||
export default {
|
||||
name: 'checkboxinput',
|
||||
|
||||
props: {
|
||||
name: String,
|
||||
},
|
||||
|
||||
methods: {
|
||||
onInput: function (e) {
|
||||
this.$root.$emit('field-change', {
|
||||
value: e.target.checked,
|
||||
name: this.name
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
71
js/components/forms/details_of_use.js
Normal file
71
js/components/forms/details_of_use.js
Normal file
@ -0,0 +1,71 @@
|
||||
import createNumberMask from 'text-mask-addons/dist/createNumberMask'
|
||||
import { conformToMask } from 'vue-text-mask'
|
||||
|
||||
import textinput from '../text_input'
|
||||
import optionsinput from '../options_input'
|
||||
|
||||
export default {
|
||||
name: 'details-of-use',
|
||||
|
||||
components: {
|
||||
textinput,
|
||||
optionsinput,
|
||||
},
|
||||
|
||||
props: {
|
||||
initialData: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
|
||||
data: function () {
|
||||
const {
|
||||
estimated_monthly_spend = 0,
|
||||
jedi_migration = '',
|
||||
technical_support_team = ''
|
||||
} = this.initialData
|
||||
|
||||
return {
|
||||
estimated_monthly_spend,
|
||||
jedi_migration,
|
||||
technical_support_team
|
||||
}
|
||||
},
|
||||
|
||||
mounted: function () {
|
||||
this.$root.$on('field-change', this.handleFieldChange)
|
||||
},
|
||||
|
||||
computed: {
|
||||
annualSpend: function () {
|
||||
const monthlySpend = this.estimated_monthly_spend || 0
|
||||
return monthlySpend * 12
|
||||
},
|
||||
annualSpendStr: function () {
|
||||
return this.formatDollars(this.annualSpend)
|
||||
},
|
||||
jediMigrationOptionSelected: function () {
|
||||
return this.jedi_migration !== ''
|
||||
},
|
||||
isJediMigration: function () {
|
||||
return this.jedi_migration === 'yes'
|
||||
},
|
||||
hasTechnicalSupportTeam: function () {
|
||||
return this.technical_support_team === 'yes'
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
formatDollars: function (intValue) {
|
||||
const mask = createNumberMask({ prefix: '$', allowDecimal: true })
|
||||
return conformToMask(intValue.toString(), mask).conformedValue
|
||||
},
|
||||
handleFieldChange: function (event) {
|
||||
const { value, name } = event
|
||||
if (typeof this[name] !== undefined) {
|
||||
this[name] = value
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
43
js/components/forms/poc.js
Normal file
43
js/components/forms/poc.js
Normal file
@ -0,0 +1,43 @@
|
||||
import optionsinput from '../options_input'
|
||||
import textinput from '../text_input'
|
||||
import checkboxinput from '../checkbox_input'
|
||||
|
||||
export default {
|
||||
name: 'poc',
|
||||
|
||||
components: {
|
||||
optionsinput,
|
||||
textinput,
|
||||
checkboxinput,
|
||||
},
|
||||
|
||||
props: {
|
||||
initialData: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
|
||||
data: function () {
|
||||
const {
|
||||
am_poc = false
|
||||
} = this.initialData
|
||||
|
||||
return {
|
||||
am_poc
|
||||
}
|
||||
},
|
||||
|
||||
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
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
16
js/components/options_input.js
Normal file
16
js/components/options_input.js
Normal file
@ -0,0 +1,16 @@
|
||||
export default {
|
||||
name: 'optionsinput',
|
||||
|
||||
props: {
|
||||
name: String
|
||||
},
|
||||
|
||||
methods: {
|
||||
onInput: function (e) {
|
||||
this.$root.$emit('field-change', {
|
||||
value: e.target.value,
|
||||
name: this.name
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
@ -14,32 +14,55 @@ export default {
|
||||
type: String,
|
||||
default: () => 'anything'
|
||||
},
|
||||
value: {
|
||||
initialValue: {
|
||||
type: String,
|
||||
default: () => ''
|
||||
}
|
||||
},
|
||||
initialErrors: Array,
|
||||
paragraph: String
|
||||
},
|
||||
|
||||
data: function () {
|
||||
return {
|
||||
showError: false,
|
||||
showError: (this.initialErrors && this.initialErrors.length) || false,
|
||||
showValid: false,
|
||||
mask: inputValidations[this.validation].mask,
|
||||
renderedValue: this.value
|
||||
pipe: inputValidations[this.validation].pipe || undefined,
|
||||
keepCharPositions: inputValidations[this.validation].keepCharPositions || false,
|
||||
validationError: inputValidations[this.validation].validationError || '',
|
||||
value: this.initialValue,
|
||||
modified: false
|
||||
}
|
||||
},
|
||||
|
||||
computed:{
|
||||
rawValue: function () {
|
||||
return this._rawValue(this.value)
|
||||
}
|
||||
},
|
||||
|
||||
mounted: function () {
|
||||
const value = this.$refs.input.value
|
||||
if (value) {
|
||||
this._checkIfValid({ value, invalidate: true })
|
||||
this.renderedValue = conformToMask(value, this.mask).conformedValue
|
||||
if (this.value) {
|
||||
this._checkIfValid({ value: this.value, invalidate: true })
|
||||
|
||||
if (this.mask && this.validation !== 'email') {
|
||||
const mask = typeof this.mask.mask !== 'function'
|
||||
? this.mask
|
||||
: mask.mask(this.value).filter((val) => val !== '[]')
|
||||
|
||||
this.value = conformToMask(this.value, mask).conformedValue
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
// When user types a character
|
||||
onInput: function (value) {
|
||||
onInput: function (e) {
|
||||
// When we use the native textarea element, we receive an event object
|
||||
// When we use the masked-input component, we receive the value directly
|
||||
const value = typeof e === 'object' ? e.target.value : e
|
||||
this.value = value
|
||||
this.modified = true
|
||||
this._checkIfValid({ value })
|
||||
},
|
||||
|
||||
@ -52,7 +75,11 @@ export default {
|
||||
//
|
||||
_checkIfValid: function ({ value, invalidate = false}) {
|
||||
// Validate the value
|
||||
const valid = this._validate(value)
|
||||
let valid = this._validate(value)
|
||||
|
||||
if (!this.modified && this.initialErrors && this.initialErrors.length) {
|
||||
valid = false
|
||||
}
|
||||
|
||||
// Show error messages or not
|
||||
if (valid) {
|
||||
@ -63,20 +90,21 @@ export default {
|
||||
this.showValid = valid
|
||||
|
||||
// Emit a change event
|
||||
this.$emit('fieldChange', {
|
||||
value,
|
||||
this.$root.$emit('field-change', {
|
||||
value: this._rawValue(value),
|
||||
valid,
|
||||
name: this.name
|
||||
})
|
||||
},
|
||||
|
||||
_validate: function (value) {
|
||||
// Strip out all the mask characters
|
||||
let rawValue = inputValidations[this.validation].unmask.reduce((currentValue, character) => {
|
||||
_rawValue: function (value) {
|
||||
return inputValidations[this.validation].unmask.reduce((currentValue, character) => {
|
||||
return currentValue.split(character).join('')
|
||||
}, value)
|
||||
},
|
||||
|
||||
return inputValidations[this.validation].match.test(rawValue)
|
||||
_validate: function (value) {
|
||||
return inputValidations[this.validation].match.test(this._rawValue(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
18
js/index.js
18
js/index.js
@ -1,12 +1,24 @@
|
||||
import classes from '../styles/atat.scss'
|
||||
import Vue from 'vue/dist/vue'
|
||||
import VTooltip from 'v-tooltip'
|
||||
|
||||
import optionsinput from './components/options_input'
|
||||
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'
|
||||
|
||||
Vue.use(VTooltip)
|
||||
|
||||
|
||||
const app = new Vue({
|
||||
el: '#app-root',
|
||||
components: {
|
||||
textinput
|
||||
optionsinput,
|
||||
textinput,
|
||||
checkboxinput,
|
||||
DetailsOfUse,
|
||||
poc,
|
||||
},
|
||||
methods: {
|
||||
closeModal: function(name) {
|
||||
@ -21,6 +33,7 @@ const app = new Vue({
|
||||
modals: {
|
||||
styleguideModal: false,
|
||||
pendingFinancialVerification: false,
|
||||
pendingCCPOApproval: false,
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -30,5 +43,6 @@ const app = new Vue({
|
||||
const modal = modalOpen.getAttribute("data-modal");
|
||||
this.modals[modal] = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
delimiters: ['!{', '}']
|
||||
})
|
||||
|
@ -1,20 +1,56 @@
|
||||
import createNumberMask from 'text-mask-addons/dist/createNumberMask'
|
||||
import emailMask from 'text-mask-addons/dist/emailMask'
|
||||
import createAutoCorrectedDatePipe from 'text-mask-addons/dist/createAutoCorrectedDatePipe'
|
||||
|
||||
export default {
|
||||
anything: {
|
||||
mask: false,
|
||||
match: /^(?!\s*$).+/,
|
||||
unmask: [],
|
||||
validationError: 'Please enter a response'
|
||||
},
|
||||
integer: {
|
||||
mask: createNumberMask({ prefix: '', allowDecimal: false }),
|
||||
match: /^[1-9]\d*$/,
|
||||
unmask: [','],
|
||||
validationError: 'Please enter a number'
|
||||
},
|
||||
dollars: {
|
||||
mask: createNumberMask({ prefix: '$', allowDecimal: true }),
|
||||
match: /^-?\d+\.?\d*$/,
|
||||
unmask: ['$',',']
|
||||
unmask: ['$',','],
|
||||
validationError: 'Please enter a dollar amount'
|
||||
},
|
||||
gigabytes: {
|
||||
mask: createNumberMask({ prefix: '', suffix:' GB', allowDecimal: false }),
|
||||
match: /^[1-9]\d*$/,
|
||||
unmask: [',',' GB'],
|
||||
validationError: 'Please enter an amount of data in gigabytes'
|
||||
},
|
||||
email: {
|
||||
mask: emailMask,
|
||||
match: /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/,
|
||||
unmask: [],
|
||||
validationError: 'Please enter a valid e-mail address'
|
||||
},
|
||||
date: {
|
||||
mask: [/\d/,/\d/,'/',/\d/,/\d/,'/',/\d/,/\d/,/\d/,/\d/],
|
||||
match: /(0[1-9]|1[012])[- \/.](0[1-9]|[12][0-9]|3[01])[- \/.](19|20)\d\d/,
|
||||
unmask: [],
|
||||
pipe: createAutoCorrectedDatePipe('mm/dd/yyyy HH:MM'),
|
||||
keepCharPositions: true,
|
||||
validationError: 'Please enter a valid date in the form MM/DD/YYYY'
|
||||
},
|
||||
usPhone: {
|
||||
mask: ['(', /[1-9]/, /\d/, /\d/, ')', ' ', /\d/, /\d/, /\d/, '-', /\d/, /\d/, /\d/, /\d/],
|
||||
match: /^\d{10}$/,
|
||||
unmask: ['(',')','-',' '],
|
||||
validationError: 'Please enter a 10-digit phone number'
|
||||
},
|
||||
dodId: {
|
||||
mask: createNumberMask({ prefix: '', allowDecimal: false, includeThousandsSeparator: false }),
|
||||
match: /^\d{10}$/,
|
||||
unmask: [],
|
||||
validationError: 'Please enter a 10-digit DoD ID number'
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,7 @@
|
||||
"parcel": "^1.9.7",
|
||||
"text-mask-addons": "^3.8.0",
|
||||
"uswds": "^1.6.3",
|
||||
"v-tooltip": "^2.0.0-rc.33",
|
||||
"vue": "^2.5.17",
|
||||
"vue-text-mask": "^6.1.2"
|
||||
},
|
||||
|
@ -5,9 +5,9 @@ set -e
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
mkdir -p crl-tmp
|
||||
pipenv run python ./atst/domain/authnid/crl/util.py crl-tmp
|
||||
pipenv run python ./atst/domain/authnid/crl/util.py crl-tmp crl
|
||||
mkdir -p crl
|
||||
rsync -rq crl-tmp/. crl/.
|
||||
rsync -rq --min-size 400 crl-tmp/. crl/.
|
||||
rm -rf crl-tmp
|
||||
|
||||
if [[ $FLASK_ENV != "prod" ]]; then
|
||||
|
@ -10,7 +10,7 @@ export FLASK_ENV=test
|
||||
RESET_DB="true"
|
||||
|
||||
# Define all relevant python files and directories for this app
|
||||
PYTHON_FILES="./app.py ./atst ./config"
|
||||
PYTHON_FILES="./app.py ./atst/** ./config"
|
||||
|
||||
# Enable Python testing
|
||||
RUN_PYTHON_TESTS="true"
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
@import 'core/grid';
|
||||
@import 'core/util';
|
||||
@import 'core/transitions';
|
||||
|
||||
@import 'elements/typography';
|
||||
@import 'elements/icons';
|
||||
@ -16,6 +17,8 @@
|
||||
@import 'elements/action_group';
|
||||
@import 'elements/labels';
|
||||
@import 'elements/diff';
|
||||
@import 'elements/tooltip';
|
||||
@import 'elements/kpi';
|
||||
|
||||
@import 'components/topbar';
|
||||
@import 'components/global_layout';
|
||||
@ -28,6 +31,7 @@
|
||||
@import 'components/footer';
|
||||
@import 'components/progress_menu.scss';
|
||||
@import 'components/search_bar';
|
||||
@import 'components/forms';
|
||||
|
||||
@import 'sections/login';
|
||||
@import 'sections/request_approval';
|
||||
|
77
styles/components/_forms.scss
Normal file
77
styles/components/_forms.scss
Normal file
@ -0,0 +1,77 @@
|
||||
// Form Grid
|
||||
.form-row {
|
||||
margin: ($gap * 4) 0;
|
||||
|
||||
.form-col {
|
||||
flex-grow: 1;
|
||||
|
||||
&:first-child .usa-input {
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child .usa-input {
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include media($medium-screen) {
|
||||
@include grid-row;
|
||||
align-items: flex-start;
|
||||
|
||||
.form-col {
|
||||
.usa-input {
|
||||
margin-left: ($gap * 4);
|
||||
margin-right: ($gap * 4);
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
.usa-input {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
.usa-input {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.form__sub-fields {
|
||||
@include alert;
|
||||
@include alert-level('default');
|
||||
|
||||
.usa-input {
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
> h1, > h2, > h3, > h4, > h5, > h6, > legend {
|
||||
@include h3;
|
||||
|
||||
margin: ($gap * 4) 0;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
&.usa-sr-only {
|
||||
+ .usa-input {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -29,6 +29,7 @@
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
max-width: 80rem;
|
||||
position: relative;
|
||||
|
||||
@include media($medium-screen) {
|
||||
padding: $gap * 4;
|
||||
@ -39,9 +40,24 @@
|
||||
@include h3;
|
||||
}
|
||||
|
||||
:first-child {
|
||||
> :first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.modal__dismiss {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 8rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.modal--dismissable {
|
||||
.modal__body {
|
||||
> :first-child {
|
||||
margin-right: 8rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,15 +6,20 @@
|
||||
|
||||
.workspace-navigation {
|
||||
@include panel-margin;
|
||||
margin-bottom: $gap * 4;
|
||||
|
||||
ul {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: column;
|
||||
li {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@include media($medium-screen) {
|
||||
margin-bottom: $gap * 5;
|
||||
}
|
||||
|
||||
@include media($large-screen) {
|
||||
width: 20rem;
|
||||
margin-right: $gap * 2;
|
||||
|
36
styles/core/_transitions.scss
Normal file
36
styles/core/_transitions.scss
Normal file
@ -0,0 +1,36 @@
|
||||
// Slide up/down transition
|
||||
.slide-enter-active {
|
||||
transform-origin: 0 0;
|
||||
transition: transform 0.5s ease-out;
|
||||
|
||||
> * {
|
||||
transition: opacity 0.5s ease-in;
|
||||
}
|
||||
}
|
||||
|
||||
.slide-leave-active {
|
||||
transform-origin: 0 0;
|
||||
transition: transform 0.5s ease-in;
|
||||
|
||||
> * {
|
||||
transition: opacity 0.5s ease-out;
|
||||
}
|
||||
}
|
||||
|
||||
.slide-enter,
|
||||
.slide-leave-to {
|
||||
transform: scaleY(0);
|
||||
|
||||
> * {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.slide-enter-to,
|
||||
.slide-leave {
|
||||
transform: scaleY(1);
|
||||
|
||||
> * {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
@ -27,7 +27,7 @@
|
||||
$state-color: $color-green;
|
||||
}
|
||||
|
||||
.icon {
|
||||
.icon-validation {
|
||||
@include icon-color($state-color);
|
||||
}
|
||||
|
||||
@ -70,22 +70,37 @@
|
||||
@include h4;
|
||||
@include line-max;
|
||||
position: relative;
|
||||
clear: both;
|
||||
|
||||
.icon {
|
||||
.icon-validation {
|
||||
position: absolute;
|
||||
left: 100%;
|
||||
top: 100%;
|
||||
margin-top: 1.4rem;
|
||||
margin-left: $gap;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.usa-input__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.icon-tooltip {
|
||||
padding: 0 $gap/2;
|
||||
cursor: default;
|
||||
margin-left: $gap/2;
|
||||
}
|
||||
}
|
||||
|
||||
.usa-input__help {
|
||||
display: block;
|
||||
@include h4;
|
||||
font-weight: normal;
|
||||
padding: $gap/2 0;
|
||||
@include line-max;
|
||||
|
||||
.icon-link {
|
||||
padding: 0 $gap/2;
|
||||
}
|
||||
}
|
||||
|
||||
input,
|
||||
@ -93,6 +108,18 @@
|
||||
select {
|
||||
@include line-max;
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
max-width: 32em;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
border-color: $color-blue !important;
|
||||
color: $color-blue-darker;
|
||||
box-shadow: inset 0 0 0 1px $color-blue;
|
||||
&::placeholder {
|
||||
color: $color-blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.usa-input__choices { // checkbox & radio sets
|
||||
@ -104,10 +131,6 @@
|
||||
font-weight: $font-bold;
|
||||
}
|
||||
|
||||
.icon {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ul {
|
||||
@ -121,6 +144,12 @@
|
||||
[type='radio'] + label,
|
||||
[type='checkbox'] + label {
|
||||
margin: 0;
|
||||
&:hover {
|
||||
color: $color-blue;
|
||||
&:before {
|
||||
box-shadow: 0 0 0 1px $color-white, 0 0 0 3px $color-blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -145,7 +174,55 @@
|
||||
.usa-input__message {
|
||||
@include h5;
|
||||
display: inline-block;
|
||||
padding-top: $gap;
|
||||
}
|
||||
|
||||
&--validation {
|
||||
|
||||
&--anything,
|
||||
&--email {
|
||||
input {
|
||||
max-width: 26em;
|
||||
}
|
||||
.icon-validation {
|
||||
left: 26em;
|
||||
}
|
||||
}
|
||||
|
||||
&--paragraph {
|
||||
.icon-validation {
|
||||
left: 32em;
|
||||
}
|
||||
}
|
||||
|
||||
&--integer,
|
||||
&--dollars,
|
||||
&--gigabytes, {
|
||||
input {
|
||||
max-width: 16em;
|
||||
}
|
||||
.icon-validation {
|
||||
left: 16em;
|
||||
}
|
||||
}
|
||||
|
||||
&--date,
|
||||
&--usPhone {
|
||||
input {
|
||||
max-width: 10em;
|
||||
}
|
||||
.icon-validation {
|
||||
left: 10em;
|
||||
}
|
||||
}
|
||||
|
||||
&--dodId {
|
||||
input {
|
||||
max-width: 18em;
|
||||
}
|
||||
.icon-validation {
|
||||
left: 18em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.usa-input--error {
|
||||
@ -198,49 +275,3 @@ select {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// Form Grid
|
||||
.form-row {
|
||||
margin: ($gap * 4) 0;
|
||||
|
||||
.form-col {
|
||||
flex-grow: 1;
|
||||
|
||||
&:first-child .usa-input {
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child .usa-input {
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include media($medium-screen) {
|
||||
@include grid-row;
|
||||
align-items: flex-start;
|
||||
|
||||
.form-col {
|
||||
.usa-input {
|
||||
margin-left: ($gap * 4);
|
||||
margin-right: ($gap * 4);
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
.usa-input {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
.usa-input {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
25
styles/elements/_kpi.scss
Normal file
25
styles/elements/_kpi.scss
Normal file
@ -0,0 +1,25 @@
|
||||
.kpi {
|
||||
|
||||
margin-bottom: $gap;
|
||||
|
||||
.kpi__item {
|
||||
@include panel-base;
|
||||
text-align: center;
|
||||
margin: $gap;
|
||||
padding: $gap * 2;
|
||||
|
||||
&:first-child {
|
||||
margin-left: -$gap;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-right: -$gap;
|
||||
}
|
||||
}
|
||||
|
||||
.kpi__item__value {
|
||||
@include h1;
|
||||
padding-bottom: $gap / 2;
|
||||
}
|
||||
|
||||
}
|
@ -29,6 +29,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
@mixin panel-row {
|
||||
@include grid-row;
|
||||
|
||||
.col {
|
||||
margin: 0 $site-margins-mobile * 2;
|
||||
|
||||
@include media($medium-screen) {
|
||||
margin: 0 $site-margins * 2;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@mixin panel-actions {
|
||||
padding: $gap;
|
||||
}
|
||||
|
@ -29,8 +29,8 @@
|
||||
|
||||
&.sidenav__link--active {
|
||||
@include h4;
|
||||
background-color: $color-white;
|
||||
color: $color-primary;
|
||||
background-color: $color-white;
|
||||
box-shadow: inset ($gap / 2) 0 0 0 $color-primary;
|
||||
|
||||
.sidenav__link-icon {
|
||||
@ -38,20 +38,32 @@
|
||||
}
|
||||
|
||||
+ ul {
|
||||
background-color: $color-white;
|
||||
background-color: $color-primary;
|
||||
|
||||
.sidenav__link {
|
||||
color: $color-white;
|
||||
background-color: $color-primary;
|
||||
|
||||
&:hover {
|
||||
background-color: $color-blue-darker;
|
||||
}
|
||||
|
||||
&--active {
|
||||
@include h5;
|
||||
color: $color-primary;
|
||||
color: $color-white;
|
||||
background-color: $color-primary;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.icon {
|
||||
@include icon-color($color-white);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+ ul {
|
||||
padding-bottom: $gap / 2;
|
||||
// padding-bottom: $gap / 2;
|
||||
|
||||
li {
|
||||
.sidenav__link {
|
||||
|
100
styles/elements/_tooltip.scss
Normal file
100
styles/elements/_tooltip.scss
Normal file
@ -0,0 +1,100 @@
|
||||
.tooltip {
|
||||
display: block;
|
||||
z-index: 10000;
|
||||
max-width: $text-max-width;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.25);
|
||||
|
||||
.tooltip-inner {
|
||||
background-color: $color-aqua-lightest;
|
||||
padding: $gap * 3;
|
||||
border-left: ($gap / 2) solid $color-blue;
|
||||
}
|
||||
|
||||
.tooltip-arrow {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
position: absolute;
|
||||
background-color: $color-aqua-lightest;
|
||||
z-index: 1;
|
||||
box-shadow: -2px 2px 2px 0 rgba(0,0,0,0.25);
|
||||
}
|
||||
|
||||
&[x-placement^="top"] {
|
||||
margin-bottom: 5px;
|
||||
|
||||
.tooltip-arrow {
|
||||
bottom: -5px;
|
||||
left: calc(50% - 5px);
|
||||
transform: rotate(-45deg);
|
||||
box-shadow: -2px 2px 2px 0 rgba(0,0,0,0.25);
|
||||
}
|
||||
}
|
||||
|
||||
&[x-placement^="bottom"] {
|
||||
margin-top: 5px;
|
||||
|
||||
.tooltip-arrow {
|
||||
top: -5px;
|
||||
left: calc(50% - 5px);
|
||||
transform: rotate(135deg);
|
||||
box-shadow: -2px 2px 2px -2px rgba(0,0,0,0.25);
|
||||
}
|
||||
}
|
||||
|
||||
&[x-placement^="right"] {
|
||||
margin-left: 5px;
|
||||
|
||||
.tooltip-arrow {
|
||||
left: -5px;
|
||||
top: calc(50% - 5px);
|
||||
transform: rotate(-135deg);
|
||||
}
|
||||
}
|
||||
|
||||
&[x-placement^="left"] {
|
||||
margin-right: 5px;
|
||||
|
||||
.tooltip-arrow {
|
||||
right: -5px;
|
||||
top: calc(50% - 5px);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
}
|
||||
|
||||
&.popover {
|
||||
$color: #f9f9f9;
|
||||
|
||||
.popover-inner {
|
||||
background: $color;
|
||||
color: black;
|
||||
padding: 24px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 5px 30px rgba(black, .1);
|
||||
}
|
||||
|
||||
.popover-arrow {
|
||||
border-color: $color;
|
||||
}
|
||||
}
|
||||
|
||||
&[aria-hidden='true'] {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transition: opacity .15s, visibility .15s;
|
||||
}
|
||||
|
||||
&[aria-hidden='false'] {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transition: opacity .15s;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.icon-tooltip {
|
||||
@include icon-link;
|
||||
|
||||
.icon {
|
||||
@include icon-size(16);
|
||||
}
|
||||
}
|
@ -52,9 +52,13 @@ dl {
|
||||
}
|
||||
dd {
|
||||
-webkit-margin-start: 0;
|
||||
|
||||
.label {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
> div {
|
||||
margin-bottom: $gap * 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,11 +3,12 @@
|
||||
{% set context=g.navigationContext %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<title>{% block title %}JEDI{% endblock %}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}JEDI Cloud{% endblock %}</title>
|
||||
{% assets "css" %}
|
||||
<link rel="stylesheet" href="{{ ASSET_URL }}" type="text/css">
|
||||
{% endassets %}
|
||||
|
18
templates/components/checkbox_input.html
Normal file
18
templates/components/checkbox_input.html
Normal file
@ -0,0 +1,18 @@
|
||||
|
||||
{% macro CheckboxInput(field, inline=False) -%}
|
||||
<checkboxinput name='{{ field.name }}' inline-template key='{{ field.name }}'>
|
||||
<div class='usa-input {% if field.errors %}usa-input--error{% endif %}'>
|
||||
|
||||
<fieldset v-on:change="onInput" class="usa-input__choices {% if inline %}usa-input__choices--inline{% endif %}">
|
||||
<legend>
|
||||
{{ field() }}
|
||||
{{ field.label }}
|
||||
|
||||
{% if field.description %}
|
||||
<span class='usa-input__help'>{{ field.description | safe }}</span>
|
||||
{% endif %}
|
||||
</fieldset>
|
||||
</div>
|
||||
</checkboxinput>
|
||||
|
||||
{%- endmacro %}
|
@ -2,18 +2,17 @@
|
||||
|
||||
{% macro Modal(name, dismissable=False) -%}
|
||||
<div v-if='modals.{{name}} === true' v-cloak>
|
||||
<div class='modal'>
|
||||
<div class='modal {% if dismissable %}modal--dismissable{% endif%}'>
|
||||
<div class='modal__dialog' role='dialog' aria-modal='true'>
|
||||
|
||||
{% if dismissable %}
|
||||
<button class='icon-link modal__dismiss' v-on:click='closeModal("{{name}}")'>
|
||||
{{ Icon('x') }}
|
||||
<span>Close</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
<div class='modal__body'>
|
||||
{{ caller() }}
|
||||
|
||||
{% if dismissable %}
|
||||
<button class='icon-link modal__dismiss' v-on:click='closeModal("{{name}}")'>
|
||||
{{ Icon('x') }}
|
||||
<span>Close</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,32 +1,37 @@
|
||||
{% from "components/icon.html" import Icon %}
|
||||
{% from "components/tooltip.html" import Tooltip %}
|
||||
|
||||
{% macro OptionsInput(field, inline=False) -%}
|
||||
<div class='usa-input {% if field.errors %}usa-input--error{% endif %}'>
|
||||
{% macro OptionsInput(field, tooltip, inline=False) -%}
|
||||
<optionsinput name='{{ field.name }}' inline-template key='{{ field.name }}'>
|
||||
<div class='usa-input {% if field.errors %}usa-input--error{% endif %}'>
|
||||
|
||||
<fieldset class="usa-input__choices {% if inline %}usa-input__choices--inline{% endif %}">
|
||||
<legend>
|
||||
{{ field.label | striptags}}
|
||||
<fieldset v-on:change="onInput" class="usa-input__choices {% if inline %}usa-input__choices--inline{% endif %}">
|
||||
<legend>
|
||||
<div class="usa-input__title">
|
||||
{{ field.label | striptags}}
|
||||
{% if tooltip %}{{ Tooltip(tooltip) }}{% endif %}
|
||||
</div>
|
||||
|
||||
{% if field.description %}
|
||||
<span class='usa-input__help'>{{ field.description | safe }}</span>
|
||||
{% endif %}
|
||||
{% if field.description %}
|
||||
<span class='usa-input__help'>{{ field.description | safe }}</span>
|
||||
{% endif %}
|
||||
|
||||
{% if field.errors %}
|
||||
{{ Icon('alert',classes="icon-validation") }}
|
||||
{% endif %}
|
||||
</legend>
|
||||
|
||||
{{ field() }}
|
||||
|
||||
{% if field.errors %}
|
||||
{{ Icon('alert') }}
|
||||
{% for error in field.errors %}
|
||||
<span class='usa-input__message'>{{ error }}</span>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</legend>
|
||||
|
||||
{{ field() }}
|
||||
|
||||
{% if field.errors %}
|
||||
{% for error in field.errors %}
|
||||
<span class='usa-input__message'>{{ error }}</span>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
</optionsinput>
|
||||
|
||||
{%- endmacro %}
|
||||
|
@ -1,23 +1,67 @@
|
||||
{% from "components/icon.html" import Icon %}
|
||||
{% from "components/tooltip.html" import Tooltip %}
|
||||
|
||||
{% macro TextInput(field, placeholder='') -%}
|
||||
<div class='usa-input {% if field.errors %}usa-input--error{% endif %}'>
|
||||
<label for={{field.name}}>
|
||||
{{ field.label }}
|
||||
{% macro TextInput(field, tooltip='', placeholder='', validation='anything', paragraph=False) -%}
|
||||
<textinput
|
||||
name='{{ field.name }}'
|
||||
validation='{{ validation }}'
|
||||
{% if paragraph %}paragraph='true'{% endif %}
|
||||
{% if field.data %}initial-value='{{ field.data }}'{% endif %}
|
||||
{% if field.errors %}v-bind:initial-errors='{{ field.errors }}'{% endif %}
|
||||
key='{{ field.name }}'
|
||||
inline-template>
|
||||
|
||||
<div
|
||||
v-bind:class="['usa-input usa-input--validation--' + validation, { 'usa-input--error': showError, 'usa-input--success': showValid, 'usa-input--validation--paragraph': paragraph }]">
|
||||
|
||||
<label for={{field.name}}>
|
||||
<div class="usa-input__title">{{ field.label | striptags }} {% if tooltip %}{{ Tooltip(tooltip) }}{% endif %}</div>
|
||||
|
||||
{% if field.description %}
|
||||
<span class='usa-input__help'>{{ field.description | safe }}</span>
|
||||
{% endif %}
|
||||
|
||||
<span v-show='showError'>{{ Icon('alert',classes="icon-validation") }}</span>
|
||||
<span v-show='showValid'>{{ Icon('ok',classes="icon-validation") }}</span>
|
||||
|
||||
</label>
|
||||
|
||||
{% if paragraph %}
|
||||
|
||||
<textarea
|
||||
v-on:input='onInput'
|
||||
v-on:change='onChange'
|
||||
v-bind:value='value'
|
||||
id='{{ field.name }}'
|
||||
ref='input'
|
||||
placeholder='{{ placeholder }}'>
|
||||
</textarea>
|
||||
|
||||
{% else %}
|
||||
|
||||
<masked-input
|
||||
v-on:input='onInput'
|
||||
v-on:change='onChange'
|
||||
v-bind:value='value'
|
||||
v-bind:mask='mask'
|
||||
v-bind:pipe='pipe'
|
||||
v-bind:keep-char-positions='keepCharPositions'
|
||||
v-bind:aria-invalid='showError'
|
||||
id='{{ field.name }}'
|
||||
type='text'
|
||||
ref='input'
|
||||
placeholder='{{ placeholder }}'>
|
||||
</masked-input>
|
||||
|
||||
{% if field.description %}
|
||||
<span class='usa-input__help'>{{ field.description | safe }}</span>
|
||||
{% endif %}
|
||||
|
||||
{% if field.errors %}{{ Icon('alert') }}{% endif %}
|
||||
</label>
|
||||
<input type='hidden' v-bind:value='rawValue' name='{{ field.name }}' />
|
||||
|
||||
{{ field(placeholder=placeholder) | safe }}
|
||||
<template v-if='showError'>
|
||||
<span v-if='initialErrors' v-for='error in initialErrors' class='usa-input__message' v-html='error'></span>
|
||||
<span v-if='!initialErrors' class='usa-input__message' v-html='validationError'></span>
|
||||
</template>
|
||||
|
||||
{% if field.errors %}
|
||||
{% for error in field.errors %}
|
||||
<span class='usa-input__message'>{{ error }}</span>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{%- endmacro %}
|
||||
</div>
|
||||
</textinput>
|
||||
{%- endmacro %}
|
||||
|
9
templates/components/tooltip.html
Normal file
9
templates/components/tooltip.html
Normal file
@ -0,0 +1,9 @@
|
||||
{% from "components/icon.html" import Icon %}
|
||||
|
||||
{% macro Tooltip(message,title='Help') -%}
|
||||
|
||||
<span class="icon-tooltip" v-tooltip.top="{content: '{{message}}'}">
|
||||
{{ Icon('help') }}<span>{{ title }}</span>
|
||||
</span>
|
||||
|
||||
{%- endmacro %}
|
11
templates/fragments/pending_ccpo_approval_alert.html
Normal file
11
templates/fragments/pending_ccpo_approval_alert.html
Normal file
@ -0,0 +1,11 @@
|
||||
<p>
|
||||
We will review and respond to your request in 72 hours. You’ll be notified via email or phone.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
While your request is being reviewed, your next step is to create a Task Order associated with JEDI Cloud. Please contact a Contracting Officer (KO), Contracting Officer Representative (COR), or a Financial Manager to help with this step.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Learn more about the JEDI Task Order and the Financial Verification process.
|
||||
</p>
|
35
templates/fragments/pending_ccpo_approval_modal.html
Normal file
35
templates/fragments/pending_ccpo_approval_modal.html
Normal file
@ -0,0 +1,35 @@
|
||||
<h1>
|
||||
Request submitted. Approval pending.
|
||||
</h1>
|
||||
|
||||
<p>
|
||||
We will review and respond to your request in 72 hours. You’ll be notified via email or phone.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Your request is being reviewed because:
|
||||
<ul>
|
||||
<li>
|
||||
Your request includes over $1 million for cloud resources
|
||||
</li>
|
||||
<li>
|
||||
We may need more information about your request
|
||||
</li>
|
||||
</ul>
|
||||
</p>
|
||||
|
||||
<h2>
|
||||
Next Steps
|
||||
</h2>
|
||||
|
||||
<p>
|
||||
While your request is being reviewed, your next step is to create a Task Order associated with JEDI Cloud. Please contact a Contracting Officer (KO), Contracting Officer Representative (COR), or a Financial Manager to help with this step.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Once the Task Order has been created, you will be asked to provide details about the task order in the Financial Verification step.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Learn more about the JEDI Task Order and the Financial Verification process.
|
||||
</p>
|
@ -26,11 +26,6 @@
|
||||
"href": "",
|
||||
"active": g.matchesPath('/workspaces/members/new'),
|
||||
"icon": "plus"
|
||||
},
|
||||
{
|
||||
"label": "Editing Member",
|
||||
"href": "",
|
||||
"active": g.matchesPath('/workspaces/123456/members/789/edit')
|
||||
}
|
||||
]
|
||||
) }}
|
||||
|
@ -16,6 +16,15 @@
|
||||
</div>
|
||||
{% endcall %}
|
||||
|
||||
{% call Modal(name='pendingCCPOApproval', dismissable=True) %}
|
||||
|
||||
{% include 'fragments/pending_ccpo_approval_modal.html' %}
|
||||
|
||||
<div class='action-group'>
|
||||
<a v-on:click="closeModal('pendingCCPOApproval')" class='action-group__action usa-button'>Close</a>
|
||||
</div>
|
||||
{% endcall %}
|
||||
|
||||
{% if not requests %}
|
||||
|
||||
{{ EmptyState(
|
||||
@ -33,38 +42,65 @@
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% if pending_ccpo_approval %}
|
||||
|
||||
{{ Alert('Request submitted. Approval pending.', fragment="fragments/pending_ccpo_approval_alert.html") }}
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% if extended_view %}
|
||||
<div class="row kpi">
|
||||
<div class="kpi__item col col--grow">
|
||||
<div class="kpi__item__value">{{ kpi_inprogress }}</div>
|
||||
<div class="kpi__item__description">In Progress</div>
|
||||
</div>
|
||||
<div class="kpi__item col col--grow">
|
||||
<div class="kpi__item__value">{{ kpi_pending }}</div>
|
||||
<div class="kpi__item__description">Pending CCPO Action</div>
|
||||
</div>
|
||||
<div class="kpi__item col col--grow">
|
||||
<div class="kpi__item__value">{{ kpi_completed }}</div>
|
||||
<div class="kpi__item__description">Completed (Overall)</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="col col--grow">
|
||||
|
||||
<form class='search-bar'>
|
||||
<div class='usa-input search-input'>
|
||||
<label for='requests-search'>Search requests by Order ID</label>
|
||||
<input type='search' id='requests-search' name='requests-search' placeholder="Search by Order ID"/>
|
||||
<button type="submit">
|
||||
<span class="hide">Search</span>
|
||||
</button>
|
||||
</div>
|
||||
{% if extended_view %}
|
||||
<form class='search-bar'>
|
||||
<div class='usa-input search-input'>
|
||||
<label for='requests-search'>Search requests by Order ID</label>
|
||||
<input type='search' id='requests-search' name='requests-search' placeholder="Search by Order ID"/>
|
||||
<button type="submit">
|
||||
<span class="hide">Search</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class='usa-input'>
|
||||
<label for='filter-status'>Filter requests by status</label>
|
||||
<select id="filter-status" name="filter-status">
|
||||
<option value="" selected disabled>Filter by status</option>
|
||||
<option value="">Active</option>
|
||||
<option value="">Pending</option>
|
||||
<option value="">Denied</option>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
<div class='usa-input'>
|
||||
<label for='filter-status'>Filter requests by status</label>
|
||||
<select id="filter-status" name="filter-status">
|
||||
<option value="" selected disabled>Filter by status</option>
|
||||
<option value="">Active</option>
|
||||
<option value="">Pending</option>
|
||||
<option value="">Denied</option>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
<div class='responsive-table-wrapper'>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Order ID</th>
|
||||
<th scope="col">Request Date</th>
|
||||
<th scope="col">Requester</th>
|
||||
<th scope="col">Total Apps</th>
|
||||
<th scope="col">Status</th>
|
||||
<th scope="col" class="table-cell--shrink">Actions</th>
|
||||
<th scope="col">JEDI Cloud Request ID</th>
|
||||
<th scope="col">Date Request Initiated / Created</th>
|
||||
{% if extended_view %}
|
||||
<th scope="col">Requester</th>
|
||||
<th scope="col">Reason Flagged</th>
|
||||
{% endif %}
|
||||
<th scope="col">Projected Annual Usage ($)</th>
|
||||
<th scope="col">Request Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -76,13 +112,12 @@
|
||||
</th>
|
||||
{% endif %}
|
||||
<td>{{ r['date'] }}</td>
|
||||
<td>{{ r['full_name'] }}</td>
|
||||
<td>{{ r['app_count'] }}</td>
|
||||
{% if extended_view %}
|
||||
<td>{{ r['full_name'] }}</td>
|
||||
<td></td>
|
||||
{% endif %}
|
||||
<td>${{ r['annual_usage'] }}</td>
|
||||
<td>{{ r['status'] }}</td>
|
||||
<td class="table-cell--shrink">
|
||||
<a href="" class='icon-link'>Download</a>
|
||||
<a href="/request/approval" class='icon-link'>Approval</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
@ -1,17 +1,21 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% from "components/alert.html" import Alert %}
|
||||
{% from "components/text_input.html" import TextInput %}
|
||||
{% from "components/options_input.html" import OptionsInput %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="col">
|
||||
|
||||
<div class="panel">
|
||||
|
||||
<div class="panel__content">
|
||||
<div class="panel__heading">
|
||||
<h1>Order #{{ request_id }}</h1>
|
||||
<div class="subtitle" id="financial-verification"><h2>Financial Verification</h2></div>
|
||||
</div>
|
||||
|
||||
<div class="panel__heading">
|
||||
<h1>Order #{{ request_id }}</h1>
|
||||
<h2 id="financial-verification">Financial Verification</h2>
|
||||
</div>
|
||||
<div class="panel__content">
|
||||
|
||||
{% block form_action %}
|
||||
<form method='POST' action="{{ url_for('requests.financial_verification', request_id=request_id) }}" autocomplete="off">
|
||||
@ -20,192 +24,47 @@
|
||||
{{ f.csrf_token }}
|
||||
{% block form %}
|
||||
{% autoescape false %}
|
||||
|
||||
{% if f.errors %}
|
||||
<b class="usa-input-error-message">There were some errors, see below.</b>
|
||||
{{ Alert('There were some errors',
|
||||
message="<p>Please see below.</p>",
|
||||
level='error'
|
||||
) }}
|
||||
{% endif %}
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
{{ f.task_order_id.label }}
|
||||
{{ f.task_order_id(placeholder="Example: 1234567899C0001") }}
|
||||
{% for e in f.task_order_id.errors %}
|
||||
<div class="usa-input-error-message">
|
||||
{{ e }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{{ f.uii_ids.label }}
|
||||
{{ f.uii_ids(placeholder="Example: \nDI 0CVA5786950 \nUN1945326361234786950") }}
|
||||
{% for e in f.uii_ids.errors %}
|
||||
<div class="usa-input-error-message">
|
||||
{{ e }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{{ f.pe_id.label }}
|
||||
{{ f.pe_id(placeholder="Example: 0203752A") }}
|
||||
{% for e in f.pe_id.errors %}
|
||||
<div class="usa-input-error-message">
|
||||
{{ e }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{{ f.treasury_code.label }}
|
||||
{{ f.treasury_code(placeholder="Example: 1200") }}
|
||||
{% for e in f.treasury_code.errors %}
|
||||
<div class="usa-input-error-message">
|
||||
{{ e }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{{ f.ba_code.label }}
|
||||
{{ f.ba_code(placeholder="Example: 02") }}
|
||||
{% for e in f.ba_code.errors %}
|
||||
<div class="usa-input-error-message">
|
||||
{{ e }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<!-- KO Information -->
|
||||
{{ 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. <br>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. <br>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") }}
|
||||
|
||||
<h3>Contracting Officer (KO) Information</h3>
|
||||
{{ 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") }}
|
||||
|
||||
{{ f.fname_co.label }}
|
||||
{{ f.fname_co(placeholder="Contracting Officer first name") }}
|
||||
{% for e in f.fname_co.errors %}
|
||||
<div class="usa-input-error-message">
|
||||
{{ e }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{{ f.lname_co.label }}
|
||||
{{ f.lname_co(placeholder="Contracting Officer last name") }}
|
||||
{% for e in f.lname_co.errors %}
|
||||
<div class="usa-input-error-message">
|
||||
{{ e }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{{ f.email_co.label }}
|
||||
{{ f.email_co(placeholder="jane@mail.mil") }}
|
||||
{% for e in f.email_co.errors %}
|
||||
<div class="usa-input-error-message">
|
||||
{{ e }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{{ f.office_co.label }}
|
||||
{{ f.office_co(placeholder="Example: WHS") }}
|
||||
{% for e in f.office_co.errors %}
|
||||
<div class="usa-input-error-message">
|
||||
{{ e }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
|
||||
<!-- COR Information -->
|
||||
|
||||
<h3>Contracting Officer Representative (COR) Information</h3>
|
||||
|
||||
{{ f.fname_cor.label }}
|
||||
{{ f.fname_cor(placeholder="Contracting Officer Representative first name") }}
|
||||
{% for e in f.fname_cor.errors %}
|
||||
<div class="usa-input-error-message">
|
||||
{{ e }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{{ f.lname_cor.label }}
|
||||
{{ f.lname_cor(placeholder="Contracting Officer Representative last name") }}
|
||||
{% for e in f.lname_cor.errors %}
|
||||
<div class="usa-input-error-message">
|
||||
{{ e }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{{ f.email_cor.label }}
|
||||
{{ f.email_cor(placeholder="jane@mail.mil") }}
|
||||
{% for e in f.email_cor.errors %}
|
||||
<div class="usa-input-error-message">
|
||||
{{ e }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{{ f.office_cor.label }}
|
||||
{{ f.office_cor(placeholder="Example: WHS") }}
|
||||
{% for e in f.office_cor.errors %}
|
||||
<div class="usa-input-error-message">
|
||||
{{ e }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{{ 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") }}
|
||||
|
||||
<br><hr>
|
||||
<em>↓ FIELDS NEEDED FOR MANUAL ENTRY OF TASK ORDER INFORMATION (only necessary if EDA info not available)</em>
|
||||
|
||||
{{ 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.") }}
|
||||
|
||||
{{ f.funding_type.label }}
|
||||
{{ f.funding_type }}
|
||||
{% for e in f.funding_type.errors %}
|
||||
<div class="usa-input-error-message">
|
||||
{{ e }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{{ f.funding_type_other.label }}
|
||||
{{ f.funding_type_other(placeholder="") }}
|
||||
{% for e in f.funding_type_other.errors %}
|
||||
<div class="usa-input-error-message">
|
||||
{{ e }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{{ f.clin_0001.label }}
|
||||
{{ f.clin_0001(placeholder="50,000") }}
|
||||
{% for e in f.clin_0001.errors %}
|
||||
<div class="usa-input-error-message">
|
||||
{{ e }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{{ f.clin_0003.label }}
|
||||
{{ f.clin_0003(placeholder="13,000") }}
|
||||
{% for e in f.clin_0003.errors %}
|
||||
<div class="usa-input-error-message">
|
||||
{{ e }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{{ f.clin_1001.label }}
|
||||
{{ f.clin_1001(placeholder="30,000") }}
|
||||
{% for e in f.clin_1001.errors %}
|
||||
<div class="usa-input-error-message">
|
||||
{{ e }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{{ f.clin_1003.label }}
|
||||
{{ f.clin_1003(placeholder="7,000") }}
|
||||
{% for e in f.clin_1003.errors %}
|
||||
<div class="usa-input-error-message">
|
||||
{{ e }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{{ f.clin_2001.label }}
|
||||
{{ f.clin_2001(placeholder="30,000") }}
|
||||
{% for e in f.clin_2001.errors %}
|
||||
<div class="usa-input-error-message">
|
||||
{{ e }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{{ f.clin_2003.label }}
|
||||
{{ f.clin_2003(placeholder="7,000") }}
|
||||
{% for e in f.clin_2003.errors %}
|
||||
<div class="usa-input-error-message">
|
||||
{{ e }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endautoescape %}
|
||||
{% endblock form %}
|
||||
{% block next %}
|
||||
@ -217,4 +76,4 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
{% endblock %}
|
@ -1,7 +1,7 @@
|
||||
<div class="progress-menu progress-menu--four">
|
||||
<ul>
|
||||
{% for s in screens %}
|
||||
{% if loop.index < current %}
|
||||
{% if jedi_request and s.section in jedi_request.body %}
|
||||
{% set step_indicator = 'complete' %}
|
||||
{% elif loop.index == current %}
|
||||
{% set step_indicator = 'active' %}
|
||||
|
@ -17,32 +17,68 @@
|
||||
) }}
|
||||
{% endif %}
|
||||
|
||||
<details-of-use inline-template v-bind:initial-data='{{ f.data|tojson }}'>
|
||||
<div>
|
||||
|
||||
<p>We’d like to know a little about how you plan to use JEDI Cloud services to process your request. Please answer the following questions to the best of your ability. Note that the CCPO does not directly help with migrating systems to JEDI Cloud. These questions are for learning about your cloud readiness and financial usage of the JEDI Cloud; your estimates will not be used for any department level reporting.</p>
|
||||
<p><em>All fields are required, unless specified optional.</em></p>
|
||||
<p>We’d like to know a little about how you plan to use JEDI Cloud services to process your request. Please answer the following questions to the best of your ability. Note that the CCPO does not directly help with migrating systems to JEDI Cloud. These questions are for learning about your cloud readiness and financial usage of the JEDI Cloud; your estimates will not be used for any department level reporting.</p>
|
||||
<p><em>All fields are required, unless specified optional.</em></p>
|
||||
|
||||
<h2>General</h2>
|
||||
{{ TextInput(f.dod_component) }}
|
||||
{{ TextInput(f.jedi_usage,placeholder="e.g. We are migrating XYZ application to the cloud so that...") }}
|
||||
<h2>General</h2>
|
||||
{{ 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...") }}
|
||||
|
||||
<h2>Cloud Readiness</h2>
|
||||
{{ TextInput(f.num_software_systems,placeholder="Number of systems") }}
|
||||
{{ OptionsInput(f.jedi_migration) }}
|
||||
{{ OptionsInput(f.rationalization_software_systems) }}
|
||||
{{ OptionsInput(f.technical_support_team) }}
|
||||
{{ OptionsInput(f.organization_providing_assistance) }}
|
||||
{{ OptionsInput(f.engineering_assessment) }}
|
||||
{{ TextInput(f.data_transfers) }}
|
||||
{{ TextInput(f.expected_completion_date) }}
|
||||
{{ OptionsInput(f.cloud_native) }}
|
||||
<h2>Cloud Readiness</h2>
|
||||
{{ 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") }}
|
||||
{{ OptionsInput(f.jedi_migration, tooltip="Cloud migration is the process of moving data, applications or other business elements from an organization\\'s onsite computers/data centers to the cloud, or moving them from one cloud environment to another.") }}
|
||||
|
||||
<h2>Financial Usage</h2>
|
||||
{{ TextInput(f.estimated_monthly_spend) }}
|
||||
<p>So this means you are spending approximately <b>$X</b> annually</p>
|
||||
{{ TextInput(f.dollar_value) }}
|
||||
{{ TextInput(f.number_user_sessions) }}
|
||||
{{ TextInput(f.average_daily_traffic) }}
|
||||
{{ TextInput(f.start_date) }}
|
||||
<transition name='slide'>
|
||||
<template v-if="jediMigrationOptionSelected">
|
||||
<fieldset class='form__sub-fields' v-if='isJediMigration' v-cloak>
|
||||
<legend class='usa-sr-only'>Questions related to JEDI Cloud migration</legend>
|
||||
{{ OptionsInput(f.rationalization_software_systems, tooltip="Rationalization is the DoD process to determine whether the application should move to the cloud.") }}
|
||||
{{ OptionsInput(f.technical_support_team) }}
|
||||
<transition name='slide'>
|
||||
<template v-if="hasTechnicalSupportTeam">
|
||||
{{ OptionsInput(f.organization_providing_assistance) }}
|
||||
</template>
|
||||
</transition>
|
||||
{{ OptionsInput(f.engineering_assessment, tooltip="An engineering assessment is an evaluation to convert your application architecture from on-premises to using the commercial cloud") }}
|
||||
{{ OptionsInput(f.data_transfers) }}
|
||||
{{ OptionsInput(f.expected_completion_date) }}
|
||||
</fieldset>
|
||||
|
||||
<template v-if='!isJediMigration' v-cloak>
|
||||
{{ OptionsInput(f.cloud_native, tooltip="Cloud native is architecting and designing your application to use all the benefits of the commercial cloud. Specifically, designing applications so that they are decoupled from a physical resource.") }}
|
||||
</template>
|
||||
</template>
|
||||
</transition>
|
||||
|
||||
<h2>Financial Usage</h2>
|
||||
{{ TextInput(f.estimated_monthly_spend, tooltip="Refer to financial verification step help docs", validation="dollars", placeholder="$0") }}
|
||||
|
||||
<div class='alert alert-info'>
|
||||
<div class='alert__content'>
|
||||
<div class='alert__message'>
|
||||
<p>So this means you are spending approximately <span class="label label--info">!{ annualSpendStr }</span> annually</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<transition name='slide'>
|
||||
<template v-if="annualSpend > 1000000">
|
||||
<fieldset class='form__sub-fields'>
|
||||
<h3>Because the approximate annual spend is over $1,000,000, please answer a few additional questions.</h3>
|
||||
{{ TextInput(f.number_user_sessions, validation='integer', placeholder="0") }}
|
||||
{{ TextInput(f.average_daily_traffic, tooltip="Requests are the client-to-server network traffic that is being transferred to your systems",validation="integer", placeholder="0") }}
|
||||
{{ TextInput(f.average_daily_traffic_gb, tooltip="GB uploaded is the gigabyte amount of data traffic that is being transferred to your systems",validation="gigabytes", placeholder="0GB") }}
|
||||
</fieldset>
|
||||
</template>
|
||||
</transition>
|
||||
|
||||
{{ TextInput(f.dollar_value, validation='dollars', placeholder="$0") }}
|
||||
{{ TextInput(f.start_date, validation='date', placeholder='MM / DD / YYYY') }}
|
||||
|
||||
</div>
|
||||
</details-of-use>
|
||||
|
||||
{% endblock %}
|
||||
|
@ -19,16 +19,15 @@
|
||||
|
||||
<p>Please tell us more about you.</p>
|
||||
|
||||
{{ TextInput(f.fname_request,placeholder='First Name') }}
|
||||
{{ TextInput(f.lname_request,placeholder='Last Name') }}
|
||||
{{ TextInput(f.email_request,placeholder='jane@mail.mil') }}
|
||||
{{ TextInput(f.phone_number,placeholder='(123) 456-7890') }}
|
||||
{{ TextInput(f.fname_request, placeholder='First Name') }}
|
||||
{{ TextInput(f.lname_request, placeholder='Last Name') }}
|
||||
{{ TextInput(f.email_request, placeholder='jane@mail.mil', validation='email') }}
|
||||
{{ TextInput(f.phone_number, placeholder='e.g. (123) 456-7890', validation='usPhone') }}
|
||||
|
||||
<p>We want to collect the following information from you for security auditing and determining priviledged user access</p>
|
||||
<p>We want to collect the following information from you for security auditing and determining priviledged user access.</p>
|
||||
|
||||
{{ TextInput(f.service_branch,placeholder='e.g. US Air Force, US Army, US Navy, Marine Corps, Defense Media Agency') }}
|
||||
{{ OptionsInput(f.service_branch) }}
|
||||
{{ OptionsInput(f.citizenship) }}
|
||||
{{ OptionsInput(f.designation) }}
|
||||
{{ TextInput(f.date_latest_training) }}
|
||||
|
||||
{{ TextInput(f.date_latest_training,tooltip="When was the last time you completed the IA training? <br> Information Assurance (IA) training is an important step in cyber awareness.",placeholder="MM / DD / YYYY", validation="date") }}
|
||||
{% endblock %}
|
||||
|
@ -2,9 +2,10 @@
|
||||
|
||||
{% from "components/alert.html" import Alert %}
|
||||
{% from "components/text_input.html" import TextInput %}
|
||||
{% from "components/checkbox_input.html" import CheckboxInput %}
|
||||
|
||||
{% block subtitle %}
|
||||
<h2>Primary Government/Military <br> Point of Contact (POC)</h2>
|
||||
<h2>Designate a Workspace Owner</h2>
|
||||
{% endblock %}
|
||||
|
||||
{% block form %}
|
||||
@ -16,21 +17,31 @@
|
||||
) }}
|
||||
{% endif %}
|
||||
|
||||
<p>Please designate a Primary Point of Contact that will be responsible for owning the workspace in the JEDI Cloud.</p>
|
||||
<p>The Point of Contact will become the primary owner of the <em>workspace</em> created to use the JEDI Cloud. As a workspace owner, this person will have the ability to:
|
||||
<ul>
|
||||
<li>Create multiple application stacks and environments in the workspace to access the commercial cloud service provider portal</li>
|
||||
<li>Add and manage users in the workspace</li>
|
||||
<li>View the budget and billing history related to this workspace</li>
|
||||
<li>Manage access to the Cloud Service Provider's Console</li>
|
||||
<li>Transfer Workspace ownership to another person</li>
|
||||
</ul>
|
||||
<em>This POC may be you.</em>
|
||||
</p>
|
||||
<poc inline-template v-bind:initial-data='{{ f.data|tojson }}'>
|
||||
<div>
|
||||
|
||||
{{ TextInput(f.fname_poc,placeholder='First Name') }}
|
||||
{{ TextInput(f.lname_poc,placeholder='Last Name') }}
|
||||
{{ TextInput(f.email_poc,placeholder='jane@mail.mil') }}
|
||||
{{ TextInput(f.dodid_poc,placeholder='10-digit number on the back of the CAC') }}
|
||||
<p>The Workspace Owner is the primary point of contact and technical administrator of the JEDI Workspace and will have the
|
||||
following responsibilities:</p>
|
||||
<ul>
|
||||
<li>Organize your cloud-hosted systems into projects and environments</li>
|
||||
<li>Add users to this workspace and manage members</li>
|
||||
<li>Manage access to the JEDI Cloud service provider’s portal</li>
|
||||
</ul>
|
||||
</p>
|
||||
|
||||
<p>This person must be a DoD employee (not a contractor).</p>
|
||||
<p>The Workspace Owner may be you. You will be able to add other administrators later. This person will be invited via email
|
||||
once your request is approved.</p>
|
||||
|
||||
{{ CheckboxInput(f.am_poc) }}
|
||||
|
||||
<template v-if="!am_poc" v-cloak>
|
||||
{{ TextInput(f.fname_poc,placeholder='First Name') }}
|
||||
{{ TextInput(f.lname_poc,placeholder='Last Name') }}
|
||||
{{ TextInput(f.email_poc,placeholder='jane@mail.mil', validation='email') }}
|
||||
{{ TextInput(f.dodid_poc,placeholder='10-digit number on the back of the CAC', validation='dodId') }}
|
||||
</template>
|
||||
|
||||
</div>
|
||||
</poc>
|
||||
{% endblock %}
|
||||
|
@ -1,7 +1,12 @@
|
||||
{% macro RequiredLabel() -%}
|
||||
<span class='label label--error'>Response Required</span>
|
||||
{%- endmacro %}
|
||||
|
||||
{% extends 'requests_new.html' %}
|
||||
|
||||
{% from "components/alert.html" import Alert %}
|
||||
{% from "components/text_input.html" import TextInput %}
|
||||
{% from "components/icon.html" import Icon %}
|
||||
|
||||
{% block subtitle %}
|
||||
<h2>Review & Submit</h2>
|
||||
@ -14,167 +19,201 @@
|
||||
|
||||
{% block form %}
|
||||
|
||||
{% if f.errors %}
|
||||
{{ Alert('There were some errors',
|
||||
message="<p>Please complete all required fields before submitting.</p>",
|
||||
<p>Before you can submit your request, please take a moment to review the information entered in the form. You may make changes by clicking the edit link on each section. When all information looks right, go ahead and submit.</p>
|
||||
|
||||
{% if f.errors or not can_submit%}
|
||||
{{ Alert('Please complete all sections',
|
||||
message="<p>In order to submit your JEDI Cloud request, you'll need to complete all required sections of this form without error. Missing or invalid fields are noted below.</p>",
|
||||
level='error'
|
||||
) }}
|
||||
{% endif %}
|
||||
|
||||
<p>Before you can submit your request, please take a moment to review the information entered in the form. You may make changes by clicking the edit link on each section. When all information looks right, go ahead and submit.</p>
|
||||
|
||||
<h2>Details of Use <a href="{{ url_for('requests.requests_form_update', screen=1, request_id=request_id) }}" class="icon-link">Edit</a></h2>
|
||||
<h2>
|
||||
Details of Use
|
||||
<a href="{{ url_for('requests.requests_form_update', screen=1, request_id=request_id) }}" class="icon-link">
|
||||
{{ Icon('edit') }}
|
||||
<span>Edit this section</span>
|
||||
</a>
|
||||
</h2>
|
||||
|
||||
<dl>
|
||||
<div>
|
||||
<dt>DoD Component</dt>
|
||||
<dd>{{data['details_of_use']['dod_component']}}</dd>
|
||||
<dd>{{ data['details_of_use']['dod_component'] or RequiredLabel() }}</dd>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<dt>JEDI Usage</dt>
|
||||
<dd>{{data['details_of_use']['jedi_usage']}}</dd>
|
||||
<dd>{{ data['details_of_use']['jedi_usage'] or RequiredLabel() }}</dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt>Number of software systems</dt>
|
||||
<dd>{{data['details_of_use']['num_software_systems']}}</dd>
|
||||
<dd>{{ data['details_of_use']['num_software_systems'] or RequiredLabel() }}</dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt>JEDI Migration</dt>
|
||||
<dd>{{data['details_of_use']['jedi_migration']}}</dd>
|
||||
<dd>{{ data['details_of_use']['jedi_migration'] or RequiredLabel() }}</dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt>Rationalization of Software Systems</dt>
|
||||
<dd>{{data['details_of_use']['rationalization_software_systems']}}</dd>
|
||||
</div>
|
||||
{% if data['details_of_use']['jedi_migration'] == 'yes' %}
|
||||
<div>
|
||||
<dt>Rationalization of Software Systems</dt>
|
||||
<dd>{{ data['details_of_use']['rationalization_software_systems'] or RequiredLabel() }}</dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt>Technical Support Team</dt>
|
||||
<dd>{{data['details_of_use']['technical_support_team']}}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Technical Support Team</dt>
|
||||
<dd>{{ data['details_of_use']['technical_support_team'] or RequiredLabel() }}</dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt>Organization Providing Assistance</dt>
|
||||
<dd>{{data['details_of_use']['organization_providing_assistance']}}</dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt>Engineering Assessment</dt>
|
||||
<dd>{{data['details_of_use']['engineering_assessment']}}</dd>
|
||||
</div>
|
||||
{% if data['details_of_use']['technical_support_team'] == 'yes' %}
|
||||
|
||||
<div>
|
||||
<dt>Data Transfers</dt>
|
||||
<dd>{{data['details_of_use']['data_transfers']}}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Organization Providing Assistance</dt>
|
||||
<dd>{{ data['details_of_use']['organization_providing_assistance'] or RequiredLabel() }}</dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt>Expected Completion Date</dt>
|
||||
<dd>{{data['details_of_use']['expected_completion_date']}}</dd>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div>
|
||||
<dt>Cloud Native</dt>
|
||||
<dd>{{data['details_of_use']['cloud_native']}}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Engineering Assessment</dt>
|
||||
<dd>{{ data['details_of_use']['engineering_assessment'] or RequiredLabel() }}</dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt>Data Transfers</dt>
|
||||
<dd>{{ data['details_of_use']['data_transfers'] or RequiredLabel() }}</dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt>Expected Completion Date</dt>
|
||||
<dd>{{ data['details_of_use']['expected_completion_date'] or RequiredLabel() }}</dd>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
|
||||
<div>
|
||||
<dt>Cloud Native</dt>
|
||||
<dd>{{ data['details_of_use']['cloud_native'] or RequiredLabel() }}</dd>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
<div>
|
||||
<dt>Estimated Monthly Spend</dt>
|
||||
<dd>{{data['details_of_use']['estimated_monthly_spend']}}</dd>
|
||||
<dd>{{ data['details_of_use']['estimated_monthly_spend'] or RequiredLabel() }}</dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt>Total Spend</dt>
|
||||
<dd>${{data['details_of_use']['dollar_value']}}</dd>
|
||||
<dd>{{ data['details_of_use']['dollar_value'] or RequiredLabel() }}</dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt>Number of User Sessions</dt>
|
||||
<dd>{{data['details_of_use']['number_user_sessions']}}</dd>
|
||||
<dd>{{ data['details_of_use']['number_user_sessions'] or RequiredLabel() }}</dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt>Average Daily Traffic</dt>
|
||||
<dd>{{data['details_of_use']['average_daily_traffic']}}</dd>
|
||||
<dt>Average Daily Traffic (Number of Requests)</dt>
|
||||
<dd>{{ data['details_of_use']['average_daily_traffic'] or RequiredLabel() }}</dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt>Average Daily Traffic (GB)</dt>
|
||||
<dd>{{ data['details_of_use']['average_daily_traffic_gb'] or RequiredLabel() }}</dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt>Start Date</dt>
|
||||
<dd>{{data['details_of_use']['start_date']}}</dd>
|
||||
<dd>{{ data['details_of_use']['start_date'] or RequiredLabel() }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
|
||||
<h2>Information About You <a href="{{ url_for('requests.requests_form_update', screen=2, request_id=request_id) }}" class="icon-link">Edit</a></h2>
|
||||
<h2>
|
||||
Information About You
|
||||
<a href="{{ url_for('requests.requests_form_update', screen=2, request_id=request_id) }}" class="icon-link">
|
||||
{{ Icon('edit') }}
|
||||
<span>Edit this section</span>
|
||||
</a>
|
||||
</h2>
|
||||
|
||||
<dl>
|
||||
<div>
|
||||
<dt>First Name</dt>
|
||||
<dd>{{data['information_about_you']['fname_request']}}</dd>
|
||||
<dd>{{ data['information_about_you']['fname_request'] or RequiredLabel() }}</dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt>Last Name</dt>
|
||||
<dd>{{data['information_about_you']['lname_request']}}</dd>
|
||||
<dd>{{ data['information_about_you']['lname_request'] or RequiredLabel() }}</dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt>Email Address</dt>
|
||||
<dd>{{data['information_about_you']['email_request']}}</dd>
|
||||
<dd>{{ data['information_about_you']['email_request'] or RequiredLabel() }}</dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt>Phone Number</dt>
|
||||
<dd>{{data['information_about_you']['phone_number']}}</dd>
|
||||
<dd>{{ data['information_about_you']['phone_number'] or RequiredLabel() }}</dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt>Service Branch or Agency</dt>
|
||||
<dd>{{data['information_about_you']['service_branch']}}</dd>
|
||||
<dd>{{ data['information_about_you']['service_branch'] or RequiredLabel() }}</dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt>Citizenship</dt>
|
||||
<dd>{{data['information_about_you']['citizenship']}}</dd>
|
||||
<dd>{{ data['information_about_you']['citizenship'] or RequiredLabel() }}</dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt>Designation of Person</dt>
|
||||
<dd>{{data['information_about_you']['designation']}}</dd>
|
||||
<dd>{{ data['information_about_you']['designation'] or RequiredLabel() }}</dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt>Latest Information Assurance (IA) Training completion date</dt>
|
||||
<dd>{{data['information_about_you']['date_latest_training']}}</dd>
|
||||
<dd>{{ data['information_about_you']['date_latest_training'] or RequiredLabel() }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
|
||||
<h2>Primary Point of Contact <a href="{{ url_for('requests.requests_form_update', screen=3, request_id=request_id) }}" class="icon-link">Edit</a></h2>
|
||||
<h2>
|
||||
Primary Point of Contact
|
||||
<a href="{{ url_for('requests.requests_form_update', screen=3, request_id=request_id) }}" class="icon-link">
|
||||
{{ Icon('edit') }}
|
||||
<span>Edit this section</span>
|
||||
</a>
|
||||
</h2>
|
||||
|
||||
<dl>
|
||||
<div>
|
||||
<dt>POC First Name</dt>
|
||||
<dd>{{data['primary_poc']['fname_poc']}}</dd>
|
||||
<dd>{{ data['primary_poc']['fname_poc'] or RequiredLabel() }}</dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt>POC Last Name</dt>
|
||||
<dd>{{data['primary_poc']['lname_poc']}}</dd>
|
||||
<dd>{{ data['primary_poc']['lname_poc'] or RequiredLabel() }}</dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt>POC Email Address</dt>
|
||||
<dd>{{data['primary_poc']['email_poc']}}</dd>
|
||||
<dd>{{ data['primary_poc']['email_poc'] or RequiredLabel() }}</dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt>DOD ID</dt>
|
||||
<dd>{{data['primary_poc']['dodid_poc']}}</dd>
|
||||
<dd>{{ data['primary_poc']['dodid_poc'] or RequiredLabel() }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
@ -183,12 +222,6 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block next %}
|
||||
{% if not can_submit %}
|
||||
{{ Alert('There were some errors',
|
||||
message="<p>Please complete all required fields before submitting.</p>",
|
||||
level='error'
|
||||
) }}
|
||||
{% endif %}
|
||||
|
||||
<div class='action-group'>
|
||||
<input type='submit' class='usa-button usa-button-primary' value='Submit' {{ "disabled" if not can_submit else "" }} />
|
||||
|
@ -3,6 +3,7 @@
|
||||
{% from "components/icon.html" import Icon %}
|
||||
{% from "components/modal.html" import Modal %}
|
||||
{% from "components/alert.html" import Alert %}
|
||||
{% from "components/tooltip.html" import Tooltip %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
@ -128,7 +129,6 @@
|
||||
<div class='col col--grow'>col 12</div>
|
||||
</div>
|
||||
|
||||
|
||||
<form>
|
||||
<textinput inline-template validation='dollars' value='1231231'>
|
||||
<div v-bind:class="['usa-input usa-input--validation--' + validation, { 'usa-input--error': showError, 'usa-input--success': showValid }]">
|
||||
@ -286,6 +286,27 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<div class="panel__content">
|
||||
<h5>Tooltips</h5>
|
||||
{{ Tooltip('this is a tooltip text') }}<code>default</code> <br>
|
||||
<span v-tooltip.top-start="'this is a tooltip text'">{{ Icon('help') }}</span><code>top-start</code> <br>
|
||||
<span v-tooltip.right="'this is a tooltip text'">{{ Icon('help') }}</span><code>right</code> <br>
|
||||
<span v-tooltip.bottom="'this is a tooltip text'">{{ Icon('help') }}</span><code>bottom</code> <br>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<div class="panel__content">
|
||||
<h5>Labels</h5>
|
||||
<span class="label">Label</span>
|
||||
<span class="label label--info">Label Info</span>
|
||||
<span class="label label--warning">Label Warning</span>
|
||||
<span class="label label--error">Label Error</span>
|
||||
<span class="label label--success">Label Success</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='panel'>
|
||||
Another panel without padding
|
||||
</div>
|
||||
|
@ -69,6 +69,7 @@ def session(db, request):
|
||||
]
|
||||
for factory in factory_list:
|
||||
factory._meta.sqlalchemy_session = session
|
||||
factory._meta.sqlalchemy_session_persistence = "commit"
|
||||
|
||||
yield session
|
||||
|
||||
|
@ -66,8 +66,9 @@ def test_parse_disa_pki_list():
|
||||
assert len(crl_list) == len(href_matches)
|
||||
|
||||
class MockStreamingResponse():
|
||||
def __init__(self, content_chunks):
|
||||
def __init__(self, content_chunks, code=200):
|
||||
self.content_chunks = content_chunks
|
||||
self.status_code = code
|
||||
|
||||
def iter_content(self, chunk_size=0):
|
||||
return self.content_chunks
|
||||
@ -81,6 +82,10 @@ class MockStreamingResponse():
|
||||
def test_write_crl(tmpdir, monkeypatch):
|
||||
monkeypatch.setattr('requests.get', lambda u, **kwargs: MockStreamingResponse([b'it worked']))
|
||||
crl = 'crl_1'
|
||||
util.write_crl(tmpdir, crl)
|
||||
assert util.write_crl(tmpdir, "random_target_dir", crl)
|
||||
assert [p.basename for p in tmpdir.listdir()] == [crl]
|
||||
assert [p.read() for p in tmpdir.listdir()] == ['it worked']
|
||||
|
||||
def test_skips_crl_if_it_has_not_been_modified(tmpdir, monkeypatch):
|
||||
monkeypatch.setattr('requests.get', lambda u, **kwargs: MockStreamingResponse([b'it worked'], 304))
|
||||
assert not util.write_crl(tmpdir, "random_target_dir", 'crl_file_name')
|
||||
|
21
tests/domain/test_date.py
Normal file
21
tests/domain/test_date.py
Normal file
@ -0,0 +1,21 @@
|
||||
import pytest
|
||||
import pendulum
|
||||
|
||||
from atst.domain.date import parse_date
|
||||
|
||||
|
||||
def test_date_with_slashes():
|
||||
date_str = "1/2/2020"
|
||||
assert parse_date(date_str) == pendulum.date(2020, 1, 2)
|
||||
|
||||
|
||||
def test_date_with_dashes():
|
||||
date_str = "2020-1-2"
|
||||
assert parse_date(date_str) == pendulum.date(2020, 1, 2)
|
||||
|
||||
|
||||
def test_invalid_date():
|
||||
date_str = "This is not a valid data"
|
||||
with pytest.raises(ValueError):
|
||||
parse_date(date_str)
|
||||
|
@ -3,9 +3,10 @@ from uuid import uuid4
|
||||
|
||||
from atst.domain.exceptions import NotFoundError
|
||||
from atst.domain.requests import Requests
|
||||
from atst.models.request import Request
|
||||
from atst.models.request_status_event import RequestStatus
|
||||
|
||||
from tests.factories import RequestFactory, UserFactory
|
||||
from tests.factories import RequestFactory, UserFactory, RequestStatusEventFactory
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
@ -63,3 +64,27 @@ def test_exists(session):
|
||||
request = RequestFactory.create(creator=user_allowed)
|
||||
assert Requests.exists(request.id, user_allowed)
|
||||
assert not Requests.exists(request.id, user_denied)
|
||||
|
||||
|
||||
def test_status_count(session):
|
||||
# make sure table is empty
|
||||
session.query(Request).delete()
|
||||
|
||||
request1 = RequestFactory.create()
|
||||
request2 = RequestFactory.create()
|
||||
RequestStatusEventFactory.create(sequence=2, request_id=request2.id, new_status=RequestStatus.PENDING_FINANCIAL_VERIFICATION)
|
||||
|
||||
assert Requests.status_count(RequestStatus.PENDING_FINANCIAL_VERIFICATION) == 1
|
||||
assert Requests.status_count(RequestStatus.STARTED) == 1
|
||||
assert Requests.in_progress_count() == 2
|
||||
|
||||
def test_status_count_scoped_to_creator(session):
|
||||
# make sure table is empty
|
||||
session.query(Request).delete()
|
||||
|
||||
user = UserFactory.create()
|
||||
request1 = RequestFactory.create()
|
||||
request2 = RequestFactory.create(creator=user)
|
||||
|
||||
assert Requests.status_count(RequestStatus.STARTED) == 2
|
||||
assert Requests.status_count(RequestStatus.STARTED, creator=user) == 1
|
||||
|
@ -39,6 +39,7 @@ class RequestStatusEventFactory(factory.alchemy.SQLAlchemyModelFactory):
|
||||
model = RequestStatusEvent
|
||||
|
||||
id = factory.Sequence(lambda x: uuid4())
|
||||
sequence = 1
|
||||
|
||||
|
||||
class RequestFactory(factory.alchemy.SQLAlchemyModelFactory):
|
||||
@ -56,6 +57,7 @@ class RequestFactory(factory.alchemy.SQLAlchemyModelFactory):
|
||||
def build_request_body(cls, user, dollar_value=1000000):
|
||||
return {
|
||||
"primary_poc": {
|
||||
"am_poc": False,
|
||||
"dodid_poc": user.dod_id,
|
||||
"email_poc": user.email,
|
||||
"fname_poc": user.first_name,
|
||||
@ -66,7 +68,7 @@ class RequestFactory(factory.alchemy.SQLAlchemyModelFactory):
|
||||
"start_date": "2018-08-08",
|
||||
"cloud_native": "yes",
|
||||
"dollar_value": dollar_value,
|
||||
"dod_component": "us_navy",
|
||||
"dod_component": "Army and Air Force Exchange Service",
|
||||
"data_transfers": "less_than_100gb",
|
||||
"jedi_migration": "yes",
|
||||
"num_software_systems": 1,
|
||||
@ -75,6 +77,7 @@ class RequestFactory(factory.alchemy.SQLAlchemyModelFactory):
|
||||
"engineering_assessment": "yes",
|
||||
"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"
|
||||
@ -86,7 +89,7 @@ class RequestFactory(factory.alchemy.SQLAlchemyModelFactory):
|
||||
"email_request": user.email,
|
||||
"fname_request": user.first_name,
|
||||
"lname_request": user.last_name,
|
||||
"service_branch": "ads",
|
||||
"service_branch": "Air Force, Department of the",
|
||||
"date_latest_training": "2018-08-06"
|
||||
}
|
||||
}
|
||||
|
85
tests/forms/test_request.py
Normal file
85
tests/forms/test_request.py
Normal file
@ -0,0 +1,85 @@
|
||||
import pytest
|
||||
|
||||
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',
|
||||
}
|
||||
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'
|
||||
}
|
||||
|
||||
def test_require_cloud_native_when_not_migrating(self):
|
||||
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'] }
|
||||
|
||||
def test_require_migration_questions_when_migrating(self):
|
||||
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']
|
||||
}
|
||||
|
||||
def test_require_organization_when_technical_support_team(self):
|
||||
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'],
|
||||
}
|
||||
|
||||
def test_valid_form_data(self):
|
||||
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']
|
||||
|
||||
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.'],
|
||||
}
|
||||
|
||||
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']
|
||||
|
||||
request_form = RequestForm(data=data)
|
||||
assert request_form.validate()
|
@ -65,6 +65,6 @@ def test_request_status_pending_expired_displayname():
|
||||
|
||||
def test_request_status_pending_deleted_displayname():
|
||||
request = RequestFactory.create()
|
||||
request = Requests.set_status(request, RequestStatus.DELETED)
|
||||
request = Requests.set_status(request, RequestStatus.CANCELED)
|
||||
|
||||
assert request.status_displayname == "Deleted"
|
||||
assert request.status_displayname == "Canceled"
|
||||
|
@ -47,7 +47,7 @@ class TestPENumberInForm:
|
||||
|
||||
response = self.submit_data(client, self.required_data)
|
||||
|
||||
assert "We couldn\'t find that PE number" in response.data.decode()
|
||||
assert "We couldn't find that PE number" in response.data.decode()
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_submit_request_form_with_unchanged_pe_id(self, monkeypatch, client):
|
||||
@ -81,5 +81,5 @@ class TestPENumberInForm:
|
||||
|
||||
response = self.submit_data(client, data)
|
||||
|
||||
assert "There were some errors, see below" in response.data.decode()
|
||||
assert "There were some errors" in response.data.decode()
|
||||
assert response.status_code == 200
|
||||
|
@ -1,9 +1,8 @@
|
||||
import re
|
||||
import pytest
|
||||
import urllib
|
||||
from tests.mocks import MOCK_USER, MOCK_REQUEST
|
||||
from tests.factories import RequestFactory, UserFactory
|
||||
from atst.domain.roles import Roles
|
||||
from atst.domain.requests import Requests
|
||||
from urllib.parse import urlencode
|
||||
|
||||
|
||||
ERROR_CLASS = "alert--error"
|
||||
@ -75,9 +74,9 @@ def test_creator_info_is_autopopulated(monkeypatch, client, user_session):
|
||||
|
||||
response = client.get("/requests/new/2/{}".format(request.id))
|
||||
body = response.data.decode()
|
||||
assert 'value="{}"'.format(user.first_name) in body
|
||||
assert 'value="{}"'.format(user.last_name) in body
|
||||
assert 'value="{}"'.format(user.email) in body
|
||||
assert "initial-value='{}'".format(user.first_name) in body
|
||||
assert "initial-value='{}'".format(user.last_name) in body
|
||||
assert "initial-value='{}'".format(user.email) in body
|
||||
|
||||
|
||||
def test_creator_info_is_autopopulated_for_new_request(monkeypatch, client, user_session):
|
||||
@ -86,9 +85,9 @@ def test_creator_info_is_autopopulated_for_new_request(monkeypatch, client, user
|
||||
|
||||
response = client.get("/requests/new/2")
|
||||
body = response.data.decode()
|
||||
assert 'value="{}"'.format(user.first_name) in body
|
||||
assert 'value="{}"'.format(user.last_name) in body
|
||||
assert 'value="{}"'.format(user.email) in body
|
||||
assert "initial-value='{}'".format(user.first_name) in body
|
||||
assert "initial-value='{}'".format(user.last_name) in body
|
||||
assert "initial-value='{}'".format(user.email) in body
|
||||
|
||||
|
||||
def test_non_creator_info_is_not_autopopulated(monkeypatch, client, user_session):
|
||||
@ -102,3 +101,72 @@ def test_non_creator_info_is_not_autopopulated(monkeypatch, client, user_session
|
||||
assert not user.first_name in body
|
||||
assert not user.last_name in body
|
||||
assert not user.email in body
|
||||
|
||||
def test_am_poc_causes_poc_to_be_autopopulated(client, user_session):
|
||||
creator = UserFactory.create()
|
||||
user_session(creator)
|
||||
request = RequestFactory.create(creator=creator, body={})
|
||||
client.post(
|
||||
"/requests/new/3/{}".format(request.id),
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||
data="am_poc=yes",
|
||||
)
|
||||
request = Requests.get(request.id)
|
||||
assert request.body["primary_poc"]["dodid_poc"] == creator.dod_id
|
||||
|
||||
|
||||
def test_not_am_poc_requires_poc_info_to_be_completed(client, user_session):
|
||||
creator = UserFactory.create()
|
||||
user_session(creator)
|
||||
request = RequestFactory.create(creator=creator, body={})
|
||||
response = client.post(
|
||||
"/requests/new/3/{}".format(request.id),
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||
data="am_poc=no",
|
||||
follow_redirects=True
|
||||
)
|
||||
assert ERROR_CLASS in response.data.decode()
|
||||
|
||||
|
||||
def test_not_am_poc_allows_user_to_fill_in_poc_info(client, user_session):
|
||||
creator = UserFactory.create()
|
||||
user_session(creator)
|
||||
request = RequestFactory.create(creator=creator, body={})
|
||||
poc_input = {
|
||||
"am_poc": "no",
|
||||
"fname_poc": "test",
|
||||
"lname_poc": "user",
|
||||
"email_poc": "test.user@mail.com",
|
||||
"dodid_poc": "1234567890",
|
||||
}
|
||||
response = client.post(
|
||||
"/requests/new/3/{}".format(request.id),
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||
data=urlencode(poc_input),
|
||||
)
|
||||
assert ERROR_CLASS not in response.data.decode()
|
||||
|
||||
|
||||
def test_poc_details_can_be_autopopulated_on_new_request(client, user_session):
|
||||
creator = UserFactory.create()
|
||||
user_session(creator)
|
||||
response = client.post(
|
||||
"/requests/new/3",
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||
data="am_poc=yes",
|
||||
)
|
||||
request_id = response.headers["Location"].split('/')[-1]
|
||||
request = Requests.get(request_id)
|
||||
|
||||
assert request.body["primary_poc"]["dodid_poc"] == creator.dod_id
|
||||
|
||||
|
||||
def test_can_review_data(user_session, client):
|
||||
creator = UserFactory.create()
|
||||
user_session(creator)
|
||||
request = RequestFactory.create(creator=creator)
|
||||
response = client.get("/requests/new/4/{}".format(request.id))
|
||||
body = response.data.decode()
|
||||
# assert a sampling of the request data is on the review page
|
||||
assert request.body["primary_poc"]["fname_poc"] in body
|
||||
assert request.body["information_about_you"]["email_request"] in body
|
||||
|
@ -21,7 +21,7 @@ def test_submit_reviewed_request(monkeypatch, client, user_session):
|
||||
follow_redirects=False,
|
||||
)
|
||||
assert "/requests" in response.headers["Location"]
|
||||
assert "modal" not in response.headers["Location"]
|
||||
assert "modal=pendingCCPOApproval" in response.headers["Location"]
|
||||
|
||||
|
||||
def test_submit_autoapproved_reviewed_request(monkeypatch, client, user_session):
|
||||
@ -35,4 +35,4 @@ def test_submit_autoapproved_reviewed_request(monkeypatch, client, user_session)
|
||||
data="",
|
||||
follow_redirects=False,
|
||||
)
|
||||
assert "/requests?modal=" in response.headers["Location"]
|
||||
assert "/requests?modal=pendingFinancialVerification" in response.headers["Location"]
|
||||
|
@ -1,46 +1,67 @@
|
||||
import pytest
|
||||
from tests.mocks import MOCK_USER
|
||||
from urllib.parse import urlencode
|
||||
from .factories import UserFactory, RequestFactory
|
||||
|
||||
from atst.routes.requests.jedi_request_flow import JEDIRequestFlow
|
||||
from atst.models.request_status_event import RequestStatus
|
||||
from atst.domain.requests import Requests
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def screens(app):
|
||||
return JEDIRequestFlow(3).screens
|
||||
|
||||
|
||||
@pytest.mark.skip()
|
||||
def test_stepthrough_request_form(monkeypatch, screens, client):
|
||||
monkeypatch.setattr(
|
||||
"atst.handlers.request_new.RequestNew.get_current_user", lambda s: MOCK_USER
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"atst.handlers.request_new.RequestNew.check_xsrf_cookie", lambda s: True
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"atst.handlers.request_new.JEDIRequestFlow.validate", lambda s: True
|
||||
)
|
||||
def test_stepthrough_request_form(user_session, screens, client):
|
||||
user = UserFactory.create()
|
||||
user_session(user)
|
||||
mock_request = RequestFactory.stub()
|
||||
|
||||
def take_a_step(inc, req=None):
|
||||
def post_form(url, redirects=False, data=""):
|
||||
return client.post(
|
||||
url,
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||
data=data,
|
||||
follow_redirects=redirects,
|
||||
)
|
||||
|
||||
def take_a_step(inc, req=None, data=None):
|
||||
req_url = "/requests/new/{}".format(inc)
|
||||
if req:
|
||||
req_url += "/" + req
|
||||
response = client.post(
|
||||
req_url,
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||
data="meaning=42",
|
||||
)
|
||||
return response
|
||||
# we do it twice, with and without redirect, in order to get the
|
||||
# destination url
|
||||
prelim_resp = post_form(req_url, data=data)
|
||||
response = post_form(req_url, True, data=data)
|
||||
return (prelim_resp.headers.get("Location"), response)
|
||||
|
||||
# GET the initial form
|
||||
response = client.get("/requests/new")
|
||||
response = client.get("/requests/new/1")
|
||||
assert screens[0]["title"] in response.data.decode()
|
||||
|
||||
# POST to each of the form pages up until review and submit
|
||||
req_id = None
|
||||
for i in range(1, len(screens)):
|
||||
resp = take_a_step(i, req=req_id)
|
||||
__import__('ipdb').set_trace()
|
||||
req_id = resp.effective_url.split("/")[-1]
|
||||
# get appropriate form data to POST for this section
|
||||
section = screens[i - 1]["section"]
|
||||
post_data = urlencode(mock_request.body[section])
|
||||
|
||||
effective_url, resp = take_a_step(i, req=req_id, data=post_data)
|
||||
req_id = effective_url.split("/")[-1]
|
||||
screen_title = screens[i]["title"].replace("&", "&")
|
||||
|
||||
assert "/requests/new/{}/{}".format(i + 1, req_id) in resp.effective_url
|
||||
assert "/requests/new/{}/{}".format(i + 1, req_id) in effective_url
|
||||
assert screen_title in resp.data.decode()
|
||||
|
||||
# at this point, the real request we made and the mock_request bodies
|
||||
# should be equivalent
|
||||
assert Requests.get(req_id).body == mock_request.body
|
||||
|
||||
# finish the review and submit step
|
||||
client.post(
|
||||
"/requests/submit/{}".format(req_id),
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||
)
|
||||
|
||||
finished_request = Requests.get(req_id)
|
||||
assert finished_request.status == RequestStatus.PENDING_CCPO_APPROVAL
|
||||
|
81
yarn.lock
81
yarn.lock
@ -127,14 +127,10 @@ anymatch@^2.0.0:
|
||||
micromatch "^3.1.4"
|
||||
normalize-path "^2.1.1"
|
||||
|
||||
aproba@^1.0.3, aproba@^1.1.1, aproba@^1.1.2, aproba@~1.2.0:
|
||||
aproba@^1.0.3, aproba@^1.1.1, aproba@^1.1.2, "aproba@^1.1.2 || 2", aproba@~1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
|
||||
|
||||
"aproba@^1.1.2 || 2":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc"
|
||||
|
||||
archy@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40"
|
||||
@ -1355,14 +1351,10 @@ color-convert@^1.3.0, color-convert@^1.9.0, color-convert@^1.9.1:
|
||||
dependencies:
|
||||
color-name "1.1.1"
|
||||
|
||||
color-name@1.1.1:
|
||||
color-name@1.1.1, color-name@^1.0.0:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.1.tgz#4b1415304cf50028ea81643643bd82ea05803689"
|
||||
|
||||
color-name@^1.0.0:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
|
||||
|
||||
color-string@^0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/color-string/-/color-string-0.3.0.tgz#27d46fb67025c5c2fa25993bfbf579e47841b991"
|
||||
@ -2148,18 +2140,7 @@ escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
|
||||
|
||||
escodegen@^1.8.1:
|
||||
version "1.11.0"
|
||||
resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.11.0.tgz#b27a9389481d5bfd5bec76f7bb1eb3f8f4556589"
|
||||
dependencies:
|
||||
esprima "^3.1.3"
|
||||
estraverse "^4.2.0"
|
||||
esutils "^2.0.2"
|
||||
optionator "^0.8.1"
|
||||
optionalDependencies:
|
||||
source-map "~0.6.1"
|
||||
|
||||
escodegen@~1.9.0:
|
||||
escodegen@^1.8.1, escodegen@~1.9.0:
|
||||
version "1.9.1"
|
||||
resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.9.1.tgz#dbae17ef96c8e4bedb1356f4504fa4cc2f7cb7e2"
|
||||
dependencies:
|
||||
@ -2259,14 +2240,10 @@ extglob@^2.0.4:
|
||||
snapdragon "^0.8.1"
|
||||
to-regex "^3.0.1"
|
||||
|
||||
extsprintf@1.3.0:
|
||||
extsprintf@1.3.0, extsprintf@^1.2.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
|
||||
|
||||
extsprintf@^1.2.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
|
||||
|
||||
falafel@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/falafel/-/falafel-2.1.0.tgz#96bb17761daba94f46d001738b3cedf3a67fe06c"
|
||||
@ -3214,11 +3191,7 @@ js-beautify@^1.7.5:
|
||||
mkdirp "~0.5.0"
|
||||
nopt "~3.0.1"
|
||||
|
||||
"js-tokens@^3.0.0 || ^4.0.0":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||
|
||||
js-tokens@^3.0.2:
|
||||
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
|
||||
|
||||
@ -3482,6 +3455,10 @@ lodash.memoize@~3.0.3:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-3.0.4.tgz#2dcbd2c287cbc0a55cc42328bd0c736150d53e3f"
|
||||
|
||||
lodash.merge@^4.6.0:
|
||||
version "4.6.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.1.tgz#adc25d9cb99b9391c59624f379fbba60d7111d54"
|
||||
|
||||
lodash.mergewith@^4.6.0:
|
||||
version "4.6.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927"
|
||||
@ -3802,14 +3779,10 @@ move-concurrently@^1.0.1:
|
||||
rimraf "^2.5.4"
|
||||
run-queue "^1.0.3"
|
||||
|
||||
ms@2.0.0:
|
||||
ms@2.0.0, ms@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
||||
|
||||
ms@^2.0.0:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
|
||||
|
||||
mute-stream@~0.0.4:
|
||||
version "0.0.7"
|
||||
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
|
||||
@ -4556,14 +4529,10 @@ pascalcase@^0.1.1:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
|
||||
|
||||
path-browserify@0.0.0:
|
||||
path-browserify@0.0.0, path-browserify@~0.0.0:
|
||||
version "0.0.0"
|
||||
resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a"
|
||||
|
||||
path-browserify@~0.0.0:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.1.tgz#e6c4ddd7ed3aa27c68a20cc4e50e1a4ee83bbc4a"
|
||||
|
||||
path-dirname@^1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0"
|
||||
@ -4652,6 +4621,10 @@ pinkie@^2.0.0:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870"
|
||||
|
||||
popper.js@^1.12.9:
|
||||
version "1.14.4"
|
||||
resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.14.4.tgz#8eec1d8ff02a5a3a152dd43414a15c7b79fd69b6"
|
||||
|
||||
posix-character-classes@^0.1.0:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
|
||||
@ -6084,11 +6057,7 @@ static-module@^2.2.0:
|
||||
static-eval "^2.0.0"
|
||||
through2 "~2.0.3"
|
||||
|
||||
"statuses@>= 1.4.0 < 2":
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
|
||||
|
||||
statuses@~1.4.0:
|
||||
"statuses@>= 1.4.0 < 2", statuses@~1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087"
|
||||
|
||||
@ -6430,14 +6399,10 @@ trim-right@^1.0.1:
|
||||
dependencies:
|
||||
glob "^6.0.4"
|
||||
|
||||
tty-browserify@0.0.0:
|
||||
tty-browserify@0.0.0, tty-browserify@~0.0.0:
|
||||
version "0.0.0"
|
||||
resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
|
||||
|
||||
tty-browserify@~0.0.0:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.1.tgz#3f05251ee17904dfd0677546670db9651682b811"
|
||||
|
||||
tunnel-agent@^0.6.0:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
|
||||
@ -6640,6 +6605,14 @@ uuid@^3.0.0, uuid@^3.1.0, uuid@^3.3.2:
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
|
||||
|
||||
v-tooltip@^2.0.0-rc.33:
|
||||
version "2.0.0-rc.33"
|
||||
resolved "https://registry.yarnpkg.com/v-tooltip/-/v-tooltip-2.0.0-rc.33.tgz#78f7d8e9c34265622be65ba9dc78c67f1dc02b73"
|
||||
dependencies:
|
||||
lodash.merge "^4.6.0"
|
||||
popper.js "^1.12.9"
|
||||
vue-resize "^0.4.3"
|
||||
|
||||
v8-compile-cache@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.0.tgz#526492e35fc616864284700b7043e01baee09f0a"
|
||||
@ -6679,6 +6652,10 @@ vm-browserify@0.0.4, vm-browserify@~0.0.1:
|
||||
dependencies:
|
||||
indexof "0.0.1"
|
||||
|
||||
vue-resize@^0.4.3:
|
||||
version "0.4.4"
|
||||
resolved "https://registry.yarnpkg.com/vue-resize/-/vue-resize-0.4.4.tgz#dee9b8dd1b189e7e3f6ec47f86c60694a241bb17"
|
||||
|
||||
vue-text-mask@^6.1.2:
|
||||
version "6.1.2"
|
||||
resolved "https://registry.yarnpkg.com/vue-text-mask/-/vue-text-mask-6.1.2.tgz#2cc18a1ca04ea66798518a9373929a12256d14b9"
|
||||
|
Loading…
x
Reference in New Issue
Block a user