Merge branch 'master' into continuous-deployment

This commit is contained in:
Patrick Smith 2018-08-16 10:48:40 -04:00
commit 23e5c04597
65 changed files with 1802 additions and 661 deletions

View File

@ -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"

View File

@ -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
View 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))

View File

@ -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)

View File

@ -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

View File

@ -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 = (

View File

@ -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

View File

@ -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(

View File

@ -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()]

View File

@ -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?"
)

View File

@ -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)

View File

@ -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):

View File

@ -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,
)

View File

@ -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}

View File

@ -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

View 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
})
}
}
}

View 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
}
},
}
}

View 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
}
},
}
}

View 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
})
}
}
}

View File

@ -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))
}
}
}

View File

@ -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: ['!{', '}']
})

View File

@ -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'
}
}

View File

@ -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"
},

View File

@ -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

View File

@ -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"

View File

@ -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';

View 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;
}
}
}
}

View File

@ -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;
}
}
}
}

View File

@ -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;

View 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;
}
}

View File

@ -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
View 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;
}
}

View File

@ -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;
}

View File

@ -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 {

View 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);
}
}

View File

@ -52,9 +52,13 @@ dl {
}
dd {
-webkit-margin-start: 0;
.label {
margin-left: 0;
}
}
> div {
margin-bottom: $gap * 2;
}
}
}

View File

@ -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 %}

View 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 %}

View File

@ -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>

View File

@ -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 %}

View File

@ -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 %}

View 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 %}

View File

@ -0,0 +1,11 @@
<p>
We will review and respond to your request in 72 hours. Youll 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>

View File

@ -0,0 +1,35 @@
<h1>
Request submitted. Approval pending.
</h1>
<p>
We will review and respond to your request in 72 hours. Youll 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>

View File

@ -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')
}
]
) }}

View File

@ -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>

View File

@ -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>&darr; 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 %}

View File

@ -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' %}

View File

@ -17,32 +17,68 @@
) }}
{% endif %}
<details-of-use inline-template v-bind:initial-data='{{ f.data|tojson }}'>
<div>
<p>Wed 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>Wed 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 %}

View File

@ -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 %}

View File

@ -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 providers 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 %}

View File

@ -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 &amp; 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 "" }} />

View File

@ -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>

View File

@ -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

View File

@ -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
View 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)

View File

@ -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

View File

@ -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"
}
}

View 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()

View File

@ -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"

View File

@ -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&#39;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

View File

@ -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

View File

@ -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"]

View File

@ -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("&", "&amp;")
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

View File

@ -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"