Merge branch 'master' into ui-progress-bar
This commit is contained in:
commit
f020115a51
22
atst/app.py
22
atst/app.py
@ -58,19 +58,31 @@ def make_app(config, deps, **kwargs):
|
|||||||
url(
|
url(
|
||||||
r"/requests/new",
|
r"/requests/new",
|
||||||
RequestNew,
|
RequestNew,
|
||||||
{"page": "requests_new", "requests_client": deps["requests_client"]},
|
{
|
||||||
|
"page": "requests_new",
|
||||||
|
"requests_client": deps["requests_client"],
|
||||||
|
"fundz_client": deps["fundz_client"],
|
||||||
|
},
|
||||||
name="request_new",
|
name="request_new",
|
||||||
),
|
),
|
||||||
url(
|
url(
|
||||||
r"/requests/new/([0-9])",
|
r"/requests/new/([0-9])",
|
||||||
RequestNew,
|
RequestNew,
|
||||||
{"page": "requests_new", "requests_client": deps["requests_client"]},
|
{
|
||||||
|
"page": "requests_new",
|
||||||
|
"requests_client": deps["requests_client"],
|
||||||
|
"fundz_client": deps["fundz_client"],
|
||||||
|
},
|
||||||
name="request_form_new",
|
name="request_form_new",
|
||||||
),
|
),
|
||||||
url(
|
url(
|
||||||
r"/requests/new/([0-9])/(\S+)",
|
r"/requests/new/([0-9])/(\S+)",
|
||||||
RequestNew,
|
RequestNew,
|
||||||
{"page": "requests_new", "requests_client": deps["requests_client"]},
|
{
|
||||||
|
"page": "requests_new",
|
||||||
|
"requests_client": deps["requests_client"],
|
||||||
|
"fundz_client": deps["fundz_client"],
|
||||||
|
},
|
||||||
name="request_form_update",
|
name="request_form_update",
|
||||||
),
|
),
|
||||||
url(
|
url(
|
||||||
@ -128,6 +140,10 @@ def make_deps(config):
|
|||||||
api_version="v1",
|
api_version="v1",
|
||||||
validate_cert=validate_cert,
|
validate_cert=validate_cert,
|
||||||
),
|
),
|
||||||
|
"fundz_client": ApiClient(
|
||||||
|
config["default"]["FUNDZ_BASE_URL"],
|
||||||
|
validate_cert=validate_cert,
|
||||||
|
),
|
||||||
"requests_client": ApiClient(
|
"requests_client": ApiClient(
|
||||||
config["default"]["REQUESTS_QUEUE_BASE_URL"],
|
config["default"]["REQUESTS_QUEUE_BASE_URL"],
|
||||||
api_version="v1",
|
api_version="v1",
|
||||||
|
@ -1,12 +1,40 @@
|
|||||||
|
import tornado
|
||||||
|
from tornado.gen import Return
|
||||||
from wtforms.fields.html5 import EmailField
|
from wtforms.fields.html5 import EmailField
|
||||||
from wtforms.fields import StringField, SelectField
|
from wtforms.fields import StringField, SelectField
|
||||||
|
from wtforms.form import Form
|
||||||
from wtforms.validators import Required, Email
|
from wtforms.validators import Required, Email
|
||||||
from wtforms_tornado import Form
|
|
||||||
|
|
||||||
from .fields import NewlineListField
|
from .fields import NewlineListField
|
||||||
|
from .forms import ValidatedForm
|
||||||
|
|
||||||
|
|
||||||
class FinancialForm(Form):
|
@tornado.gen.coroutine
|
||||||
|
def validate_pe_id(field, existing_request, fundz_client):
|
||||||
|
response = yield fundz_client.get(
|
||||||
|
"/pe-number/{}".format(field.data),
|
||||||
|
raise_error=False,
|
||||||
|
)
|
||||||
|
if not response.ok:
|
||||||
|
field.errors.append(
|
||||||
|
"We couldn't find that PE number, but if you have double checked "
|
||||||
|
"it you can submit anyway. Your request will need to go through a "
|
||||||
|
"manual review."
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class FinancialForm(ValidatedForm):
|
||||||
|
|
||||||
|
@tornado.gen.coroutine
|
||||||
|
def perform_extra_validation(self, existing_request, fundz_client):
|
||||||
|
valid = True
|
||||||
|
if not existing_request or existing_request.get('pe_id') != self.pe_id.data:
|
||||||
|
valid = yield validate_pe_id(self.pe_id, existing_request, fundz_client)
|
||||||
|
raise Return(valid)
|
||||||
|
|
||||||
task_order_id = StringField(
|
task_order_id = StringField(
|
||||||
"Task Order Number associated with this request.", validators=[Required()]
|
"Task Order Number associated with this request.", validators=[Required()]
|
||||||
)
|
)
|
||||||
|
12
atst/forms/forms.py
Normal file
12
atst/forms/forms.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import tornado
|
||||||
|
from tornado.gen import Return
|
||||||
|
from wtforms_tornado import Form
|
||||||
|
|
||||||
|
|
||||||
|
class ValidatedForm(Form):
|
||||||
|
|
||||||
|
@tornado.gen.coroutine
|
||||||
|
def perform_extra_validation(self, *args, **kwargs):
|
||||||
|
"""A coroutine that performs any applicable extra validation. Must
|
||||||
|
return True if the form is valid or False otherwise."""
|
||||||
|
raise Return(True)
|
@ -1,13 +1,13 @@
|
|||||||
from wtforms.fields.html5 import EmailField, TelField
|
from wtforms.fields.html5 import EmailField, TelField
|
||||||
from wtforms.fields import RadioField, StringField
|
from wtforms.fields import RadioField, StringField
|
||||||
from wtforms.validators import Required, Email
|
from wtforms.validators import Required, Email
|
||||||
from wtforms_tornado import Form
|
|
||||||
import pendulum
|
import pendulum
|
||||||
from .fields import DateField
|
from .fields import DateField
|
||||||
|
from .forms import ValidatedForm
|
||||||
from .validators import DateRange, PhoneNumber, Alphabet
|
from .validators import DateRange, PhoneNumber, Alphabet
|
||||||
|
|
||||||
|
|
||||||
class OrgForm(Form):
|
class OrgForm(ValidatedForm):
|
||||||
fname_request = StringField("First Name", validators=[Required(), Alphabet()])
|
fname_request = StringField("First Name", validators=[Required(), Alphabet()])
|
||||||
lname_request = StringField("Last Name", validators=[Required(), Alphabet()])
|
lname_request = StringField("Last Name", validators=[Required(), Alphabet()])
|
||||||
|
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
from wtforms.fields import StringField
|
from wtforms.fields import StringField
|
||||||
from wtforms.validators import Required, Email, Length
|
from wtforms.validators import Required, Email, Length
|
||||||
from wtforms_tornado import Form
|
from .forms import ValidatedForm
|
||||||
from .validators import IsNumber, Alphabet
|
from .validators import IsNumber, Alphabet
|
||||||
|
|
||||||
|
|
||||||
class POCForm(Form):
|
class POCForm(ValidatedForm):
|
||||||
fname_poc = StringField("POC First Name", validators=[Required(), Alphabet()])
|
fname_poc = StringField("POC First Name", validators=[Required(), Alphabet()])
|
||||||
lname_poc = StringField("POC Last Name", validators=[Required(), Alphabet()])
|
lname_poc = StringField("POC Last Name", validators=[Required(), Alphabet()])
|
||||||
|
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
from wtforms.fields.html5 import IntegerField
|
from wtforms.fields.html5 import IntegerField
|
||||||
from wtforms.fields import RadioField, StringField, TextAreaField
|
from wtforms.fields import RadioField, StringField, TextAreaField
|
||||||
from wtforms.validators import NumberRange, InputRequired
|
from wtforms.validators import NumberRange, InputRequired
|
||||||
from wtforms_tornado import Form
|
|
||||||
from .fields import DateField
|
from .fields import DateField
|
||||||
|
from .forms import ValidatedForm
|
||||||
from .validators import DateRange
|
from .validators import DateRange
|
||||||
import pendulum
|
import pendulum
|
||||||
|
|
||||||
|
|
||||||
class RequestForm(Form):
|
class RequestForm(ValidatedForm):
|
||||||
|
|
||||||
# Details of Use: Overall Request Details
|
# Details of Use: Overall Request Details
|
||||||
dollar_value = IntegerField(
|
dollar_value = IntegerField(
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
from wtforms.fields import BooleanField
|
from wtforms.fields import BooleanField
|
||||||
from wtforms_tornado import Form
|
|
||||||
|
from .forms import ValidatedForm
|
||||||
|
|
||||||
|
|
||||||
class ReviewForm(Form):
|
class ReviewForm(ValidatedForm):
|
||||||
reviewed = BooleanField("I have reviewed this data and it is correct.")
|
reviewed = BooleanField("I have reviewed this data and it is correct.")
|
||||||
|
@ -15,17 +15,26 @@ class BaseHandler(tornado.web.RequestHandler):
|
|||||||
|
|
||||||
@tornado.gen.coroutine
|
@tornado.gen.coroutine
|
||||||
def login(self, user):
|
def login(self, user):
|
||||||
user["atat_permissions"] = yield self._get_user_permissions(user["id"])
|
user_permissions = yield self._get_user_permissions(user["id"])
|
||||||
|
user["atat_permissions"] = user_permissions["atat_permissions"]
|
||||||
|
user["atat_role"] = user_permissions["atat_role"]
|
||||||
session_id = self.sessions.start_session(user)
|
session_id = self.sessions.start_session(user)
|
||||||
self.set_secure_cookie("atat", session_id)
|
self.set_secure_cookie("atat", session_id)
|
||||||
return self.redirect("/home")
|
return self.redirect("/home")
|
||||||
|
|
||||||
@tornado.gen.coroutine
|
@tornado.gen.coroutine
|
||||||
def _get_user_permissions(self, user_id):
|
def _get_user_permissions(self, user_id):
|
||||||
response = yield self.authz_client.post(
|
response = yield self.authz_client.get(
|
||||||
"/users", json={"id": user_id, "atat_role": "ccpo"}
|
"/users/{}".format(user_id), raise_error=False
|
||||||
)
|
)
|
||||||
return response.json["atat_permissions"]
|
if response.code == 404:
|
||||||
|
response = yield self.authz_client.post(
|
||||||
|
"/users", json={"id": user_id, "atat_role": "developer"}
|
||||||
|
)
|
||||||
|
return response.json
|
||||||
|
|
||||||
|
else:
|
||||||
|
return response.json
|
||||||
|
|
||||||
def get_current_user(self):
|
def get_current_user(self):
|
||||||
cookie = self.get_secure_cookie("atat")
|
cookie = self.get_secure_cookie("atat")
|
||||||
|
@ -2,8 +2,41 @@ import tornado.gen
|
|||||||
|
|
||||||
from atst.handler import BaseHandler
|
from atst.handler import BaseHandler
|
||||||
|
|
||||||
|
_DEV_USERS = {
|
||||||
|
"ccpo": {
|
||||||
|
"id": "164497f6-c1ea-4f42-a5ef-101da278c012",
|
||||||
|
"first_name": "Sam",
|
||||||
|
"last_name": "CCPO",
|
||||||
|
},
|
||||||
|
"owner": {
|
||||||
|
"id": "cce17030-4109-4719-b958-ed109dbb87c8",
|
||||||
|
"first_name": "Olivia",
|
||||||
|
"last_name": "Owner",
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"id": "66ebf7b8-cbf0-4ed8-a102-5f105330df75",
|
||||||
|
"first_name": "Andreas",
|
||||||
|
"last_name": "Admin",
|
||||||
|
},
|
||||||
|
"developer": {
|
||||||
|
"id": "7707b9f2-5945-49ae-967a-be65baa88baf",
|
||||||
|
"first_name": "Dominick",
|
||||||
|
"last_name": "Developer",
|
||||||
|
},
|
||||||
|
"billing_auditor": {
|
||||||
|
"id": "6978ac0c-442a-46aa-a0c3-ff17b5ec2a8c",
|
||||||
|
"first_name": "Billie",
|
||||||
|
"last_name": "The Billing Auditor",
|
||||||
|
},
|
||||||
|
"security_auditor": {
|
||||||
|
"id": "596fd001-bb1d-4adf-87d8-fa2312e882de",
|
||||||
|
"first_name": "Sawyer",
|
||||||
|
"last_name": "The Security Auditor",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
class Dev(BaseHandler):
|
class Dev(BaseHandler):
|
||||||
|
|
||||||
def initialize(self, action, sessions, authz_client):
|
def initialize(self, action, sessions, authz_client):
|
||||||
self.action = action
|
self.action = action
|
||||||
self.sessions = sessions
|
self.sessions = sessions
|
||||||
@ -11,9 +44,14 @@ class Dev(BaseHandler):
|
|||||||
|
|
||||||
@tornado.gen.coroutine
|
@tornado.gen.coroutine
|
||||||
def get(self):
|
def get(self):
|
||||||
user = {
|
role = self.get_argument("role", "ccpo")
|
||||||
"id": "164497f6-c1ea-4f42-a5ef-101da278c012",
|
user = _DEV_USERS[role]
|
||||||
"first_name": "Test",
|
yield self._set_user_permissions(user["id"], role)
|
||||||
"last_name": "User",
|
|
||||||
}
|
|
||||||
yield self.login(user)
|
yield self.login(user)
|
||||||
|
|
||||||
|
@tornado.gen.coroutine
|
||||||
|
def _set_user_permissions(self, user_id, role):
|
||||||
|
response = yield self.authz_client.post(
|
||||||
|
"/users", json={"id": user_id, "atat_role": role}
|
||||||
|
)
|
||||||
|
return response.json
|
||||||
|
@ -10,9 +10,17 @@ from atst.forms.financial import FinancialForm
|
|||||||
|
|
||||||
|
|
||||||
class RequestNew(BaseHandler):
|
class RequestNew(BaseHandler):
|
||||||
def initialize(self, page, requests_client):
|
def initialize(self, page, requests_client, fundz_client):
|
||||||
self.page = page
|
self.page = page
|
||||||
self.requests_client = requests_client
|
self.requests_client = requests_client
|
||||||
|
self.fundz_client = fundz_client
|
||||||
|
|
||||||
|
@tornado.gen.coroutine
|
||||||
|
def get_existing_request(self, request_id):
|
||||||
|
if request_id is None:
|
||||||
|
return {}
|
||||||
|
request = yield self.requests_client.get("/requests/{}".format(request_id))
|
||||||
|
return request.json
|
||||||
|
|
||||||
@tornado.web.authenticated
|
@tornado.web.authenticated
|
||||||
@tornado.gen.coroutine
|
@tornado.gen.coroutine
|
||||||
@ -20,22 +28,19 @@ class RequestNew(BaseHandler):
|
|||||||
self.check_xsrf_cookie()
|
self.check_xsrf_cookie()
|
||||||
screen = int(screen)
|
screen = int(screen)
|
||||||
post_data = self.request.arguments
|
post_data = self.request.arguments
|
||||||
|
current_user = self.get_current_user()
|
||||||
|
existing_request = yield self.get_existing_request(request_id)
|
||||||
jedi_flow = JEDIRequestFlow(
|
jedi_flow = JEDIRequestFlow(
|
||||||
self.requests_client, screen, post_data=post_data, request_id=request_id
|
self.requests_client,
|
||||||
|
self.fundz_client,
|
||||||
|
screen,
|
||||||
|
post_data=post_data,
|
||||||
|
request_id=request_id,
|
||||||
|
current_user=current_user,
|
||||||
|
existing_request=existing_request,
|
||||||
)
|
)
|
||||||
|
|
||||||
if jedi_flow.validate():
|
rerender_args = dict(
|
||||||
response = yield jedi_flow.create_or_update_request(self.get_current_user())
|
|
||||||
if response.ok:
|
|
||||||
where = self.application.default_router.reverse_url(
|
|
||||||
"request_form_update", str(screen + 1), jedi_flow.request_id
|
|
||||||
)
|
|
||||||
self.redirect(where)
|
|
||||||
else:
|
|
||||||
self.set_status(response.code)
|
|
||||||
else:
|
|
||||||
self.render(
|
|
||||||
"requests/screen-%d.html.to" % int(screen),
|
|
||||||
f=jedi_flow.form,
|
f=jedi_flow.form,
|
||||||
data=post_data,
|
data=post_data,
|
||||||
page=self.page,
|
page=self.page,
|
||||||
@ -45,6 +50,31 @@ class RequestNew(BaseHandler):
|
|||||||
request_id=jedi_flow.request_id,
|
request_id=jedi_flow.request_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if jedi_flow.validate():
|
||||||
|
response = yield jedi_flow.create_or_update_request()
|
||||||
|
if response.ok:
|
||||||
|
valid = yield jedi_flow.validate_warnings()
|
||||||
|
if valid:
|
||||||
|
if jedi_flow.next_screen >= len(jedi_flow.screens):
|
||||||
|
where = "/requests"
|
||||||
|
else:
|
||||||
|
where = self.application.default_router.reverse_url(
|
||||||
|
"request_form_update", jedi_flow.next_screen, jedi_flow.request_id
|
||||||
|
)
|
||||||
|
self.redirect(where)
|
||||||
|
else:
|
||||||
|
self.render(
|
||||||
|
"requests/screen-%d.html.to" % int(screen),
|
||||||
|
**rerender_args
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.set_status(response.code)
|
||||||
|
else:
|
||||||
|
self.render(
|
||||||
|
"requests/screen-%d.html.to" % int(screen),
|
||||||
|
**rerender_args
|
||||||
|
)
|
||||||
|
|
||||||
@tornado.web.authenticated
|
@tornado.web.authenticated
|
||||||
@tornado.gen.coroutine
|
@tornado.gen.coroutine
|
||||||
def get(self, screen=1, request_id=None):
|
def get(self, screen=1, request_id=None):
|
||||||
@ -60,7 +90,7 @@ class RequestNew(BaseHandler):
|
|||||||
request = response.json
|
request = response.json
|
||||||
|
|
||||||
jedi_flow = JEDIRequestFlow(
|
jedi_flow = JEDIRequestFlow(
|
||||||
self.requests_client, screen, request, request_id=request_id
|
self.requests_client, self.fundz_client, screen, request, request_id=request_id
|
||||||
)
|
)
|
||||||
|
|
||||||
self.render(
|
self.render(
|
||||||
@ -80,12 +110,16 @@ class JEDIRequestFlow(object):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
requests_client,
|
requests_client,
|
||||||
|
fundz_client,
|
||||||
current_step,
|
current_step,
|
||||||
request=None,
|
request=None,
|
||||||
post_data=None,
|
post_data=None,
|
||||||
request_id=None,
|
request_id=None,
|
||||||
|
current_user=None,
|
||||||
|
existing_request=None,
|
||||||
):
|
):
|
||||||
self.requests_client = requests_client
|
self.requests_client = requests_client
|
||||||
|
self.fundz_client = fundz_client
|
||||||
|
|
||||||
self.current_step = current_step
|
self.current_step = current_step
|
||||||
self.request = request
|
self.request = request
|
||||||
@ -96,6 +130,9 @@ class JEDIRequestFlow(object):
|
|||||||
self.request_id = request_id
|
self.request_id = request_id
|
||||||
self.form = self._form()
|
self.form = self._form()
|
||||||
|
|
||||||
|
self.current_user = current_user
|
||||||
|
self.existing_request = existing_request
|
||||||
|
|
||||||
def _form(self):
|
def _form(self):
|
||||||
if self.is_post:
|
if self.is_post:
|
||||||
return self.form_class()(self.post_data)
|
return self.form_class()(self.post_data)
|
||||||
@ -107,6 +144,14 @@ class JEDIRequestFlow(object):
|
|||||||
def validate(self):
|
def validate(self):
|
||||||
return self.form.validate()
|
return self.form.validate()
|
||||||
|
|
||||||
|
@tornado.gen.coroutine
|
||||||
|
def validate_warnings(self):
|
||||||
|
valid = yield self.form.perform_extra_validation(
|
||||||
|
self.existing_request.get('body', {}).get(self.form_section),
|
||||||
|
self.fundz_client,
|
||||||
|
)
|
||||||
|
return valid
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_screen(self):
|
def current_screen(self):
|
||||||
return self.screens[self.current_step - 1]
|
return self.screens[self.current_step - 1]
|
||||||
@ -185,9 +230,9 @@ class JEDIRequestFlow(object):
|
|||||||
]
|
]
|
||||||
|
|
||||||
@tornado.gen.coroutine
|
@tornado.gen.coroutine
|
||||||
def create_or_update_request(self, user):
|
def create_or_update_request(self):
|
||||||
request_data = {
|
request_data = {
|
||||||
"creator_id": user["id"],
|
"creator_id": self.current_user["id"],
|
||||||
"request": {self.form_section: self.form.data},
|
"request": {self.form_section: self.form.data},
|
||||||
}
|
}
|
||||||
if self.request_id:
|
if self.request_id:
|
||||||
|
@ -5,3 +5,12 @@ class Icon(UIModule):
|
|||||||
with open('static/icons/%s.svg' % name) as svg:
|
with open('static/icons/%s.svg' % name) as svg:
|
||||||
return self.render_string(
|
return self.render_string(
|
||||||
"components/icon.html.to", svg=svg.read(), name=name, classes=classes)
|
"components/icon.html.to", svg=svg.read(), name=name, classes=classes)
|
||||||
|
|
||||||
|
class SidenavItem(UIModule):
|
||||||
|
def render(self, label, href, active=False, icon=None):
|
||||||
|
return self.render_string(
|
||||||
|
"navigation/_sidenav_item.html.to",
|
||||||
|
label=label,
|
||||||
|
href=href,
|
||||||
|
active=active,
|
||||||
|
icon=icon)
|
||||||
|
@ -4,6 +4,7 @@ ENVIRONMENT = dev
|
|||||||
DEBUG = true
|
DEBUG = true
|
||||||
AUTHZ_BASE_URL = http://localhost:8002
|
AUTHZ_BASE_URL = http://localhost:8002
|
||||||
AUTHNID_BASE_URL= https://localhost:8001
|
AUTHNID_BASE_URL= https://localhost:8001
|
||||||
|
FUNDZ_BASE_URL= http://localhost:8004
|
||||||
COOKIE_SECRET = some-secret-please-replace
|
COOKIE_SECRET = some-secret-please-replace
|
||||||
SECRET = change_me_into_something_secret
|
SECRET = change_me_into_something_secret
|
||||||
CAC_URL = https://localhost:8001
|
CAC_URL = https://localhost:8001
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
@import 'core/variables';
|
@import 'core/variables';
|
||||||
@import '../node_modules/uswds/src/stylesheets/uswds';
|
@import '../node_modules/uswds/src/stylesheets/uswds';
|
||||||
|
|
||||||
@import 'core/base';
|
|
||||||
@import 'core/grid';
|
@import 'core/grid';
|
||||||
@import 'core/util';
|
@import 'core/util';
|
||||||
|
|
||||||
@ -12,7 +11,11 @@
|
|||||||
@import 'elements/block_lists';
|
@import 'elements/block_lists';
|
||||||
@import 'elements/tables';
|
@import 'elements/tables';
|
||||||
@import 'elements/icons';
|
@import 'elements/icons';
|
||||||
|
@import 'elements/sidenav';
|
||||||
|
|
||||||
|
@import 'components/layout';
|
||||||
|
@import 'components/topbar';
|
||||||
|
@import 'components/global_navigation';
|
||||||
@import 'components/site_action';
|
@import 'components/site_action';
|
||||||
@import 'components/empty_state';
|
@import 'components/empty_state';
|
||||||
|
|
||||||
|
30
scss/components/_global_navigation.scss
Normal file
30
scss/components/_global_navigation.scss
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
.global-navigation {
|
||||||
|
background-color: $color-white;
|
||||||
|
|
||||||
|
.sidenav__link {
|
||||||
|
padding-right: $gap;
|
||||||
|
|
||||||
|
@include media($medium-screen) {
|
||||||
|
padding-right: $gap * 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidenav__link-label {
|
||||||
|
@include hide;
|
||||||
|
|
||||||
|
@include media($medium-screen) {
|
||||||
|
@include unhide;
|
||||||
|
padding-left: $gap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.global-navigation__context--workspace {
|
||||||
|
.sidenav__link {
|
||||||
|
padding-right: $gap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidenav__link-label {
|
||||||
|
@include hide;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
31
scss/components/_layout.scss
Normal file
31
scss/components/_layout.scss
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
body {
|
||||||
|
background-color: $color-gray-lightest;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
min-height: 100vh;
|
||||||
|
|
||||||
|
> footer {
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-layout {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
.global-navigation {
|
||||||
|
margin-top: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-panel-container {
|
||||||
|
margin: $gap;
|
||||||
|
max-width: $site-max-width;
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
@include media($medium-screen) {
|
||||||
|
margin: $gap * 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
71
scss/components/_topbar.scss
Normal file
71
scss/components/_topbar.scss
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
.topbar {
|
||||||
|
background-color: $color-white;
|
||||||
|
border-bottom: 1px solid $color-black;
|
||||||
|
|
||||||
|
.topbar__navigation {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: stretch;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.topbar__link {
|
||||||
|
color: $color-black;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
height: $topbar-height;
|
||||||
|
padding: 0 ($gap * 2);
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
.topbar__link-label {
|
||||||
|
@include h5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar__link-icon {
|
||||||
|
margin-left: $gap;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.topbar__link--shield {
|
||||||
|
width: $icon-bar-width;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
.topbar__link-icon {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $color-primary-darker;
|
||||||
|
color: $color-white;
|
||||||
|
|
||||||
|
.topbar__link-icon {
|
||||||
|
@include icon-style-inverted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar__context {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: stretch;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
&.topbar__context--workspace {
|
||||||
|
background-color: $color-primary;
|
||||||
|
|
||||||
|
.topbar__link {
|
||||||
|
color: $color-white;
|
||||||
|
|
||||||
|
.topbar__link-icon {
|
||||||
|
@include icon-style-inverted;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $color-primary-darker;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,16 +0,0 @@
|
|||||||
/*
|
|
||||||
* Base Styles
|
|
||||||
* @source https://github.com/uswds/uswds/blob/develop/src/stylesheets/core/_base.scss
|
|
||||||
*/
|
|
||||||
|
|
||||||
body {
|
|
||||||
background-color: $color-gray-lightest;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-start;
|
|
||||||
min-height: 100vh;
|
|
||||||
|
|
||||||
> footer {
|
|
||||||
margin-top: auto;
|
|
||||||
}
|
|
||||||
}
|
|
@ -9,12 +9,9 @@
|
|||||||
// We are implementing a simple flexbox row/column system
|
// We are implementing a simple flexbox row/column system
|
||||||
|
|
||||||
@mixin grid-row {
|
@mixin grid-row {
|
||||||
@include media($medium-screen) {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
flex-wrap: wrap;
|
flex-wrap: nowrap;
|
||||||
max-width: $site-max-width;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin grid-pad {
|
@mixin grid-pad {
|
||||||
|
@ -1,3 +1,25 @@
|
|||||||
.nowrap {
|
.nowrap {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mixin hide {
|
||||||
|
border: 0;
|
||||||
|
clip: rect(0 0 0 0);
|
||||||
|
height: 1px;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0;
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin unhide {
|
||||||
|
border: unset;
|
||||||
|
clip: unset;
|
||||||
|
height: unset;
|
||||||
|
margin: unset;
|
||||||
|
overflow: unset;
|
||||||
|
padding: unset;
|
||||||
|
position: unset;
|
||||||
|
width: unset;
|
||||||
|
}
|
||||||
|
@ -3,9 +3,11 @@
|
|||||||
* ===================================================
|
* ===================================================
|
||||||
*/
|
*/
|
||||||
|
|
||||||
$gap: .8rem; // 8px at 10px $em-base
|
$gap: 0.8rem; // 8px at 10px $em-base
|
||||||
$topbar-height: 4.8rem;
|
$topbar-height: 4.8rem;
|
||||||
|
$icon-bar-width: 4.0rem;
|
||||||
$icon-size-small: 2.4rem;
|
$icon-size-small: 2.4rem;
|
||||||
|
$hover-transition-time: 0.2s;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* USWDS Variables
|
* USWDS Variables
|
||||||
|
@ -1,17 +1,19 @@
|
|||||||
@mixin icon {
|
@mixin icon {
|
||||||
display: inline-block;
|
display: inline-flex;
|
||||||
vertical-align: bottom;
|
|
||||||
|
|
||||||
> svg {
|
> svg {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
* {
|
||||||
|
transition: fill $hover-transition-time;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin icon-size($size) {
|
@mixin icon-size($size) {
|
||||||
$icon-size: $size * .1rem;
|
$icon-size: $size * .1rem;
|
||||||
width: $icon-size;
|
width: $icon-size;
|
||||||
height: $icon-size;
|
height: auto;
|
||||||
margin: $icon-size / 4;
|
margin: $icon-size / 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -21,8 +23,24 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mixin icon-style-active {
|
||||||
|
> svg * {
|
||||||
|
fill: $color-primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin icon-style-inverted {
|
||||||
|
> svg * {
|
||||||
|
fill: $color-white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
@include icon;
|
@include icon;
|
||||||
@include icon-size(16);
|
@include icon-size(16);
|
||||||
@include icon-style-default;
|
@include icon-style-default;
|
||||||
|
|
||||||
|
&.icon--tiny {
|
||||||
|
@include icon-size(10);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -44,36 +44,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* Panel Container
|
|
||||||
* Grid container for panel blocks
|
|
||||||
*/
|
|
||||||
|
|
||||||
.panel-container {
|
|
||||||
@include grid-row;
|
|
||||||
@include grid-pad;
|
|
||||||
@include margin(($site-margins-mobile * 2) null);
|
|
||||||
|
|
||||||
@include media($medium-screen) {
|
|
||||||
@include margin(($site-margins * 2) null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@include media($large-screen) {
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .col {
|
|
||||||
@include grid-pad;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
color: $color-gray;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
@ -1,15 +1,13 @@
|
|||||||
.sidenav {
|
.sidenav {
|
||||||
@include grid-pad;
|
ul {
|
||||||
@include panel-margin;
|
list-style: none;
|
||||||
width: 100%;
|
margin: 0;
|
||||||
flex-shrink: 0;
|
padding: 0;
|
||||||
|
|
||||||
@include media($large-screen) {
|
li {
|
||||||
width: 21rem;
|
margin: 0;
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
@include media($xlarge-screen) {
|
|
||||||
width: 30rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidenav__link {
|
.sidenav__link {
|
||||||
@ -18,10 +16,10 @@
|
|||||||
padding: $gap ($gap * 2);
|
padding: $gap ($gap * 2);
|
||||||
color: $color-black;
|
color: $color-black;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
&:hover {
|
.sidenav__link-icon {
|
||||||
background-color: $color-white;
|
margin-left: - ($gap * .5);
|
||||||
color: $color-primary;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.sidenav__link--disabled {
|
&.sidenav__link--disabled {
|
||||||
@ -35,60 +33,42 @@
|
|||||||
color: $color-primary;
|
color: $color-primary;
|
||||||
box-shadow: inset .4rem 0 0 0 $color-primary;
|
box-shadow: inset .4rem 0 0 0 $color-primary;
|
||||||
|
|
||||||
|
.sidenav__link-icon {
|
||||||
|
@include icon-style-active;
|
||||||
|
}
|
||||||
|
|
||||||
+ ul {
|
+ ul {
|
||||||
background-color: $color-white;
|
background-color: $color-white;
|
||||||
|
|
||||||
.sidenav__link {
|
.sidenav__link {
|
||||||
&--active {
|
&--active {
|
||||||
@include h5;
|
@include h5;
|
||||||
box-shadow: none;
|
color: $color-primary;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
+ ul {
|
+ ul {
|
||||||
|
padding-bottom: $gap / 2;
|
||||||
|
|
||||||
li {
|
li {
|
||||||
.sidenav__link {
|
.sidenav__link {
|
||||||
|
@include h5;
|
||||||
padding: ($gap * .75) ($gap * 3);
|
padding: ($gap * .75) ($gap * 3);
|
||||||
border: 0;
|
border: 0;
|
||||||
@include h5;
|
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
.sidenav__link {
|
|
||||||
padding-bottom: $gap * 1.5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
li {
|
|
||||||
margin: 0;
|
|
||||||
display: block;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> ul {
|
&:hover {
|
||||||
@include panel-margin;
|
color: $color-primary;
|
||||||
|
|
||||||
&:last-child {
|
.sidenav__link-icon {
|
||||||
margin: 0;
|
@include icon-style-active;
|
||||||
}
|
}
|
||||||
|
|
||||||
> li {
|
|
||||||
&:last-child {
|
|
||||||
> .sidenav__link {
|
|
||||||
border-bottom: 1px solid $color-black;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -4,6 +4,11 @@
|
|||||||
* @source https://github.com/uswds/uswds/blob/develop/src/stylesheets/elements/_typography.scss
|
* @source https://github.com/uswds/uswds/blob/develop/src/stylesheets/elements/_typography.scss
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
* {
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
h1, h2, h3, h4, h5, h6 {
|
h1, h2, h3, h4, h5, h6 {
|
||||||
font-family: $font-sans;
|
font-family: $font-sans;
|
||||||
|
|
||||||
@ -24,12 +29,12 @@ h2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
a,
|
a,
|
||||||
a > span {
|
a:hover {
|
||||||
transition:
|
transition:
|
||||||
background 0.2s,
|
background 0.2s,
|
||||||
border 0.2s,
|
border 0.2s,
|
||||||
box-shadow 0.2s,
|
box-shadow 0.2s,
|
||||||
color 0.2s,
|
color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:visited {
|
a:visited {
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
section {
|
|
||||||
margin-bottom: 10rem;
|
|
||||||
}
|
|
@ -1,54 +0,0 @@
|
|||||||
.topbar {
|
|
||||||
background-color: $color-white;
|
|
||||||
height: $topbar-height;
|
|
||||||
border-bottom: 1px solid $color-black;
|
|
||||||
|
|
||||||
.topbar__navigation {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: stretch;
|
|
||||||
justify-content: flex-end;
|
|
||||||
|
|
||||||
> .topbar__link {
|
|
||||||
@include h5;
|
|
||||||
color: $color-primary;
|
|
||||||
display: inline-block;
|
|
||||||
height: $topbar-height;
|
|
||||||
line-height: $topbar-height;
|
|
||||||
padding: 0 ($gap * 2);
|
|
||||||
text-decoration: none;
|
|
||||||
|
|
||||||
> span {
|
|
||||||
display: inline-block;
|
|
||||||
height: $topbar-height;
|
|
||||||
line-height: $topbar-height;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.topbar__link--primary {
|
|
||||||
margin-right: auto;
|
|
||||||
> span {
|
|
||||||
@include nav-border;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: $color-white;
|
|
||||||
background-color: $color-primary-darkest;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.topbar__link--secondary {
|
|
||||||
font-weight: normal;
|
|
||||||
> span {
|
|
||||||
@include nav-border;
|
|
||||||
border-bottom-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
> span {
|
|
||||||
@include nav-border;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,3 +1,7 @@
|
|||||||
|
{# TODO: set this context elsewhere #}
|
||||||
|
{# set context='workspace' #}
|
||||||
|
{% set context='global' %}
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
@ -10,18 +14,19 @@
|
|||||||
<link rel="icon" type="image/x-icon" href="/static/img/favicon.ico">
|
<link rel="icon" type="image/x-icon" href="/static/img/favicon.ico">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
{% include 'navigation/topbar.html.to' %}
|
||||||
|
|
||||||
{% include 'header.html.to' %}
|
<div class='global-layout'>
|
||||||
|
{% include 'navigation/global_navigation.html.to' %}
|
||||||
|
|
||||||
<div class='panel-container'>
|
<div class='global-panel-container'>
|
||||||
{% block sidenav %}{% end %}
|
{% block sidenav %}{% end %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
these are not the droids you are looking for
|
these are not the droids you are looking for
|
||||||
{% end %}
|
{% end %}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{% include 'footer.html.to' %}
|
{% include 'footer.html.to' %}
|
||||||
</body>
|
</body>
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href="/" class="topbar__link topbar__link--secondary">
|
<a href="/" class="topbar__link topbar__link--secondary">
|
||||||
<span>Tech Lead</span>
|
<span>{{ current_user.get("atat_role") }}</span>
|
||||||
</a>
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
@ -1,9 +1,5 @@
|
|||||||
{% extends "base.html.to" %}
|
{% extends "base.html.to" %}
|
||||||
|
|
||||||
{% block sidenav %}
|
|
||||||
{% include 'nav-side.html.to' %}
|
|
||||||
{% end %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<main class="usa-section usa-content">
|
<main class="usa-section usa-content">
|
||||||
|
9
templates/navigation/_sidenav_item.html.to
Normal file
9
templates/navigation/_sidenav_item.html.to
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<li>
|
||||||
|
<a class="sidenav__link {% if active %}sidenav__link--active{% end %}" href="{{href}}" title="{{label}}">
|
||||||
|
{% if icon %}
|
||||||
|
{% module Icon(icon, classes="sidenav__link-icon") %}
|
||||||
|
{% end %}
|
||||||
|
|
||||||
|
<span class="sidenav__link-label">{{label}}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
6
templates/navigation/global_navigation.html.to
Normal file
6
templates/navigation/global_navigation.html.to
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<div class="global-navigation sidenav global-navigation__context--{{context}}">
|
||||||
|
<ul>
|
||||||
|
{% module SidenavItem("Requests", href="/requests", icon="document", active=matchesPath('/requests')) %}
|
||||||
|
{% module SidenavItem("Workspaces", href="/workspaces", icon="cloud", active=matchesPath('/workspaces')) %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
19
templates/navigation/topbar.html.to
Normal file
19
templates/navigation/topbar.html.to
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<header class="topbar">
|
||||||
|
<nav class="topbar__navigation">
|
||||||
|
<a href="/home" class="topbar__link topbar__link--shield" title="JEDI Home">
|
||||||
|
{% module Icon('shield', classes='topbar__link-icon') %}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="topbar__context topbar__context--{{context}}">
|
||||||
|
<a href="/" class="topbar__link">
|
||||||
|
<span class="topbar__link-label">JEDI</span>
|
||||||
|
{% module Icon('caret_down', classes='topbar__link-icon icon--tiny') %}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="/" class="topbar__link">
|
||||||
|
<span class="topbar__link-label">Sam Seeceepio</span>
|
||||||
|
{% module Icon('avatar', classes='topbar__link-icon') %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</header>
|
@ -1,9 +1,5 @@
|
|||||||
{% extends "base.html.to" %}
|
{% extends "base.html.to" %}
|
||||||
|
|
||||||
{% block sidenav %}
|
|
||||||
{% include 'nav-side.html.to' %}
|
|
||||||
{% end %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,12 +1,8 @@
|
|||||||
{% extends "base.html.to" %}
|
{% extends "base.html.to" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<div class='col'>
|
||||||
<main class="usa-section usa-content usa-width-one-whole">
|
<table>
|
||||||
|
|
||||||
<h1>Workspaces</h1>
|
|
||||||
|
|
||||||
<table class="usa-table-borderless" width="100%">
|
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" width="50%">Workspace Name</th>
|
<th scope="col" width="50%">Workspace Name</th>
|
||||||
@ -29,8 +25,6 @@
|
|||||||
{% end %}
|
{% end %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
</main>
|
|
||||||
|
|
||||||
{% end %}
|
{% end %}
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from atst.app import make_app, make_deps, make_config
|
from atst.app import make_app, make_deps, make_config
|
||||||
from tests.mocks import MockApiClient, MockRequestsClient, MockAuthzClient
|
from tests.mocks import MockApiClient, MockFundzClient, MockRequestsClient, MockAuthzClient
|
||||||
from atst.sessions import DictSessions
|
from atst.sessions import DictSessions
|
||||||
|
|
||||||
|
|
||||||
@ -11,6 +11,7 @@ def app():
|
|||||||
"authz_client": MockAuthzClient("authz"),
|
"authz_client": MockAuthzClient("authz"),
|
||||||
"requests_client": MockRequestsClient("requests"),
|
"requests_client": MockRequestsClient("requests"),
|
||||||
"authnid_client": MockApiClient("authnid"),
|
"authnid_client": MockApiClient("authnid"),
|
||||||
|
"fundz_client": MockFundzClient("fundz"),
|
||||||
"sessions": DictSessions(),
|
"sessions": DictSessions(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
import re
|
import re
|
||||||
import pytest
|
import pytest
|
||||||
|
import tornado
|
||||||
|
import urllib
|
||||||
|
from tests.mocks import MOCK_REQUEST, MOCK_VALID_PE_ID
|
||||||
|
|
||||||
ERROR_CLASS = "usa-input-error-message"
|
ERROR_CLASS = "usa-input-error-message"
|
||||||
MOCK_USER = {
|
MOCK_USER = {
|
||||||
@ -47,3 +50,82 @@ def test_submit_valid_request_form(monkeypatch, http_client, base_url):
|
|||||||
body="meaning=42",
|
body="meaning=42",
|
||||||
)
|
)
|
||||||
assert "/requests/new/2" in response.effective_url
|
assert "/requests/new/2" in response.effective_url
|
||||||
|
|
||||||
|
|
||||||
|
class TestPENumberInForm:
|
||||||
|
|
||||||
|
required_data = {
|
||||||
|
"pe_id": "123",
|
||||||
|
"task_order_id": "1234567899C0001",
|
||||||
|
"fname_co": "Contracting",
|
||||||
|
"lname_co": "Officer",
|
||||||
|
"email_co": "jane@mail.mil",
|
||||||
|
"office_co": "WHS",
|
||||||
|
"fname_cor": "Officer",
|
||||||
|
"lname_cor": "Representative",
|
||||||
|
"email_cor": "jane@mail.mil",
|
||||||
|
"office_cor": "WHS",
|
||||||
|
"funding_type": "RDTE",
|
||||||
|
"funding_type_other": "other",
|
||||||
|
"clin_0001": "50,000",
|
||||||
|
"clin_0003": "13,000",
|
||||||
|
"clin_1001": "30,000",
|
||||||
|
"clin_1003": "7,000",
|
||||||
|
"clin_2001": "30,000",
|
||||||
|
"clin_2003": "7,000",
|
||||||
|
}
|
||||||
|
|
||||||
|
def _set_monkeypatches(self, monkeypatch):
|
||||||
|
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.forms.request.RequestForm.validate", lambda s: True)
|
||||||
|
|
||||||
|
@tornado.gen.coroutine
|
||||||
|
def submit_data(self, http_client, base_url, data):
|
||||||
|
response = yield http_client.fetch(
|
||||||
|
base_url + "/requests/new/5/{}".format(MOCK_REQUEST["id"]),
|
||||||
|
method="POST",
|
||||||
|
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||||
|
body=urllib.parse.urlencode(data),
|
||||||
|
follow_redirects=False,
|
||||||
|
raise_error=False,
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
|
||||||
|
@pytest.mark.gen_test
|
||||||
|
def test_submit_request_form_with_invalid_pe_id(self, monkeypatch, http_client, base_url):
|
||||||
|
self._set_monkeypatches(monkeypatch)
|
||||||
|
|
||||||
|
response = yield self.submit_data(http_client, base_url, self.required_data)
|
||||||
|
|
||||||
|
assert "We couldn\'t find that PE number" in response.body.decode()
|
||||||
|
assert response.code == 200
|
||||||
|
assert "/requests/new/5" in response.effective_url
|
||||||
|
|
||||||
|
@pytest.mark.gen_test
|
||||||
|
def test_submit_request_form_with_unchanged_pe_id(self, monkeypatch, http_client, base_url):
|
||||||
|
self._set_monkeypatches(monkeypatch)
|
||||||
|
|
||||||
|
data = dict(self.required_data)
|
||||||
|
data['pe_id'] = MOCK_REQUEST['body']['financial_verification']['pe_id']
|
||||||
|
|
||||||
|
response = yield self.submit_data(http_client, base_url, data)
|
||||||
|
|
||||||
|
assert response.code == 302
|
||||||
|
assert response.headers.get("Location") == "/requests"
|
||||||
|
|
||||||
|
@pytest.mark.gen_test
|
||||||
|
def test_submit_request_form_with_new_valid_pe_id(self, monkeypatch, http_client, base_url):
|
||||||
|
self._set_monkeypatches(monkeypatch)
|
||||||
|
|
||||||
|
data = dict(self.required_data)
|
||||||
|
data['pe_id'] = MOCK_VALID_PE_ID
|
||||||
|
|
||||||
|
response = yield self.submit_data(http_client, base_url, data)
|
||||||
|
|
||||||
|
assert response.code == 302
|
||||||
|
assert response.headers.get("Location") == "/requests"
|
||||||
|
@ -5,6 +5,7 @@ from atst.api_client import ApiClient
|
|||||||
|
|
||||||
|
|
||||||
class MockApiClient(ApiClient):
|
class MockApiClient(ApiClient):
|
||||||
|
|
||||||
def __init__(self, service):
|
def __init__(self, service):
|
||||||
self.service = service
|
self.service = service
|
||||||
|
|
||||||
@ -42,31 +43,43 @@ class MockApiClient(ApiClient):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
class MockRequestsClient(MockApiClient):
|
MOCK_REQUEST = {
|
||||||
@tornado.gen.coroutine
|
|
||||||
def get(self, path, **kwargs):
|
|
||||||
json = {
|
|
||||||
"id": "66b8ef71-86d3-48ef-abc2-51bfa1732b6b",
|
"id": "66b8ef71-86d3-48ef-abc2-51bfa1732b6b",
|
||||||
"creator": "49903ae7-da4a-49bf-a6dc-9dff5d004238",
|
"creator": "49903ae7-da4a-49bf-a6dc-9dff5d004238",
|
||||||
"body": {},
|
"body": {
|
||||||
"status": "incomplete",
|
"financial_verification": {
|
||||||
|
"pe_id": "0203752A",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"status": "incomplete"
|
||||||
}
|
}
|
||||||
return self._get_response("GET", path, 200, json=json)
|
|
||||||
|
class MockRequestsClient(MockApiClient):
|
||||||
|
|
||||||
|
@tornado.gen.coroutine
|
||||||
|
def get(self, path, **kwargs):
|
||||||
|
return self._get_response("GET", path, 200, json=MOCK_REQUEST)
|
||||||
|
|
||||||
@tornado.gen.coroutine
|
@tornado.gen.coroutine
|
||||||
def post(self, path, **kwargs):
|
def post(self, path, **kwargs):
|
||||||
json = {
|
return self._get_response("POST", path, 202, json=MOCK_REQUEST)
|
||||||
"id": "66b8ef71-86d3-48ef-abc2-51bfa1732b6b",
|
|
||||||
"creator": "49903ae7-da4a-49bf-a6dc-9dff5d004238",
|
|
||||||
"body": {},
|
MOCK_VALID_PE_ID = "8675309U"
|
||||||
}
|
|
||||||
return self._get_response("POST", path, 202, json=json)
|
|
||||||
|
class MockFundzClient(MockApiClient):
|
||||||
|
|
||||||
|
@tornado.gen.coroutine
|
||||||
|
def get(self, path, **kwargs):
|
||||||
|
if path.endswith(MOCK_VALID_PE_ID):
|
||||||
|
return self._get_response("GET", path, 200)
|
||||||
|
else:
|
||||||
|
return self._get_response("GET", path, 404)
|
||||||
|
|
||||||
|
|
||||||
class MockAuthzClient(MockApiClient):
|
class MockAuthzClient(MockApiClient):
|
||||||
@tornado.gen.coroutine
|
_json = {
|
||||||
def post(self, path, **kwargs):
|
|
||||||
json = {
|
|
||||||
"atat_permissions": [
|
"atat_permissions": [
|
||||||
"view_original_jedi_request",
|
"view_original_jedi_request",
|
||||||
"review_and_approve_jedi_workspace_request",
|
"review_and_approve_jedi_workspace_request",
|
||||||
@ -104,4 +117,11 @@ class MockAuthzClient(MockApiClient):
|
|||||||
"username": None,
|
"username": None,
|
||||||
"workspace_roles": [],
|
"workspace_roles": [],
|
||||||
}
|
}
|
||||||
return self._get_response("POST", path, 200, json=json)
|
|
||||||
|
@tornado.gen.coroutine
|
||||||
|
def post(self, path, **kwargs):
|
||||||
|
return self._get_response("POST", path, 200, json=self._json)
|
||||||
|
|
||||||
|
@tornado.gen.coroutine
|
||||||
|
def get(self, path, **kwargs):
|
||||||
|
return self._get_response("POST", path, 200, json=self._json)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user