From f573181154f27599a31fb9bbd8fefd98ca259ffd Mon Sep 17 00:00:00 2001 From: dandds Date: Tue, 17 Jul 2018 13:56:36 -0400 Subject: [PATCH 01/29] login-dev endpoint accepts role query arg --- atst/handlers/dev.py | 21 ++++++++++++++++----- templates/header.html.to | 2 +- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/atst/handlers/dev.py b/atst/handlers/dev.py index 38c71419..bfb5684e 100644 --- a/atst/handlers/dev.py +++ b/atst/handlers/dev.py @@ -2,8 +2,22 @@ import tornado.gen 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", + }, +} + class Dev(BaseHandler): + def initialize(self, action, sessions, authz_client): self.action = action self.sessions = sessions @@ -11,9 +25,6 @@ class Dev(BaseHandler): @tornado.gen.coroutine def get(self): - user = { - "id": "164497f6-c1ea-4f42-a5ef-101da278c012", - "first_name": "Test", - "last_name": "User", - } + role = self.get_argument("role", "ccpo") + user = _DEV_USERS[role] yield self.login(user) diff --git a/templates/header.html.to b/templates/header.html.to index 48ec82e8..f14f4337 100644 --- a/templates/header.html.to +++ b/templates/header.html.to @@ -5,7 +5,7 @@ - Tech Lead + {{ current_user["atat_role"] }} From 45588e0ec752acaf9a68297ec74a7f55204b548c Mon Sep 17 00:00:00 2001 From: dandds Date: Tue, 17 Jul 2018 14:34:24 -0400 Subject: [PATCH 02/29] account for null current_user in testing --- templates/header.html.to | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/header.html.to b/templates/header.html.to index f14f4337..35feff12 100644 --- a/templates/header.html.to +++ b/templates/header.html.to @@ -5,7 +5,7 @@ - {{ current_user["atat_role"] }} + {{ current_user.get("atat_role") }} From ac150d1af398d609201ca6a9c43c3e1a93544256 Mon Sep 17 00:00:00 2001 From: dandds Date: Wed, 18 Jul 2018 09:51:20 -0400 Subject: [PATCH 03/29] add atat_role to user session data --- atst/handler.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/atst/handler.py b/atst/handler.py index 5543a43a..485873c0 100644 --- a/atst/handler.py +++ b/atst/handler.py @@ -15,7 +15,9 @@ class BaseHandler(tornado.web.RequestHandler): @tornado.gen.coroutine 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) self.set_secure_cookie("atat", session_id) return self.redirect("/home") @@ -25,7 +27,7 @@ class BaseHandler(tornado.web.RequestHandler): response = yield self.authz_client.post( "/users", json={"id": user_id, "atat_role": "ccpo"} ) - return response.json["atat_permissions"] + return response.json def get_current_user(self): cookie = self.get_secure_cookie("atat") From 0c0aa444687f89ec6d073b1c3caff2bc66712972 Mon Sep 17 00:00:00 2001 From: dandds Date: Wed, 18 Jul 2018 10:44:42 -0400 Subject: [PATCH 04/29] get user perms or create them on login --- atst/handler.py | 13 ++++++-- tests/mocks.py | 85 ++++++++++++++++++++++++++----------------------- 2 files changed, 56 insertions(+), 42 deletions(-) diff --git a/atst/handler.py b/atst/handler.py index 485873c0..3766aa95 100644 --- a/atst/handler.py +++ b/atst/handler.py @@ -24,10 +24,17 @@ class BaseHandler(tornado.web.RequestHandler): @tornado.gen.coroutine def _get_user_permissions(self, user_id): - response = yield self.authz_client.post( - "/users", json={"id": user_id, "atat_role": "ccpo"} + response = yield self.authz_client.get( + "/users/{}".format(user_id), raise_error=False ) - return response.json + 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): cookie = self.get_secure_cookie("atat") diff --git a/tests/mocks.py b/tests/mocks.py index b916d677..2b91e25f 100644 --- a/tests/mocks.py +++ b/tests/mocks.py @@ -5,6 +5,7 @@ from atst.api_client import ApiClient class MockApiClient(ApiClient): + def __init__(self, service): self.service = service @@ -43,6 +44,7 @@ class MockApiClient(ApiClient): class MockRequestsClient(MockApiClient): + @tornado.gen.coroutine def get(self, path, **kwargs): json = { @@ -64,44 +66,49 @@ class MockRequestsClient(MockApiClient): class MockAuthzClient(MockApiClient): + _json = { + "atat_permissions": [ + "view_original_jedi_request", + "review_and_approve_jedi_workspace_request", + "modify_atat_role_permissions", + "create_csp_role", + "delete_csp_role", + "deactivate_csp_role", + "modify_csp_role_permissions", + "view_usage_report", + "view_usage_dollars", + "add_and_assign_csp_roles", + "remove_csp_roles", + "request_new_csp_role", + "assign_and_unassign_atat_role", + "view_assigned_atat_role_configurations", + "view_assigned_csp_role_configurations", + "deactivate_workspace", + "view_atat_permissions", + "transfer_ownership_of_workspace", + "add_application_in_workspace", + "delete_application_in_workspace", + "deactivate_application_in_workspace", + "view_application_in_workspace", + "rename_application_in_workspace", + "add_environment_in_application", + "delete_environment_in_application", + "deactivate_environment_in_application", + "view_environment_in_application", + "rename_environment_in_application", + "add_tag_to_workspace", + "remove_tag_from_workspace", + ], + "atat_role": "ccpo", + "id": "164497f6-c1ea-4f42-a5ef-101da278c012", + "username": None, + "workspace_roles": [], + } + @tornado.gen.coroutine def post(self, path, **kwargs): - json = { - "atat_permissions": [ - "view_original_jedi_request", - "review_and_approve_jedi_workspace_request", - "modify_atat_role_permissions", - "create_csp_role", - "delete_csp_role", - "deactivate_csp_role", - "modify_csp_role_permissions", - "view_usage_report", - "view_usage_dollars", - "add_and_assign_csp_roles", - "remove_csp_roles", - "request_new_csp_role", - "assign_and_unassign_atat_role", - "view_assigned_atat_role_configurations", - "view_assigned_csp_role_configurations", - "deactivate_workspace", - "view_atat_permissions", - "transfer_ownership_of_workspace", - "add_application_in_workspace", - "delete_application_in_workspace", - "deactivate_application_in_workspace", - "view_application_in_workspace", - "rename_application_in_workspace", - "add_environment_in_application", - "delete_environment_in_application", - "deactivate_environment_in_application", - "view_environment_in_application", - "rename_environment_in_application", - "add_tag_to_workspace", - "remove_tag_from_workspace", - ], - "atat_role": "ccpo", - "id": "164497f6-c1ea-4f42-a5ef-101da278c012", - "username": None, - "workspace_roles": [], - } - return self._get_response("POST", path, 200, json=json) + 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) From 46a8d8aade5ccc08d39eae1b44f5b763be2c0168 Mon Sep 17 00:00:00 2001 From: dandds Date: Wed, 18 Jul 2018 10:45:26 -0400 Subject: [PATCH 05/29] add additional dev roles and set perms for them --- atst/handlers/dev.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/atst/handlers/dev.py b/atst/handlers/dev.py index bfb5684e..71fdb8e8 100644 --- a/atst/handlers/dev.py +++ b/atst/handlers/dev.py @@ -13,9 +13,28 @@ _DEV_USERS = { "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): def initialize(self, action, sessions, authz_client): @@ -27,4 +46,12 @@ class Dev(BaseHandler): def get(self): role = self.get_argument("role", "ccpo") user = _DEV_USERS[role] + yield self._set_user_permissions(user["id"], role) 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 From f51dc8f07cb17658c754730a3464dcf3138b5395 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Wed, 11 Jul 2018 13:35:29 -0400 Subject: [PATCH 06/29] Refactor forms so we can pass requests_client to validate --- atst/forms/financial.py | 5 +++-- atst/forms/forms.py | 7 +++++++ atst/forms/org.py | 4 ++-- atst/forms/poc.py | 4 ++-- atst/forms/request.py | 4 ++-- atst/forms/review.py | 5 +++-- atst/handlers/request_new.py | 2 +- tests/handlers/test_request_new.py | 2 +- 8 files changed, 21 insertions(+), 12 deletions(-) create mode 100644 atst/forms/forms.py diff --git a/atst/forms/financial.py b/atst/forms/financial.py index da4915f3..9d1bf6c7 100644 --- a/atst/forms/financial.py +++ b/atst/forms/financial.py @@ -1,12 +1,13 @@ from wtforms.fields.html5 import EmailField from wtforms.fields import StringField, SelectField from wtforms.validators import Required, Email -from wtforms_tornado import Form from .fields import NewlineListField +from .forms import ValidatedForm -class FinancialForm(Form): +class FinancialForm(ValidatedForm): + task_order_id = StringField( "Task Order Number associated with this request.", validators=[Required()] ) diff --git a/atst/forms/forms.py b/atst/forms/forms.py new file mode 100644 index 00000000..064eed58 --- /dev/null +++ b/atst/forms/forms.py @@ -0,0 +1,7 @@ +from wtforms_tornado import Form + + +class ValidatedForm(Form): + + def validate(self, requests_client=None): + return super(ValidatedForm, self).validate() diff --git a/atst/forms/org.py b/atst/forms/org.py index 50d80123..80bb5722 100644 --- a/atst/forms/org.py +++ b/atst/forms/org.py @@ -1,13 +1,13 @@ from wtforms.fields.html5 import EmailField, TelField from wtforms.fields import RadioField, StringField from wtforms.validators import Required, Email -from wtforms_tornado import Form import pendulum from .fields import DateField +from .forms import ValidatedForm from .validators import DateRange, PhoneNumber, Alphabet -class OrgForm(Form): +class OrgForm(ValidatedForm): fname_request = StringField("First Name", validators=[Required(), Alphabet()]) lname_request = StringField("Last Name", validators=[Required(), Alphabet()]) diff --git a/atst/forms/poc.py b/atst/forms/poc.py index 5d64182d..cd57add1 100644 --- a/atst/forms/poc.py +++ b/atst/forms/poc.py @@ -1,10 +1,10 @@ from wtforms.fields import StringField from wtforms.validators import Required, Email, Length -from wtforms_tornado import Form +from .forms import ValidatedForm from .validators import IsNumber, Alphabet -class POCForm(Form): +class POCForm(ValidatedForm): fname_poc = StringField("POC First Name", validators=[Required(), Alphabet()]) lname_poc = StringField("POC Last Name", validators=[Required(), Alphabet()]) diff --git a/atst/forms/request.py b/atst/forms/request.py index e6af039e..6c89aeb2 100644 --- a/atst/forms/request.py +++ b/atst/forms/request.py @@ -1,13 +1,13 @@ from wtforms.fields.html5 import IntegerField from wtforms.fields import RadioField, StringField, TextAreaField from wtforms.validators import NumberRange, InputRequired -from wtforms_tornado import Form from .fields import DateField +from .forms import ValidatedForm from .validators import DateRange import pendulum -class RequestForm(Form): +class RequestForm(ValidatedForm): # Details of Use: Overall Request Details dollar_value = IntegerField( diff --git a/atst/forms/review.py b/atst/forms/review.py index b3cd2a21..68b6f198 100644 --- a/atst/forms/review.py +++ b/atst/forms/review.py @@ -1,6 +1,7 @@ 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.") diff --git a/atst/handlers/request_new.py b/atst/handlers/request_new.py index 59092ddf..055c2729 100644 --- a/atst/handlers/request_new.py +++ b/atst/handlers/request_new.py @@ -105,7 +105,7 @@ class JEDIRequestFlow(object): return self.form_class()() def validate(self): - return self.form.validate() + return self.form.validate(self.requests_client) @property def current_screen(self): diff --git a/tests/handlers/test_request_new.py b/tests/handlers/test_request_new.py index 7cd99495..d0bd0a23 100644 --- a/tests/handlers/test_request_new.py +++ b/tests/handlers/test_request_new.py @@ -37,7 +37,7 @@ def test_submit_valid_request_form(monkeypatch, http_client, base_url): monkeypatch.setattr( "atst.handlers.request_new.RequestNew.check_xsrf_cookie", lambda s: True ) - monkeypatch.setattr("atst.forms.request.RequestForm.validate", lambda s: True) + monkeypatch.setattr("atst.forms.request.RequestForm.validate", lambda s, c: True) # this just needs to send a known invalid form value response = yield http_client.fetch( From c86e703618232e67f0426c49275e84573eff2c38 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Wed, 11 Jul 2018 15:37:34 -0400 Subject: [PATCH 07/29] Redirect to /requests after completing final step --- atst/handlers/request_new.py | 9 ++++++--- tests/mocks.py | 1 + 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/atst/handlers/request_new.py b/atst/handlers/request_new.py index 055c2729..49641fc9 100644 --- a/atst/handlers/request_new.py +++ b/atst/handlers/request_new.py @@ -27,9 +27,12 @@ class RequestNew(BaseHandler): if jedi_flow.validate(): 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 - ) + 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.set_status(response.code) diff --git a/tests/mocks.py b/tests/mocks.py index 2b91e25f..a04e40bf 100644 --- a/tests/mocks.py +++ b/tests/mocks.py @@ -61,6 +61,7 @@ class MockRequestsClient(MockApiClient): "id": "66b8ef71-86d3-48ef-abc2-51bfa1732b6b", "creator": "49903ae7-da4a-49bf-a6dc-9dff5d004238", "body": {}, + "status": "incomplete", } return self._get_response("POST", path, 202, json=json) From 10d4c2b90bfa5549327771da88bff8ea454a6db2 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Mon, 16 Jul 2018 15:49:27 -0400 Subject: [PATCH 08/29] Refactor to allow validating warnings on a form --- atst/forms/forms.py | 4 +-- atst/handlers/request_new.py | 39 ++++++++++++++++++++---------- tests/handlers/test_request_new.py | 2 +- 3 files changed, 29 insertions(+), 16 deletions(-) diff --git a/atst/forms/forms.py b/atst/forms/forms.py index 064eed58..732bcc52 100644 --- a/atst/forms/forms.py +++ b/atst/forms/forms.py @@ -3,5 +3,5 @@ from wtforms_tornado import Form class ValidatedForm(Form): - def validate(self, requests_client=None): - return super(ValidatedForm, self).validate() + def validate_warnings(self, requests_client=None): + return True diff --git a/atst/handlers/request_new.py b/atst/handlers/request_new.py index 49641fc9..29b99a75 100644 --- a/atst/handlers/request_new.py +++ b/atst/handlers/request_new.py @@ -24,28 +24,38 @@ class RequestNew(BaseHandler): self.requests_client, screen, post_data=post_data, request_id=request_id ) + rerender_args = dict( + f=jedi_flow.form, + data=post_data, + page=self.page, + screens=jedi_flow.screens, + current=screen, + next_screen=jedi_flow.next_screen, + request_id=jedi_flow.request_id, + ) + if jedi_flow.validate(): response = yield jedi_flow.create_or_update_request(self.get_current_user()) if response.ok: - if jedi_flow.next_screen >= len(jedi_flow.screens): - where = "/requests" + if jedi_flow.validate_warnings(): + 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: - where = self.application.default_router.reverse_url( - "request_form_update", jedi_flow.next_screen, jedi_flow.request_id + self.render( + "requests/screen-%d.html.to" % int(screen), + **rerender_args ) - self.redirect(where) else: self.set_status(response.code) else: self.render( "requests/screen-%d.html.to" % int(screen), - f=jedi_flow.form, - data=post_data, - page=self.page, - screens=jedi_flow.screens, - current=screen, - next_screen=jedi_flow.next_screen, - request_id=jedi_flow.request_id, + **rerender_args ) @tornado.web.authenticated @@ -108,7 +118,10 @@ class JEDIRequestFlow(object): return self.form_class()() def validate(self): - return self.form.validate(self.requests_client) + return self.form.validate() + + def validate_warnings(self): + return self.form.validate_warnings(self.requests_client) @property def current_screen(self): diff --git a/tests/handlers/test_request_new.py b/tests/handlers/test_request_new.py index d0bd0a23..7cd99495 100644 --- a/tests/handlers/test_request_new.py +++ b/tests/handlers/test_request_new.py @@ -37,7 +37,7 @@ def test_submit_valid_request_form(monkeypatch, http_client, base_url): monkeypatch.setattr( "atst.handlers.request_new.RequestNew.check_xsrf_cookie", lambda s: True ) - monkeypatch.setattr("atst.forms.request.RequestForm.validate", lambda s, c: True) + monkeypatch.setattr("atst.forms.request.RequestForm.validate", lambda s: True) # this just needs to send a known invalid form value response = yield http_client.fetch( From 760c9ee9d291823f9b4920c62741ae1817c2b09a Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Tue, 17 Jul 2018 09:45:34 -0400 Subject: [PATCH 09/29] Add fundz client dep to app --- atst/app.py | 5 +++++ config/base.ini | 1 + 2 files changed, 6 insertions(+) diff --git a/atst/app.py b/atst/app.py index 07f40411..58135096 100644 --- a/atst/app.py +++ b/atst/app.py @@ -126,6 +126,11 @@ def make_deps(config): api_version="v1", validate_cert=validate_cert, ), + "fundz_client": ApiClient( + config["default"]["FUNDZ_BASE_URL"], + api_version="v1", + validate_cert=validate_cert, + ), "requests_client": ApiClient( config["default"]["REQUESTS_QUEUE_BASE_URL"], api_version="v1", diff --git a/config/base.ini b/config/base.ini index 6a367c63..431eacf7 100644 --- a/config/base.ini +++ b/config/base.ini @@ -4,6 +4,7 @@ ENVIRONMENT = dev DEBUG = true AUTHZ_BASE_URL = http://localhost:8002 AUTHNID_BASE_URL= https://localhost:8001 +FUNDZ_BASE_URL= http://localhost:8004 COOKIE_SECRET = some-secret-please-replace SECRET = change_me_into_something_secret CAC_URL = https://localhost:8001 From db097b211fc32f440866dc999fced6f684d8c59c Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Tue, 17 Jul 2018 09:58:00 -0400 Subject: [PATCH 10/29] Pass fundz client to requests_new page handler --- atst/app.py | 18 +++++++++++++++--- atst/handlers/request_new.py | 2 +- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/atst/app.py b/atst/app.py index 58135096..8577e28e 100644 --- a/atst/app.py +++ b/atst/app.py @@ -57,19 +57,31 @@ def make_app(config, deps, **kwargs): url( r"/requests/new", 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", ), url( r"/requests/new/([0-9])", 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", ), url( r"/requests/new/([0-9])/(\S+)", 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", ), url( diff --git a/atst/handlers/request_new.py b/atst/handlers/request_new.py index 29b99a75..5acdb28f 100644 --- a/atst/handlers/request_new.py +++ b/atst/handlers/request_new.py @@ -10,7 +10,7 @@ from atst.forms.financial import FinancialForm class RequestNew(BaseHandler): - def initialize(self, page, requests_client): + def initialize(self, page, requests_client, fundz_client): self.page = page self.requests_client = requests_client From 2bca2f9ac86c252601c908008ff5805afa092137 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Tue, 17 Jul 2018 10:27:27 -0400 Subject: [PATCH 11/29] Validate form warnings with existing request and fundz client --- atst/forms/forms.py | 2 +- atst/handlers/request_new.py | 38 ++++++++++++++++++++++++++++++------ 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/atst/forms/forms.py b/atst/forms/forms.py index 732bcc52..d49c70ce 100644 --- a/atst/forms/forms.py +++ b/atst/forms/forms.py @@ -3,5 +3,5 @@ from wtforms_tornado import Form class ValidatedForm(Form): - def validate_warnings(self, requests_client=None): + def validate_warnings(self, *args, **kwargs): return True diff --git a/atst/handlers/request_new.py b/atst/handlers/request_new.py index 5acdb28f..23315bcd 100644 --- a/atst/handlers/request_new.py +++ b/atst/handlers/request_new.py @@ -13,6 +13,14 @@ class RequestNew(BaseHandler): def initialize(self, page, requests_client, fundz_client): self.page = page self.requests_client = requests_client + self.fundz_client = fundz_client + + @tornado.gen.coroutine + def get_existing_request(self, current_user, request_id): + request = yield self.requests_client.get( + "/users/{}/requests/{}".format(current_user["id"], request_id), + ) + return request.json @tornado.web.authenticated @tornado.gen.coroutine @@ -20,8 +28,16 @@ class RequestNew(BaseHandler): self.check_xsrf_cookie() screen = int(screen) post_data = self.request.arguments + current_user = self.get_current_user() + existing_request = yield self.get_existing_request(current_user, request_id) 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, ) rerender_args = dict( @@ -35,7 +51,7 @@ class RequestNew(BaseHandler): ) if jedi_flow.validate(): - response = yield jedi_flow.create_or_update_request(self.get_current_user()) + response = yield jedi_flow.create_or_update_request() if response.ok: if jedi_flow.validate_warnings(): if jedi_flow.next_screen >= len(jedi_flow.screens): @@ -73,7 +89,7 @@ class RequestNew(BaseHandler): request = response.json 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( @@ -93,12 +109,16 @@ class JEDIRequestFlow(object): def __init__( self, requests_client, + fundz_client, current_step, request=None, post_data=None, request_id=None, + current_user=None, + existing_request=None, ): self.requests_client = requests_client + self.fundz_client = fundz_client self.current_step = current_step self.request = request @@ -109,6 +129,9 @@ class JEDIRequestFlow(object): self.request_id = request_id self.form = self._form() + self.current_user = current_user + self.existing_request = existing_request + def _form(self): if self.is_post: return self.form_class()(self.post_data) @@ -121,7 +144,10 @@ class JEDIRequestFlow(object): return self.form.validate() def validate_warnings(self): - return self.form.validate_warnings(self.requests_client) + return self.form.validate_warnings( + self.existing_request.get('body', {}).get(self.form_section), + self.fundz_client, + ) @property def current_screen(self): @@ -201,9 +227,9 @@ class JEDIRequestFlow(object): ] @tornado.gen.coroutine - def create_or_update_request(self, user): + def create_or_update_request(self): request_data = { - "creator_id": user["id"], + "creator_id": self.current_user["id"], "request": {self.form_section: self.form.data}, } if self.request_id: From f1ba2bcba69e5456ed85ce20cbd52bc94ca089fa Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Tue, 17 Jul 2018 16:29:02 -0400 Subject: [PATCH 12/29] Perform extra validation on financial verification step --- atst/forms/financial.py | 27 +++++++++++++++++++++++++++ atst/forms/forms.py | 9 +++++++-- atst/handlers/request_new.py | 7 +++++-- 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/atst/forms/financial.py b/atst/forms/financial.py index 9d1bf6c7..3bdb8e53 100644 --- a/atst/forms/financial.py +++ b/atst/forms/financial.py @@ -1,13 +1,40 @@ +import tornado +from tornado.gen import Return from wtforms.fields.html5 import EmailField from wtforms.fields import StringField, SelectField +from wtforms.form import Form from wtforms.validators import Required, Email from .fields import NewlineListField from .forms import ValidatedForm +@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 existing_request['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 Number associated with this request.", validators=[Required()] ) diff --git a/atst/forms/forms.py b/atst/forms/forms.py index d49c70ce..c3ea02c4 100644 --- a/atst/forms/forms.py +++ b/atst/forms/forms.py @@ -1,7 +1,12 @@ +import tornado +from tornado.gen import Return from wtforms_tornado import Form class ValidatedForm(Form): - def validate_warnings(self, *args, **kwargs): - return True + @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) diff --git a/atst/handlers/request_new.py b/atst/handlers/request_new.py index 23315bcd..2b31b0f6 100644 --- a/atst/handlers/request_new.py +++ b/atst/handlers/request_new.py @@ -53,7 +53,8 @@ class RequestNew(BaseHandler): if jedi_flow.validate(): response = yield jedi_flow.create_or_update_request() if response.ok: - if jedi_flow.validate_warnings(): + valid = yield jedi_flow.validate_warnings() + if valid: if jedi_flow.next_screen >= len(jedi_flow.screens): where = "/requests" else: @@ -143,11 +144,13 @@ class JEDIRequestFlow(object): def validate(self): return self.form.validate() + @tornado.gen.coroutine def validate_warnings(self): - return self.form.validate_warnings( + valid = yield self.form.perform_extra_validation( self.existing_request.get('body', {}).get(self.form_section), self.fundz_client, ) + return valid @property def current_screen(self): From 3f8b584ff8a79cf51fb3d317eec8a217d0710012 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Thu, 19 Jul 2018 11:29:43 -0400 Subject: [PATCH 13/29] Update url to get a request --- atst/handlers/request_new.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/atst/handlers/request_new.py b/atst/handlers/request_new.py index 2b31b0f6..ee5e1a59 100644 --- a/atst/handlers/request_new.py +++ b/atst/handlers/request_new.py @@ -16,10 +16,8 @@ class RequestNew(BaseHandler): self.fundz_client = fundz_client @tornado.gen.coroutine - def get_existing_request(self, current_user, request_id): - request = yield self.requests_client.get( - "/users/{}/requests/{}".format(current_user["id"], request_id), - ) + def get_existing_request(self, request_id): + request = yield self.requests_client.get("/requests/{}".format(request_id)) return request.json @tornado.web.authenticated @@ -29,7 +27,7 @@ class RequestNew(BaseHandler): screen = int(screen) post_data = self.request.arguments current_user = self.get_current_user() - existing_request = yield self.get_existing_request(current_user, request_id) + existing_request = yield self.get_existing_request(request_id) jedi_flow = JEDIRequestFlow( self.requests_client, self.fundz_client, From 355d2ed36a9813bc9c0f3c2a9eabd16a7cae67b7 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Thu, 19 Jul 2018 11:30:03 -0400 Subject: [PATCH 14/29] Add tests for PE id validation --- atst/forms/financial.py | 2 +- tests/conftest.py | 3 +- tests/handlers/test_request_new.py | 82 ++++++++++++++++++++++++++++++ tests/mocks.py | 40 ++++++++++----- 4 files changed, 111 insertions(+), 16 deletions(-) diff --git a/atst/forms/financial.py b/atst/forms/financial.py index 3bdb8e53..77811ee1 100644 --- a/atst/forms/financial.py +++ b/atst/forms/financial.py @@ -31,7 +31,7 @@ class FinancialForm(ValidatedForm): @tornado.gen.coroutine def perform_extra_validation(self, existing_request, fundz_client): valid = True - if existing_request['pe_id'] != self.pe_id.data: + 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) diff --git a/tests/conftest.py b/tests/conftest.py index 469a2100..c650b5f1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,7 @@ import pytest 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 @@ -11,6 +11,7 @@ def app(): "authz_client": MockAuthzClient("authz"), "requests_client": MockRequestsClient("requests"), "authnid_client": MockApiClient("authnid"), + "fundz_client": MockFundzClient("fundz"), "sessions": DictSessions(), } diff --git a/tests/handlers/test_request_new.py b/tests/handlers/test_request_new.py index 7cd99495..6a8a37f6 100644 --- a/tests/handlers/test_request_new.py +++ b/tests/handlers/test_request_new.py @@ -1,5 +1,8 @@ import re import pytest +import tornado +import urllib +from tests.mocks import MOCK_REQUEST, MOCK_VALID_PE_ID ERROR_CLASS = "usa-input-error-message" MOCK_USER = { @@ -47,3 +50,82 @@ def test_submit_valid_request_form(monkeypatch, http_client, base_url): body="meaning=42", ) 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" diff --git a/tests/mocks.py b/tests/mocks.py index a04e40bf..6cd3540c 100644 --- a/tests/mocks.py +++ b/tests/mocks.py @@ -43,27 +43,39 @@ class MockApiClient(ApiClient): return response +MOCK_REQUEST = { + "id": "66b8ef71-86d3-48ef-abc2-51bfa1732b6b", + "creator": "49903ae7-da4a-49bf-a6dc-9dff5d004238", + "body": { + "financial_verification": { + "pe_id": "0203752A", + }, + }, + "status": "incomplete" +} + class MockRequestsClient(MockApiClient): @tornado.gen.coroutine def get(self, path, **kwargs): - json = { - "id": "66b8ef71-86d3-48ef-abc2-51bfa1732b6b", - "creator": "49903ae7-da4a-49bf-a6dc-9dff5d004238", - "body": {}, - "status": "incomplete", - } - return self._get_response("GET", path, 200, json=json) + return self._get_response("GET", path, 200, json=MOCK_REQUEST) @tornado.gen.coroutine def post(self, path, **kwargs): - json = { - "id": "66b8ef71-86d3-48ef-abc2-51bfa1732b6b", - "creator": "49903ae7-da4a-49bf-a6dc-9dff5d004238", - "body": {}, - "status": "incomplete", - } - return self._get_response("POST", path, 202, json=json) + return self._get_response("POST", path, 202, json=MOCK_REQUEST) + + +MOCK_VALID_PE_ID = "8675309U" + + +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): From 07c85125501a152f7807d8a0c0cc35497b0cc5e5 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Thu, 19 Jul 2018 14:12:07 -0400 Subject: [PATCH 15/29] Fundz API does not have version info --- atst/app.py | 1 - 1 file changed, 1 deletion(-) diff --git a/atst/app.py b/atst/app.py index 8577e28e..e34d6701 100644 --- a/atst/app.py +++ b/atst/app.py @@ -140,7 +140,6 @@ def make_deps(config): ), "fundz_client": ApiClient( config["default"]["FUNDZ_BASE_URL"], - api_version="v1", validate_cert=validate_cert, ), "requests_client": ApiClient( From 7f972f8db05812eea9dd305ec900a1ad2b8fb387 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Thu, 19 Jul 2018 16:59:00 -0400 Subject: [PATCH 16/29] Handle case when request is None --- atst/handlers/request_new.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/atst/handlers/request_new.py b/atst/handlers/request_new.py index ee5e1a59..096572d3 100644 --- a/atst/handlers/request_new.py +++ b/atst/handlers/request_new.py @@ -17,6 +17,8 @@ class RequestNew(BaseHandler): @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 From 305877d9535df6d07e638607faaba3eb9c679cc9 Mon Sep 17 00:00:00 2001 From: Andrew Croce Date: Fri, 20 Jul 2018 08:23:01 -0400 Subject: [PATCH 17/29] Add UI Method to check if the current URL path matches a link href --- atst/app.py | 2 ++ atst/ui_methods.py | 2 ++ 2 files changed, 4 insertions(+) create mode 100644 atst/ui_methods.py diff --git a/atst/app.py b/atst/app.py index 07f40411..625e2a74 100644 --- a/atst/app.py +++ b/atst/app.py @@ -16,6 +16,7 @@ from atst.home import home from atst.api_client import ApiClient from atst.sessions import RedisSessions from atst import ui_modules +from atst import ui_methods ENV = os.getenv("TORNADO_ENV", "dev") @@ -101,6 +102,7 @@ def make_app(config, deps, **kwargs): cookie_secret=config["default"]["COOKIE_SECRET"], debug=config["default"].getboolean("DEBUG"), ui_modules=ui_modules, + ui_methods=ui_methods, **kwargs, ) app.config = config diff --git a/atst/ui_methods.py b/atst/ui_methods.py new file mode 100644 index 00000000..d5c5bf78 --- /dev/null +++ b/atst/ui_methods.py @@ -0,0 +1,2 @@ +def matchesPath(self, href): + return self.request.uri.startswith(href) From 6ebbdfc64d3eb9d2eb34489f76a4bd623a592c26 Mon Sep 17 00:00:00 2001 From: Andrew Croce Date: Fri, 20 Jul 2018 08:23:19 -0400 Subject: [PATCH 18/29] Sidenav Item module --- atst/ui_modules.py | 9 +++++++++ templates/navigation/_sidenav_item.html.to | 9 +++++++++ 2 files changed, 18 insertions(+) create mode 100644 templates/navigation/_sidenav_item.html.to diff --git a/atst/ui_modules.py b/atst/ui_modules.py index ec197bcd..77671339 100644 --- a/atst/ui_modules.py +++ b/atst/ui_modules.py @@ -5,3 +5,12 @@ class Icon(UIModule): with open('static/icons/%s.svg' % name) as svg: return self.render_string( "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) diff --git a/templates/navigation/_sidenav_item.html.to b/templates/navigation/_sidenav_item.html.to new file mode 100644 index 00000000..a41b7726 --- /dev/null +++ b/templates/navigation/_sidenav_item.html.to @@ -0,0 +1,9 @@ +
  • + + {% if icon %} + {% module Icon(icon, classes="sidenav__link-icon") %} + {% end %} + + {{label}} + +
  • From ccef45b78c5475dd3f55be07524b06d4d024fde8 Mon Sep 17 00:00:00 2001 From: Andrew Croce Date: Fri, 20 Jul 2018 08:24:54 -0400 Subject: [PATCH 19/29] remove/reorganize some css --- scss/components/_layout.scss | 31 +++++++++++ scss/core/_base.scss | 16 ------ scss/{sections => elements}/_sidenav.scss | 68 ++++++++--------------- scss/sections/_main.scss | 3 - scss/sections/_topbar.scss | 54 ------------------ 5 files changed, 55 insertions(+), 117 deletions(-) create mode 100644 scss/components/_layout.scss delete mode 100644 scss/core/_base.scss rename scss/{sections => elements}/_sidenav.scss (63%) delete mode 100644 scss/sections/_main.scss delete mode 100644 scss/sections/_topbar.scss diff --git a/scss/components/_layout.scss b/scss/components/_layout.scss new file mode 100644 index 00000000..ee8308af --- /dev/null +++ b/scss/components/_layout.scss @@ -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; + } + } +} diff --git a/scss/core/_base.scss b/scss/core/_base.scss deleted file mode 100644 index 57513b32..00000000 --- a/scss/core/_base.scss +++ /dev/null @@ -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; - } -} diff --git a/scss/sections/_sidenav.scss b/scss/elements/_sidenav.scss similarity index 63% rename from scss/sections/_sidenav.scss rename to scss/elements/_sidenav.scss index 4415c3ae..7fa92cf6 100644 --- a/scss/sections/_sidenav.scss +++ b/scss/elements/_sidenav.scss @@ -1,15 +1,13 @@ .sidenav { - @include grid-pad; - @include panel-margin; - width: 100%; - flex-shrink: 0; + ul { + list-style: none; + margin: 0; + padding: 0; - @include media($large-screen) { - width: 21rem; - } - - @include media($xlarge-screen) { - width: 30rem; + li { + margin: 0; + display: block; + } } .sidenav__link { @@ -18,10 +16,10 @@ padding: $gap ($gap * 2); color: $color-black; text-decoration: none; + white-space: nowrap; - &:hover { - background-color: $color-white; - color: $color-primary; + .sidenav__link-icon { + margin-left: - ($gap * .5); } &.sidenav__link--disabled { @@ -35,60 +33,42 @@ color: $color-primary; box-shadow: inset .4rem 0 0 0 $color-primary; + .sidenav__link-icon { + @include icon-style-active; + } + + ul { background-color: $color-white; .sidenav__link { &--active { @include h5; - box-shadow: none; + color: $color-primary; } } } } + ul { + padding-bottom: $gap / 2; + li { .sidenav__link { + @include h5; padding: ($gap * .75) ($gap * 3); border: 0; - @include h5; font-weight: normal; } - - &:last-child { - .sidenav__link { - padding-bottom: $gap * 1.5; - } - } } } - } - ul { - list-style: none; - margin: 0; - padding: 0; + &:hover { + color: $color-primary; - li { - margin: 0; - display: block; - } - } - - > ul { - @include panel-margin; - - &:last-child { - margin: 0; - } - - > li { - &:last-child { - > .sidenav__link { - border-bottom: 1px solid $color-black; - } + .sidenav__link-icon { + @include icon-style-active; } + } } } diff --git a/scss/sections/_main.scss b/scss/sections/_main.scss deleted file mode 100644 index 063b70e7..00000000 --- a/scss/sections/_main.scss +++ /dev/null @@ -1,3 +0,0 @@ -section { - margin-bottom: 10rem; -} diff --git a/scss/sections/_topbar.scss b/scss/sections/_topbar.scss deleted file mode 100644 index 37114a6c..00000000 --- a/scss/sections/_topbar.scss +++ /dev/null @@ -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; - } - } - } - } - } -} From 30f20e7c781e7376ee6685749b8e27db0f35323a Mon Sep 17 00:00:00 2001 From: Andrew Croce Date: Fri, 20 Jul 2018 08:25:18 -0400 Subject: [PATCH 20/29] remove unnecessary panel-container --- scss/elements/_panels.scss | 33 --------------------------------- 1 file changed, 33 deletions(-) diff --git a/scss/elements/_panels.scss b/scss/elements/_panels.scss index f52914b2..5b0c1220 100644 --- a/scss/elements/_panels.scss +++ b/scss/elements/_panels.scss @@ -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; - } - - -} From e9a2921f599079697368b7ad57523e3ed242097b Mon Sep 17 00:00:00 2001 From: Andrew Croce Date: Fri, 20 Jul 2018 08:25:47 -0400 Subject: [PATCH 21/29] Define temporary context variable --- templates/base.html.to | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/templates/base.html.to b/templates/base.html.to index f271513d..2c87844e 100644 --- a/templates/base.html.to +++ b/templates/base.html.to @@ -1,3 +1,7 @@ +{# TODO: set this context elsewhere #} +{# set context='workspace' #} +{% set context='global' %} + From 7e05b237ff72f5f7ff7de18c6ace6429c3473180 Mon Sep 17 00:00:00 2001 From: Andrew Croce Date: Fri, 20 Jul 2018 08:26:53 -0400 Subject: [PATCH 22/29] rename and refactor topbar/header --- templates/navigation/topbar.html.to | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 templates/navigation/topbar.html.to diff --git a/templates/navigation/topbar.html.to b/templates/navigation/topbar.html.to new file mode 100644 index 00000000..9c6ef28c --- /dev/null +++ b/templates/navigation/topbar.html.to @@ -0,0 +1,19 @@ +
    + +
    From 7ed3d50c2e846c4ca621d991d0ba66b8486fa6c9 Mon Sep 17 00:00:00 2001 From: Andrew Croce Date: Fri, 20 Jul 2018 08:28:17 -0400 Subject: [PATCH 23/29] refactor base template --- templates/base.html.to | 17 +++++++++-------- templates/home.html.to | 4 ---- templates/requests.html.to | 4 ---- 3 files changed, 9 insertions(+), 16 deletions(-) diff --git a/templates/base.html.to b/templates/base.html.to index 2c87844e..f3412da9 100644 --- a/templates/base.html.to +++ b/templates/base.html.to @@ -14,19 +14,20 @@ + {% include 'navigation/topbar.html.to' %} - {% include 'header.html.to' %} +
    + {% include 'navigation/global_navigation.html.to' %} -
    - {% block sidenav %}{% end %} +
    + {% block sidenav %}{% end %} - {% block content %} - these are not the droids you are looking for - {% end %} + {% block content %} + these are not the droids you are looking for + {% end %} +
    - - {% include 'footer.html.to' %} diff --git a/templates/home.html.to b/templates/home.html.to index e5f7c8f7..eb789f28 100644 --- a/templates/home.html.to +++ b/templates/home.html.to @@ -1,9 +1,5 @@ {% extends "base.html.to" %} -{% block sidenav %} -{% include 'nav-side.html.to' %} -{% end %} - {% block content %}
    diff --git a/templates/requests.html.to b/templates/requests.html.to index ff1835ce..653c01b3 100644 --- a/templates/requests.html.to +++ b/templates/requests.html.to @@ -1,9 +1,5 @@ {% extends "base.html.to" %} -{% block sidenav %} -{% include 'nav-side.html.to' %} -{% end %} - {% block content %} From 4d949b59229865c8f4bd34e7734b3bafb70cb561 Mon Sep 17 00:00:00 2001 From: Andrew Croce Date: Fri, 20 Jul 2018 08:29:14 -0400 Subject: [PATCH 24/29] global nav and topbar --- scss/components/_global_navigation.scss | 30 ++++++++ scss/components/_topbar.scss | 71 +++++++++++++++++++ scss/core/_util.scss | 22 ++++++ .../navigation/global_navigation.html.to | 6 ++ 4 files changed, 129 insertions(+) create mode 100644 scss/components/_global_navigation.scss create mode 100644 scss/components/_topbar.scss create mode 100644 templates/navigation/global_navigation.html.to diff --git a/scss/components/_global_navigation.scss b/scss/components/_global_navigation.scss new file mode 100644 index 00000000..203fba9f --- /dev/null +++ b/scss/components/_global_navigation.scss @@ -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; + } + } +} diff --git a/scss/components/_topbar.scss b/scss/components/_topbar.scss new file mode 100644 index 00000000..e56a95fa --- /dev/null +++ b/scss/components/_topbar.scss @@ -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; + } + } + } + } + } +} diff --git a/scss/core/_util.scss b/scss/core/_util.scss index 101ddcf4..505b4ed5 100644 --- a/scss/core/_util.scss +++ b/scss/core/_util.scss @@ -1,3 +1,25 @@ .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; +} diff --git a/templates/navigation/global_navigation.html.to b/templates/navigation/global_navigation.html.to new file mode 100644 index 00000000..742ddaf7 --- /dev/null +++ b/templates/navigation/global_navigation.html.to @@ -0,0 +1,6 @@ + From 09c5659d070d0ae5f489dc9e2e634f6f49d42c16 Mon Sep 17 00:00:00 2001 From: Andrew Croce Date: Fri, 20 Jul 2018 08:29:27 -0400 Subject: [PATCH 25/29] simplify grid for now --- scss/core/_grid.scss | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/scss/core/_grid.scss b/scss/core/_grid.scss index a18f6b44..b24327c3 100644 --- a/scss/core/_grid.scss +++ b/scss/core/_grid.scss @@ -9,12 +9,9 @@ // We are implementing a simple flexbox row/column system @mixin grid-row { - @include media($medium-screen) { - display: flex; - flex-direction: row; - flex-wrap: wrap; - max-width: $site-max-width; - } + display: flex; + flex-direction: row; + flex-wrap: nowrap; } @mixin grid-pad { From 607972b76c7fc899abb93891e8d2bdb81b9b262a Mon Sep 17 00:00:00 2001 From: Andrew Croce Date: Fri, 20 Jul 2018 08:29:39 -0400 Subject: [PATCH 26/29] more variables --- scss/core/_variables.scss | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/scss/core/_variables.scss b/scss/core/_variables.scss index 266065e8..4380ac69 100644 --- a/scss/core/_variables.scss +++ b/scss/core/_variables.scss @@ -3,9 +3,11 @@ * =================================================== */ -$gap: .8rem; // 8px at 10px $em-base -$topbar-height: 4.8rem; -$icon-size-small: 2.4rem; +$gap: 0.8rem; // 8px at 10px $em-base +$topbar-height: 4.8rem; +$icon-bar-width: 4.0rem; +$icon-size-small: 2.4rem; +$hover-transition-time: 0.2s; /* * USWDS Variables From f79762b9321a1dcc4ff50d329236d97cef83471b Mon Sep 17 00:00:00 2001 From: Andrew Croce Date: Fri, 20 Jul 2018 08:29:58 -0400 Subject: [PATCH 27/29] various style fixes --- scss/elements/_icons.scss | 24 +++++++++++++++++++++--- scss/elements/_typography.scss | 17 +++++++++++------ 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/scss/elements/_icons.scss b/scss/elements/_icons.scss index 95f7fb46..0379154d 100644 --- a/scss/elements/_icons.scss +++ b/scss/elements/_icons.scss @@ -1,17 +1,19 @@ @mixin icon { - display: inline-block; - vertical-align: bottom; + display: inline-flex; > svg { width: 100%; height: 100%; + * { + transition: fill $hover-transition-time; + } } } @mixin icon-size($size) { $icon-size: $size * .1rem; width: $icon-size; - height: $icon-size; + height: auto; 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 { @include icon; @include icon-size(16); @include icon-style-default; + + &.icon--tiny { + @include icon-size(10); + } } diff --git a/scss/elements/_typography.scss b/scss/elements/_typography.scss index 87022e95..9096af9d 100644 --- a/scss/elements/_typography.scss +++ b/scss/elements/_typography.scss @@ -4,6 +4,11 @@ * @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 { font-family: $font-sans; @@ -25,15 +30,15 @@ h2 { a, -a > span { +a:hover { transition: - background 0.2s, - border 0.2s, - box-shadow 0.2s, - color 0.2s, + background $hover-transition-time, + border $hover-transition-time, + box-shadow $hover-transition-time, + color $hover-transition-time, } dt { display: inline; font-weight: bold; -} \ No newline at end of file +} From dcbe8990b74077f0bb7c0b1280d2aec4f6327dd1 Mon Sep 17 00:00:00 2001 From: Andrew Croce Date: Fri, 20 Jul 2018 08:30:04 -0400 Subject: [PATCH 28/29] imports --- scss/atat.scss | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scss/atat.scss b/scss/atat.scss index 532330bb..01d4e9b6 100644 --- a/scss/atat.scss +++ b/scss/atat.scss @@ -1,7 +1,6 @@ @import 'core/variables'; @import '../node_modules/uswds/src/stylesheets/uswds'; -@import 'core/base'; @import 'core/grid'; @import 'core/util'; @@ -12,12 +11,13 @@ @import 'elements/block_lists'; @import 'elements/tables'; @import 'elements/icons'; +@import 'elements/sidenav'; +@import 'components/layout'; +@import 'components/topbar'; +@import 'components/global_navigation'; @import 'components/site_action'; @import 'components/empty_state'; -@import 'sections/main'; -@import 'sections/topbar'; -@import 'sections/sidenav'; @import 'sections/footer'; @import 'sections/login'; From 1204847a825ace86456fc019551af5162002a3c3 Mon Sep 17 00:00:00 2001 From: Andrew Croce Date: Fri, 20 Jul 2018 08:30:15 -0400 Subject: [PATCH 29/29] refactor workspaces layout --- templates/workspaces.html.to | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/templates/workspaces.html.to b/templates/workspaces.html.to index c768708c..9f87a99a 100644 --- a/templates/workspaces.html.to +++ b/templates/workspaces.html.to @@ -1,12 +1,8 @@ {% extends "base.html.to" %} {% block content %} - -
    - -

    Workspaces

    - - +
    +
    @@ -29,8 +25,6 @@ {% end %}
    Workspace Name
    - -
    - +
    {% end %}