From bf279b7eeee9696c4aff886c5ea72c4dde74f096 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Tue, 24 Jul 2018 16:48:15 -0400 Subject: [PATCH 01/43] Create mock eda client --- atst/eda_client.py | 356 +++++++++++++++++++++++++++++++++++++++ tests/test_eda_client.py | 17 ++ 2 files changed, 373 insertions(+) create mode 100644 atst/eda_client.py create mode 100644 tests/test_eda_client.py diff --git a/atst/eda_client.py b/atst/eda_client.py new file mode 100644 index 00000000..09acd859 --- /dev/null +++ b/atst/eda_client.py @@ -0,0 +1,356 @@ +class EDAClientBase(object): + def list_contracts( + self, + contract_number=None, + delivery_order=None, + cage_code=None, + duns_number=None, + ): + """ + Get a list of all contracts matching the given filters. + """ + raise NotImplementedError() + + def get_contract(self, contract_number, status): + """ + Get details for a contract. + """ + raise NotImplementedError() + + +class MockEDAClient(EDAClientBase): + def __init__(self, *args, **kwargs): + pass + + def list_contracts( + self, + contract_number=None, + delivery_order=None, + cage_code=None, + duns_number=None, + ): + return [ + { + "aco_mod": "01", + "admin_dodaac": None, + "cage_code": "1U305", + "contract_no": "DCA10096D0052", + "delivery_order": "0084", + "duns_number": None, + "issue_date": "20000228", + "issue_dodaac": None, + "location": "https://docsrv1.nit.disa.mil:443/eda/enforcer/C0414345.PDF?ver=1.4&loc=Y29udHJhY3RzL29nZGVuL3ZlbmRvci8xOTk4LzA5LzE0L0MwNDE0MzQ1LlBERg==&sourceurl=aHR0cHM6Ly9lZGE0Lm5pdC5kaXNhLm1pbC9wbHMvdXNlci9uZXdfYXBwLkdldF9Eb2M_cFRhYmxlX0lEPTImcFJlY29yZF9LZXk9OEE2ODExNjM2RUY5NkU2M0UwMzQwMDYwQjBCMjgyNkM=&uid=6CFC2B2322E86FD5E054002264936E3C&qid=19344159&signed=G&qdate=20180529194407GMT&token=6xQICrrrfIMciEJSpXmfsAYrToM=", + "pay_dodaac": None, + "pco_mod": "02", + }, + { + "aco_mod": "01", + "admin_dodaac": None, + "cage_code": "1U305", + "contract_no": "DCA10096D0052", + "delivery_order": "0084", + "duns_number": None, + "issue_date": "20000228", + "issue_dodaac": None, + "location": "https://docsrv1.nit.disa.mil:443/eda/enforcer/C0414345.PDF?ver=1.4&loc=Y29udHJhY3RzL29nZGVuL3ZlbmRvci8xOTk4LzA5LzE0L0MwNDE0MzQ1LlBERg==&sourceurl=aHR0cHM6Ly9lZGE0Lm5pdC5kaXNhLm1pbC9wbHMvdXNlci9uZXdfYXBwLkdldF9Eb2M_cFRhYmxlX0lEPTImcFJlY29yZF9LZXk9OEE2ODExNjM2RUY5NkU2M0UwMzQwMDYwQjBCMjgyNkM=&uid=6CFC2B2322E86FD5E054002264936E3C&qid=19344159&signed=G&qdate=20180529194407GMT&token=6xQICrrrfIMciEJSpXmfsAYrToM=", + "pay_dodaac": None, + "pco_mod": "02", + }, + { + "aco_mod": "01", + "admin_dodaac": None, + "cage_code": "1U305", + "contract_no": "DCA10096D0052", + "delivery_order": "0084", + "duns_number": None, + "issue_date": "20000228", + "issue_dodaac": None, + "location": "https://docsrv1.nit.disa.mil:443/eda/enforcer/C0414345.PDF?ver=1.4&loc=Y29udHJhY3RzL29nZGVuL3ZlbmRvci8xOTk4LzA5LzE0L0MwNDE0MzQ1LlBERg==&sourceurl=aHR0cHM6Ly9lZGE0Lm5pdC5kaXNhLm1pbC9wbHMvdXNlci9uZXdfYXBwLkdldF9Eb2M_cFRhYmxlX0lEPTImcFJlY29yZF9LZXk9OEE2ODExNjM2RUY5NkU2M0UwMzQwMDYwQjBCMjgyNkM=&uid=6CFC2B2322E86FD5E054002264936E3C&qid=19344159&signed=G&qdate=20180529194407GMT&token=6xQICrrrfIMciEJSpXmfsAYrToM=", + "pay_dodaac": None, + "pco_mod": "02", + }, + ] + + def get_contract(self, contract_number, status): + if contract_number == "DCA10096D0052" and status == "y": + return { + "aco_mod": "01", + "admin_dodaac": None, + "cage_code": "1U305", + "contract_no": "DCA10096D0052", + "delivery_order": "0084", + "duns_number": None, + "issue_date": "20000228", + "issue_dodaac": None, + "location": "https://docsrv1.nit.disa.mil:443/eda/enforcer/C0414345.PDF?ver=1.4&loc=Y29udHJhY3RzL29nZGVuL3ZlbmRvci8xOTk4LzA5LzE0L0MwNDE0MzQ1LlBERg==&sourceurl=aHR0cHM6Ly9lZGE0Lm5pdC5kaXNhLm1pbC9wbHMvdXNlci9uZXdfYXBwLkdldF9Eb2M_cFRhYmxlX0lEPTImcFJlY29yZF9LZXk9OEE2ODExNjM2RUY5NkU2M0UwMzQwMDYwQjBCMjgyNkM=&uid=6CFC2B2322E86FD5E054002264936E3C&qid=19344159&signed=G&qdate=20180529194407GMT&token=6xQICrrrfIMciEJSpXmfsAYrToM=", + "pay_dodaac": None, + "pco_mod": "02", + "amount": 2000000 + } + else: + return None + + +class EDAClient(EDAClientBase): + def __init__(self, base_url, user_name, user_role): + pass + + def list_contracts( + self, + contract_number=None, + delivery_order=None, + cage_code=None, + duns_number=None, + ): + # TODO: Fetch the contracts CSV and transform them into dictionaries. + # https://docs.python.org/3/library/csv.html#csv.DictReader + raise NotImplementedError() + + def get_contract(self, contract_number, status): + # TODO: Fetch the contract XML and transform it into a dictionary. + # https://docs.python.org/3.7/library/xml.etree.elementtree.html + raise NotImplementedError() + + + +CONTRACT_XML = """ + +2.5 +DD 1155 + +3244871 + +00000431 +704331 + + + + + +Department of Defense +Delivery Order +70433119F2644 +Represented Contract + + +Department of Defense +Basic Ordering Agreement +W81K0419G0001 +Ordering Instrument + + +false +Original +false + +Firm Fixed Price + + + + +http://farsite.hill.af.mil/reghtml/regs/far2afmcfars/fardfars/far/52_220.htm#P810_149596 + + +FAR +52.222-50 +Combating Trafficking in Persons. +2015-05 + + + [ lots of text ] + + +
I
+
+ +FAR +52.245-1 +Government Property. +2012-04 + + + [ lots of text ] + + +
I
+
+
+ +2016-02-04 + +2016-01-25 + +DALE WOLFE + +Telephone +520-533-9132 + + + + + +Contractor +
+ +0Z7K0 +808152482 + + +CACI TECHNOLOGIES, INC + + +6933 Gateway Ct +Manassas VA, 20109 + + + +
+
+ +Contract Issuing Office +
+ +704331 + + +FEMA DISTRIBUTION CENTER + + +3870 S. SIDE INDUSTRIAL CTR +ATLANTA GA, 30354 + + + +
+ +GENE BARBER + +Telephone +(202) 646-2727 + + +
+ +Contract Administrative Office +
+ +704331 + + +FEMA DISTRIBUTION CENTER + + +3870 S. SIDE INDUSTRIAL CTR +ATLANTA GA, 30354 + + + +
+
+ +Paying Office +
+ +HQ0131 + + +DEFENSE FINANCE AND ACCOUNTING SVC + + +P.O. BOX 369016 +COLUMBUS OH, 43236 + + + +
+
+ +Ship To +
+ +S0302A + + +DCMA PHOENIX + + +40 NORTH CENTRAL AVE, STE 400 +TWO RENAISSANCE SQUARE +PHOENIX AZ, 85004 + + + +
+
+ + +Header Only - Total Contract Value +192000.00 + + + + +Delivery Requested By + +2016-01-16 + + + + + +Defense Priorities Allocation System (DPAS) Priority Rating + +DO-A7 +
A
+
+ + +Contractor +Origin (after Loading) + + +
+ + + + + +CLIN +0001 + + + + +false + +Cost No Fee + + +Real Property +Radio Dishes +3 +false +Estimated +Each +64000.00 + +Manufacturer's Part Number +5L33M7730291DX081 + + + + + +Estimated Cost +192000.00 + + +Not to Exceed Amount (Funding) +200000.00 + + + + +
+
+""" diff --git a/tests/test_eda_client.py b/tests/test_eda_client.py new file mode 100644 index 00000000..eb1e92d8 --- /dev/null +++ b/tests/test_eda_client.py @@ -0,0 +1,17 @@ +from atst.eda_client import MockEDAClient + + +client = MockEDAClient() + +def test_list_contracts(): + results = client.list_contracts() + assert len(results) == 3 + +def test_get_contract(): + result = client.get_contract("DCA10096D0052", "y") + assert result["contract_no"] == "DCA10096D0052" + assert result["amount"] == 2000000 + +def test_contract_not_found(): + result = client.get_contract("abc", "y") + assert result is None From 6ee71113c7e8845ec5a867da2988afce263012a7 Mon Sep 17 00:00:00 2001 From: Andrew Croce Date: Tue, 31 Jul 2018 10:00:27 -0400 Subject: [PATCH 02/43] Clean up workspace list layout --- templates/workspaces.html.to | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/templates/workspaces.html.to b/templates/workspaces.html.to index 6e7e7931..21bbad7d 100644 --- a/templates/workspaces.html.to +++ b/templates/workspaces.html.to @@ -5,22 +5,23 @@ - - - + + + {% for w in workspaces %} - + - {% end %} From 34c570ab184ce659ab88d65f090f6a9e82ce506b Mon Sep 17 00:00:00 2001 From: Andrew Croce Date: Mon, 30 Jul 2018 16:09:55 -0400 Subject: [PATCH 03/43] EmptyState UI module --- atst/ui_modules.py | 9 ++++++ scss/components/_empty_state.scss | 36 ++++++++++++++++-------- templates/components/empty_state.html.to | 9 ++++++ 3 files changed, 42 insertions(+), 12 deletions(-) create mode 100644 templates/components/empty_state.html.to diff --git a/atst/ui_modules.py b/atst/ui_modules.py index 748ca980..96bdab07 100644 --- a/atst/ui_modules.py +++ b/atst/ui_modules.py @@ -46,3 +46,12 @@ class SidenavItem(UIModule): active=active, icon=icon, subnav=subnav) + +class EmptyState(UIModule): + def render(self, message, actionLabel, actionHref, icon=None): + return self.render_string( + "components/empty_state.html.to", + message=message, + actionLabel=actionLabel, + actionHref=actionHref, + icon=icon) diff --git a/scss/components/_empty_state.scss b/scss/components/_empty_state.scss index d11f5083..be8675ad 100644 --- a/scss/components/_empty_state.scss +++ b/scss/components/_empty_state.scss @@ -1,15 +1,27 @@ .empty-state { text-align: center; - padding-top: 10rem; - - p { - font-family: $font-sans; - font-weight: 300; - line-height: 10rem; - font-size: 44px; - line-height: 5rem; - margin: 0 auto; - padding-bottom: 2rem; - max-width: 40%; + padding: 6rem ($gap * 2) 0; + display: flex; + flex-direction: column; + align-items: center; + + @include media($medium-screen) { + padding: 8rem ($gap * 4) 0; } -} \ No newline at end of file + + .icon { + @include icon-size(50); + @include icon-color($color-gray-light); + } + + p { + @include h2; + line-height: 1.2; + max-width: 15em; + color: $color-gray; + + @include media($large-screen) { + @include h1; + } + } +} diff --git a/templates/components/empty_state.html.to b/templates/components/empty_state.html.to new file mode 100644 index 00000000..30246ddb --- /dev/null +++ b/templates/components/empty_state.html.to @@ -0,0 +1,9 @@ +
+

{{ message }}

+ + {% if icon %} + {% module Icon(icon) %} + {% end %} + + {{ actionLabel }} +
From 6973443f36937aa1251574d33f4246cf3ed6d2f3 Mon Sep 17 00:00:00 2001 From: Andrew Croce Date: Mon, 30 Jul 2018 16:10:28 -0400 Subject: [PATCH 04/43] Add conditional empty state modules to Requests, Workspace Members, and Workspace Projects --- templates/requests.html.to | 124 +++++++++++++++------------ templates/workspace_members.html.to | 118 +++++++++++-------------- templates/workspace_projects.html.to | 65 ++++++++------ 3 files changed, 163 insertions(+), 144 deletions(-) diff --git a/templates/requests.html.to b/templates/requests.html.to index 66dbcd9d..1cdd2433 100644 --- a/templates/requests.html.to +++ b/templates/requests.html.to @@ -1,5 +1,9 @@ {% extends "base.html.to" %} +{% block template_vars %} + {% set requestsEmpty = False %} +{% end %} + {% block modal %} {% if modalOpen() %} {% apply modal %} @@ -31,67 +35,79 @@ {% block content %} +{% if requestsEmpty %} -{% module Alert('Pending Financial Verification', - message="

Your next step is to create a Task Order (T.O.) associated with JEDI Cloud. Please consult a Contracting Officer (KO) or Contracting Officer Representative (COR) to help with this step.

" -) %} + {% module EmptyState( + 'There are currently no active requests for you to see.', + actionLabel='Create a new JEDI Cloud Request', + actionHref='/requests/new', + icon='document' + )%} -
+{% else %} - -
- - - -
+ {% module Alert('Pending Financial Verification', + message="

Your next step is to create a Task Order (T.O.) associated with JEDI Cloud. Please consult a Contracting Officer (KO) or Contracting Officer Representative (COR) to help with this step.

" + ) %} -
- - -
- +
-
-
Workspace NameWorkspace InfoActionsWorkspace NameTask OrderUsers
- {{ w['name'] }}
- Task Order: #{{ w['task_order']['number'] }} +
+ {{ w['name'] }}
- {{ w['user_count'] }}
Users + #{{ w['task_order']['number'] }} +
+ {{ w['user_count'] }}Users
- - - - - - - - - - - - {% for r in requests %} - - + +
+ + + +
+ +
+ + +
+ + +
+
Order IDRequest DateRequesterTotal AppsStatusActions
- {{ r['order_id'] }} - {% if r['is_new'] %}New -
+ + + + + + + + + + + + {% for r in requests %} + + + {% end %} + + + + + + {% end %} - - - - - - - {% end %} - -
Order IDRequest DateRequesterTotal AppsStatusActions
+ {{ r['order_id'] }} + {% if r['is_new'] %}New + {{ r['date'] }}{{ r['full_name'] }}{{ r['app_count'] }}{{ r['status'] }} + Download + Approval +
{{ r['date'] }}{{ r['full_name'] }}{{ r['app_count'] }}{{ r['status'] }} - Download - Approval -
+ + + - + +{% end %} {% end %} diff --git a/templates/workspace_members.html.to b/templates/workspace_members.html.to index a27a0b0b..53e70385 100644 --- a/templates/workspace_members.html.to +++ b/templates/workspace_members.html.to @@ -1,92 +1,78 @@ {% extends "base_workspace.html.to" %} +{% block template_vars %} + {% set membersEmpty = False %} +{% end %} + {% block workspace_content %} +{% if membersEmpty %} - +{% else %} -{#
-
-
- + + + +
+ + + + + + + + + + + {% for m in members %} + + + + + + + {% end %} + +
Name Status FlagStatusWorkspace Role
{{ m['first_name'] }} {{ m['last_name'] }}{% if m['num_projects'] == '0' %} No Project Access {% end %}{{ m['status'] }}{{ m['workspace_role'] }}
-
#} - - -
- - - - - - - - - - - {% for m in members %} - - - - - - - {% end %} - -
Name Status FlagStatusWorkspace Role
{{ m['first_name'] }} {{ m['last_name'] }}{% if m['num_projects'] == '0' %} No Project Access {% end %}{{ m['status'] }}{{ m['workspace_role'] }}
-
+{% end %} {% end %} diff --git a/templates/workspace_projects.html.to b/templates/workspace_projects.html.to index e4008c3c..620b8396 100644 --- a/templates/workspace_projects.html.to +++ b/templates/workspace_projects.html.to @@ -1,32 +1,49 @@ {% extends "base_workspace.html.to" %} +{% block template_vars %} + {% set projectsEmpty = False %} +{% end %} + {% block workspace_content %} -{% for project in projects %} -
-
-

{{ project['name'] }} ({{ len(project['environments'])}} environments)

- - {% module Icon('edit') %} - edit - -
- -
{% end %} {% end %} From 6db9db3f7decbabadf8ae4706061107ad845de26 Mon Sep 17 00:00:00 2001 From: Andrew Croce Date: Tue, 31 Jul 2018 12:07:25 -0400 Subject: [PATCH 05/43] remove dummy "empty" vars, just check if the arrays are empty --- templates/requests.html.to | 6 +----- templates/workspace_members.html.to | 6 +----- templates/workspace_projects.html.to | 6 +----- 3 files changed, 3 insertions(+), 15 deletions(-) diff --git a/templates/requests.html.to b/templates/requests.html.to index 1cdd2433..bd0c9e8c 100644 --- a/templates/requests.html.to +++ b/templates/requests.html.to @@ -1,9 +1,5 @@ {% extends "base.html.to" %} -{% block template_vars %} - {% set requestsEmpty = False %} -{% end %} - {% block modal %} {% if modalOpen() %} {% apply modal %} @@ -35,7 +31,7 @@ {% block content %} -{% if requestsEmpty %} +{% if not requests %} {% module EmptyState( 'There are currently no active requests for you to see.', diff --git a/templates/workspace_members.html.to b/templates/workspace_members.html.to index 53e70385..074628da 100644 --- a/templates/workspace_members.html.to +++ b/templates/workspace_members.html.to @@ -1,12 +1,8 @@ {% extends "base_workspace.html.to" %} -{% block template_vars %} - {% set membersEmpty = False %} -{% end %} - {% block workspace_content %} -{% if membersEmpty %} +{% if not members %} {% module EmptyState( 'There are currently no members in this Workspace.', diff --git a/templates/workspace_projects.html.to b/templates/workspace_projects.html.to index 620b8396..34b0933b 100644 --- a/templates/workspace_projects.html.to +++ b/templates/workspace_projects.html.to @@ -1,12 +1,8 @@ {% extends "base_workspace.html.to" %} -{% block template_vars %} - {% set projectsEmpty = False %} -{% end %} - {% block workspace_content %} -{% if projectsEmpty %} +{% if not projects %} {% module EmptyState( 'There are currently no projects set up for this Workspace.', From b14c490082de8aa7c7fb5eb7f4db86c7f7ae5b04 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Tue, 31 Jul 2018 14:44:09 -0400 Subject: [PATCH 06/43] Remove unnecessary XML --- atst/eda_client.py | 244 --------------------------------------------- 1 file changed, 244 deletions(-) diff --git a/atst/eda_client.py b/atst/eda_client.py index 09acd859..cebaad27 100644 --- a/atst/eda_client.py +++ b/atst/eda_client.py @@ -110,247 +110,3 @@ class EDAClient(EDAClientBase): # TODO: Fetch the contract XML and transform it into a dictionary. # https://docs.python.org/3.7/library/xml.etree.elementtree.html raise NotImplementedError() - - - -CONTRACT_XML = """ - -2.5 -DD 1155 - -3244871 - -00000431 -704331 - - - - - -Department of Defense -Delivery Order -70433119F2644 -Represented Contract - - -Department of Defense -Basic Ordering Agreement -W81K0419G0001 -Ordering Instrument - - -false -Original -false - -Firm Fixed Price - - - - -http://farsite.hill.af.mil/reghtml/regs/far2afmcfars/fardfars/far/52_220.htm#P810_149596 - - -FAR -52.222-50 -Combating Trafficking in Persons. -2015-05 - - - [ lots of text ] - - -
I
-
- -FAR -52.245-1 -Government Property. -2012-04 - - - [ lots of text ] - - -
I
-
-
- -2016-02-04 - -2016-01-25 - -DALE WOLFE - -Telephone -520-533-9132 - - - - - -Contractor -
- -0Z7K0 -808152482 - - -CACI TECHNOLOGIES, INC - - -6933 Gateway Ct -Manassas VA, 20109 - - - -
-
- -Contract Issuing Office -
- -704331 - - -FEMA DISTRIBUTION CENTER - - -3870 S. SIDE INDUSTRIAL CTR -ATLANTA GA, 30354 - - - -
- -GENE BARBER - -Telephone -(202) 646-2727 - - -
- -Contract Administrative Office -
- -704331 - - -FEMA DISTRIBUTION CENTER - - -3870 S. SIDE INDUSTRIAL CTR -ATLANTA GA, 30354 - - - -
-
- -Paying Office -
- -HQ0131 - - -DEFENSE FINANCE AND ACCOUNTING SVC - - -P.O. BOX 369016 -COLUMBUS OH, 43236 - - - -
-
- -Ship To -
- -S0302A - - -DCMA PHOENIX - - -40 NORTH CENTRAL AVE, STE 400 -TWO RENAISSANCE SQUARE -PHOENIX AZ, 85004 - - - -
-
- - -Header Only - Total Contract Value -192000.00 - - - - -Delivery Requested By - -2016-01-16 - - - - - -Defense Priorities Allocation System (DPAS) Priority Rating - -DO-A7 -
A
-
- - -Contractor -Origin (after Loading) - - -
- - - - - -CLIN -0001 - - - - -false - -Cost No Fee - - -Real Property -Radio Dishes -3 -false -Estimated -Each -64000.00 - -Manufacturer's Part Number -5L33M7730291DX081 - - - - - -Estimated Cost -192000.00 - - -Not to Exceed Amount (Funding) -200000.00 - - - - -
-
-""" From 106b87b96e7de6dcb3d75d53e6f31f9bb5b17e8e Mon Sep 17 00:00:00 2001 From: richard-dds Date: Tue, 31 Jul 2018 15:16:04 -0400 Subject: [PATCH 07/43] Allow for multiple date formats --- atst/forms/fields.py | 12 +++++++++++- tests/forms/test_fields.py | 25 +++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 tests/forms/test_fields.py diff --git a/atst/forms/fields.py b/atst/forms/fields.py index 3e9de703..6f733bd1 100644 --- a/atst/forms/fields.py +++ b/atst/forms/fields.py @@ -7,7 +7,17 @@ import pendulum class DateField(DateField): def _value(self): if self.data: - return pendulum.parse(self.data).date() + date_formats = [ + "YYYY-MM-DD", + "MM/DD/YYYY" + ] + for _format in date_formats: + try: + return pendulum.from_format(self.data, _format).date() + except (ValueError, pendulum.parsing.exceptions.ParserError): + pass + + raise ValueError("Unable to parse string {}".format(self.data)) else: return None diff --git a/tests/forms/test_fields.py b/tests/forms/test_fields.py new file mode 100644 index 00000000..ceece7c5 --- /dev/null +++ b/tests/forms/test_fields.py @@ -0,0 +1,25 @@ +import pytest +from wtforms import Form +import pendulum + +from atst.forms.fields import DateField + + +class MyForm(Form): + date = DateField() + + +def test_date_ie_format(): + form = MyForm(data={"date": "12/24/2018"}) + assert form.date._value() == pendulum.date(2018, 12, 24) + + +def test_date_sane_format(): + form = MyForm(data={"date": "2018-12-24"}) + assert form.date._value() == pendulum.date(2018, 12, 24) + + +def test_date_insane_format(): + form = MyForm(data={"date": "hello"}) + with pytest.raises(ValueError): + form.date._value() From 1db88a8191aae786792e8b6ce7ab1dc3e4ff562f Mon Sep 17 00:00:00 2001 From: Luis Cielak Date: Mon, 30 Jul 2018 17:26:07 -0400 Subject: [PATCH 08/43] Add member placeholder info --- atst/app.py | 1 + templates/member_edit.html.to | 85 +++++++++++++++++++ .../navigation/workspace_navigation.html.to | 15 +++- templates/workspace_members.html.to | 2 +- 4 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 templates/member_edit.html.to diff --git a/atst/app.py b/atst/app.py index dab66e8d..a94a93ec 100644 --- a/atst/app.py +++ b/atst/app.py @@ -122,6 +122,7 @@ def make_app(config, deps, **kwargs): ), url(r"/workspaces/(\S+)/projects", Workspace, {}, name="workspace_projects"), url(r"/workspaces/123456/projects/789/edit", Main, {"page": "project_edit"}, name="project_edit"), + url(r"/workspaces/123456/members/789/edit", Main, {"page": "member_edit"}, name="member_edit"), ] if not ENV == "production": diff --git a/templates/member_edit.html.to b/templates/member_edit.html.to new file mode 100644 index 00000000..d454152c --- /dev/null +++ b/templates/member_edit.html.to @@ -0,0 +1,85 @@ +{% extends "base_workspace.html.to" %} + +{% block template_vars %} +{% set is_new_member = False %} +{% set member_name = "Danny Knight" %} +{% set member_email = "knight@mil.gov" %} +{% set member_workspace_role = "Billing Auditor" %} +{% set member_id = "789" %} +{% end %} + +{% block workspace_content %} + +{% module Alert( + "UI Mock", + message="

Please note, this screen is a non-functional UI mockup.

", + level="info" + ) %} + + +
+
+

+ {% if is_new_member %} + Add new member + {% else %} + {{ member_name }} + {% end %} +

+
Workspace Role {{member_workspace_role}}
+
+
+ + +
+
+
+ +
+
+
+ +
+
+

Code.mil

+ revoke all access +
+
    +
  • + + Development + +
    + no accessset role +
    +
  • +
  • + + Production + +
    + Billingset role +
    +
  • + +
+
+ + + + {% end %} + diff --git a/templates/navigation/workspace_navigation.html.to b/templates/navigation/workspace_navigation.html.to index 21b48bb5..65a9525b 100644 --- a/templates/navigation/workspace_navigation.html.to +++ b/templates/navigation/workspace_navigation.html.to @@ -17,7 +17,20 @@ {% module SidenavItem( "Members", href=reverse_url('workspace_members', '123456'), - active=matchesPath('\/workspaces\/[A-Za-z0-9]*\/members') + active=matchesPath('\/workspaces\/[A-Za-z0-9]*\/members'), + subnav=[ + { + "label": "Add New Member", + "href": "", + "active": matchesPath('/workspaces/members/new'), + "icon": "plus" + }, + { + "label": "Editing Member", + "href": "", + "active": matchesPath('/workspaces/123456/members/789/edit') + } + ] )%} {% module SidenavItem( diff --git a/templates/workspace_members.html.to b/templates/workspace_members.html.to index 074628da..0f538b05 100644 --- a/templates/workspace_members.html.to +++ b/templates/workspace_members.html.to @@ -58,7 +58,7 @@ {% for m in members %} - {{ m['first_name'] }} {{ m['last_name'] }} + {{ m['first_name'] }} {{ m['last_name'] }} {% if m['num_projects'] == '0' %} No Project Access {% end %} {{ m['status'] }} {{ m['workspace_role'] }} From b4b99de7defd84c63304c1ff5a88321f3f0b71ef Mon Sep 17 00:00:00 2001 From: Luis Cielak Date: Tue, 31 Jul 2018 10:29:22 -0400 Subject: [PATCH 09/43] Style block list links --- scss/elements/_block_lists.scss | 19 +++++++-- scss/elements/_icon_link.scss | 4 ++ templates/member_edit.html.to | 4 +- templates/workspace_projects.html.to | 61 +++++++++++----------------- 4 files changed, 45 insertions(+), 43 deletions(-) diff --git a/scss/elements/_block_lists.scss b/scss/elements/_block_lists.scss index e7caed75..ac15f80d 100644 --- a/scss/elements/_block_lists.scss +++ b/scss/elements/_block_lists.scss @@ -54,14 +54,25 @@ .block-list { @include block-list; + + .icon-link { + margin: -$gap 0; + } + + .icon-link { + &:first-child { + margin-left: -$gap; + } + + &:last-child { + margin-right: -$gap; + } + + } } .block-list__header { @include block-list-header; - - .block-list__header__link { - @include icon-link; - } } .block-list__title { diff --git a/scss/elements/_icon_link.scss b/scss/elements/_icon_link.scss index 95e372ec..00daae3f 100644 --- a/scss/elements/_icon_link.scss +++ b/scss/elements/_icon_link.scss @@ -59,4 +59,8 @@ &.icon-link--large { @include icon-link-large; } + + &.icon-link--danger { + @include icon-link-color($color-red, $color-red-lightest); + } } diff --git a/templates/member_edit.html.to b/templates/member_edit.html.to index d454152c..e6f36673 100644 --- a/templates/member_edit.html.to +++ b/templates/member_edit.html.to @@ -48,7 +48,7 @@

Code.mil

- revoke all access + revoke all access
  • @@ -56,7 +56,7 @@ Development
    - no accessset role + no access set role
  • diff --git a/templates/workspace_projects.html.to b/templates/workspace_projects.html.to index 34b0933b..a9f1a8aa 100644 --- a/templates/workspace_projects.html.to +++ b/templates/workspace_projects.html.to @@ -2,44 +2,31 @@ {% block workspace_content %} -{% if not projects %} - - {% module EmptyState( - 'There are currently no projects set up for this Workspace.', - actionLabel='Create a new Project', - actionHref='/project/new', - icon='cloud' - )%} - -{% else %} - - {% for project in projects %} -
    -
    -

    {{ project['name'] }} ({{ len(project['environments'])}} environments)

    - - {% module Icon('edit') %} - edit - -
    - -
    - {% end %} +{% for project in projects %} +
    +
    +

    {{ project['name'] }} ({{ len(project['environments'])}} environments)

    + + {% module Icon('edit') %} + edit + +
    + +
    {% end %} {% end %} From 88c073e1b234c823ea93cc3e5d98d22eda0df6dc Mon Sep 17 00:00:00 2001 From: Luis Cielak Date: Tue, 31 Jul 2018 10:42:14 -0400 Subject: [PATCH 10/43] Add placeholder project item --- templates/member_edit.html.to | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/templates/member_edit.html.to b/templates/member_edit.html.to index e6f36673..ebd3416a 100644 --- a/templates/member_edit.html.to +++ b/templates/member_edit.html.to @@ -48,7 +48,7 @@

    Code.mil

    - revoke all access + revoke all access
  • -
+
+
+

Digital Dojo

+ no access +
+
+ +
  • + + Sandbox + +
    + no accessset role +
    +
  • Production From ccb38dc2c6809a7a485ab8caaab43db27b4be8f9 Mon Sep 17 00:00:00 2001 From: Luis Cielak Date: Tue, 31 Jul 2018 13:49:06 -0400 Subject: [PATCH 13/43] Add fields to header --- templates/member_edit.html.to | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/templates/member_edit.html.to b/templates/member_edit.html.to index 44b9be53..89031d6b 100644 --- a/templates/member_edit.html.to +++ b/templates/member_edit.html.to @@ -18,17 +18,19 @@
    -

    - {% if is_new_member %} - Add new member - {% else %} - {{ member_name }} - {% end %} -

    +

    {{ member_name }}

    Workspace Role {{member_workspace_role}}
    - DOD ID: {{ member_id }} -
    Email: {{ member_email }}
    - edit account details +
    + +
    DOD ID:
    +
    {{ member_id }}
    +
    + +
    Email:
    +
    {{ member_email }}
    +
    +
    + edit account details
    From c5848995507ca3425a015c2ba0694ca52247c456 Mon Sep 17 00:00:00 2001 From: Luis Cielak Date: Tue, 31 Jul 2018 14:17:14 -0400 Subject: [PATCH 14/43] Update search bar --- templates/member_edit.html.to | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/templates/member_edit.html.to b/templates/member_edit.html.to index 89031d6b..f7082b73 100644 --- a/templates/member_edit.html.to +++ b/templates/member_edit.html.to @@ -29,29 +29,24 @@
    Email:
    {{ member_email }}
    + edit account details - edit account details
  • - -
    -
    -
    - -
    +
    +
    -

    Code.mil

    +

    ▾ Code.mil

    revoke all access
      @@ -84,7 +79,7 @@
      -

      Digital Dojo

      +

      ▸ Digital Dojo

      no access
      From cfbf2efdf6956145d1805e2cf3be67c461267fbc Mon Sep 17 00:00:00 2001 From: Luis Cielak Date: Tue, 31 Jul 2018 14:27:33 -0400 Subject: [PATCH 15/43] Remove dl markup --- templates/member_edit.html.to | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/templates/member_edit.html.to b/templates/member_edit.html.to index f7082b73..f9125323 100644 --- a/templates/member_edit.html.to +++ b/templates/member_edit.html.to @@ -20,17 +20,12 @@

      {{ member_name }}

      Workspace Role {{member_workspace_role}}
      -
      - -
      DOD ID:
      -
      {{ member_id }}
      -
      - -
      Email:
      -
      {{ member_email }}
      -
      - edit account details -
      + + DOD ID: + {{ member_id }} + Email: + {{ member_email }} + edit account details
    From 727680c31f16e33ee267d17434fc8f076590d022 Mon Sep 17 00:00:00 2001 From: Luis Cielak Date: Tue, 31 Jul 2018 14:44:56 -0400 Subject: [PATCH 16/43] Replace ascii traingles with svgs --- static/icons/arrow-down.svg | 1 + static/icons/arrow-right.svg | 1 + templates/member_edit.html.to | 4 ++-- templates/styleguide.html.to | 2 ++ 4 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 static/icons/arrow-down.svg create mode 100644 static/icons/arrow-right.svg diff --git a/static/icons/arrow-down.svg b/static/icons/arrow-down.svg new file mode 100644 index 00000000..ec8fe0b9 --- /dev/null +++ b/static/icons/arrow-down.svg @@ -0,0 +1 @@ + diff --git a/static/icons/arrow-right.svg b/static/icons/arrow-right.svg new file mode 100644 index 00000000..524470fd --- /dev/null +++ b/static/icons/arrow-right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/templates/member_edit.html.to b/templates/member_edit.html.to index f9125323..3b744ad6 100644 --- a/templates/member_edit.html.to +++ b/templates/member_edit.html.to @@ -41,7 +41,7 @@
    -

    ▾ Code.mil

    +

    {% module Icon('arrow-down') %} Code.mil

    revoke all access
      @@ -74,7 +74,7 @@
      -

      ▸ Digital Dojo

      +

      {% module Icon('arrow-right') %} Digital Dojo

      no access
      diff --git a/templates/styleguide.html.to b/templates/styleguide.html.to index e90aefc9..1db19496 100644 --- a/templates/styleguide.html.to +++ b/templates/styleguide.html.to @@ -301,6 +301,8 @@ {% module Icon('link') %} 'link'    {% module Icon('ok') %} 'ok'    {% module Icon('checkmark') %} 'checkmark'    + {% module Icon('arrow-right') %} 'arrow-right'    + {% module Icon('arrow-down') %} 'arrow-down'   
    From cdd442c266bf0e26ce480b5b08705c9360456e3c Mon Sep 17 00:00:00 2001 From: Luis Cielak Date: Tue, 31 Jul 2018 16:04:17 -0400 Subject: [PATCH 17/43] Update member header layout --- scss/atat.scss | 1 + scss/sections/_member_edit.scss | 37 +++++++++++++++++++++++++++++++++ templates/member_edit.html.to | 27 +++++++++++++++--------- 3 files changed, 55 insertions(+), 10 deletions(-) create mode 100644 scss/sections/_member_edit.scss diff --git a/scss/atat.scss b/scss/atat.scss index f9e7f6aa..b4ae7797 100644 --- a/scss/atat.scss +++ b/scss/atat.scss @@ -33,3 +33,4 @@ @import 'sections/request_approval'; @import 'sections/projects_list'; @import 'sections/project_edit'; +@import 'sections/member_edit'; diff --git a/scss/sections/_member_edit.scss b/scss/sections/_member_edit.scss new file mode 100644 index 00000000..c868137a --- /dev/null +++ b/scss/sections/_member_edit.scss @@ -0,0 +1,37 @@ +.member-card { + @include grid-row; + padding: $gap*2; + justify-content: space-between; + + dl { + margin: 0; + + > div { + margin-bottom: $gap; + } + } + + dt { + font-weight: normal; + color: $color-gray; + } + + dd { + display: inline; + } + + .member-card__header { + display: flex; + flex-direction: column; + justify-content: space-between; + } + + .member-card__heading { + margin: 0; + @include h2; + } + + .member-card__details { + text-align: right; + } +} \ No newline at end of file diff --git a/templates/member_edit.html.to b/templates/member_edit.html.to index 3b744ad6..140e8a26 100644 --- a/templates/member_edit.html.to +++ b/templates/member_edit.html.to @@ -16,16 +16,23 @@ level="info" ) %} -
    -
    -

    {{ member_name }}

    -
    Workspace Role {{member_workspace_role}}
    - - DOD ID: - {{ member_id }} - Email: - {{ member_email }} - edit account details +
    +
    +

    {{ member_name }}

    +
    Workspace Role
    {{member_workspace_role}}
    +
    +
    +
    +
    +
    DOD ID:
    +
    {{ member_id }}
    +
    +
    +
    Email:
    +
    {{ member_email }}
    +
    +
    + edit account details
    From 1a6d42f385b3cbaa360c5aebe881e47c12c11463 Mon Sep 17 00:00:00 2001 From: Luis Cielak Date: Tue, 31 Jul 2018 16:16:43 -0400 Subject: [PATCH 18/43] Style lables and adjust list --- scss/elements/_block_lists.scss | 3 ++- scss/elements/_typography.scss | 8 ++------ scss/sections/_member_edit.scss | 4 ++++ templates/member_edit.html.to | 10 +++++----- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/scss/elements/_block_lists.scss b/scss/elements/_block_lists.scss index ac15f80d..6cb24d11 100644 --- a/scss/elements/_block_lists.scss +++ b/scss/elements/_block_lists.scss @@ -59,7 +59,8 @@ margin: -$gap 0; } - .icon-link { + .icon-link, + .label { &:first-child { margin-left: -$gap; } diff --git a/scss/elements/_typography.scss b/scss/elements/_typography.scss index 10f0846a..fcdc97d9 100644 --- a/scss/elements/_typography.scss +++ b/scss/elements/_typography.scss @@ -4,7 +4,7 @@ * @source https://github.com/uswds/uswds/blob/develop/src/stylesheets/elements/_typography.scss */ -* { + * { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } @@ -56,8 +56,4 @@ dl { > div { margin-bottom: $gap * 2; } -} - -.subtitle { - color: $color-gray; -} +} \ No newline at end of file diff --git a/scss/sections/_member_edit.scss b/scss/sections/_member_edit.scss index c868137a..aef2e042 100644 --- a/scss/sections/_member_edit.scss +++ b/scss/sections/_member_edit.scss @@ -33,5 +33,9 @@ .member-card__details { text-align: right; + + .icon-link { + margin: 0 -$gap; + } } } \ No newline at end of file diff --git a/templates/member_edit.html.to b/templates/member_edit.html.to index 140e8a26..24893bf2 100644 --- a/templates/member_edit.html.to +++ b/templates/member_edit.html.to @@ -19,7 +19,7 @@

    {{ member_name }}

    -
    Workspace Role
    {{member_workspace_role}}
    +
    Workspace Role
    {{member_workspace_role}}
    @@ -57,7 +57,7 @@ Development
    - no access set role + no access set role
  • @@ -65,7 +65,7 @@ Sandbox
    - no accessset role + no accessset role
  • @@ -73,7 +73,7 @@ Production
    - Billingset role + Billingset role
  • @@ -82,7 +82,7 @@

    {% module Icon('arrow-right') %} Digital Dojo

    - no access + no access
    From 6a5d3d57cf556c064b6a003cd628ae779ead2804 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Wed, 1 Aug 2018 10:51:14 -0400 Subject: [PATCH 19/43] Flask setup --- Pipfile | 13 +- Pipfile.lock | 196 ++++++++++++++++++++------- app.py | 12 +- atst/app.py | 290 ++++++++++++++++++++-------------------- atst/assets.py | 11 +- atst/database.py | 9 +- atst/handler.py | 3 +- atst/routes/__init__.py | 7 + 8 files changed, 318 insertions(+), 223 deletions(-) create mode 100644 atst/routes/__init__.py diff --git a/Pipfile b/Pipfile index 81cadd0d..4110ccbf 100644 --- a/Pipfile +++ b/Pipfile @@ -4,20 +4,23 @@ verify_ssl = true name = "pypi" [packages] -tornado = "==5.0.2" -webassets = "==0.12.1" -Unipath = "==1.1" +tornado = "*" +webassets = "*" +Unipath = "*" wtforms-tornado = "*" pendulum = "*" redis = "*" sqlalchemy = "*" alembic = "*" "psycopg2-binary" = "*" +flask = "*" +flask-sqlalchemy = "*" +flask-assets = "*" [dev-packages] bandit = "*" -pytest = "==3.6.0" -pytest-tornado = "==0.5.0" +pytest = "*" +pytest-tornado = "*" ipython = "*" ipdb = "*" pylint = "*" diff --git a/Pipfile.lock b/Pipfile.lock index d87dfa97..d6e5cf64 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "7cd87f2c2c42bc776a6aa6f72fcbb8b30d4e703e50b6480ce1c8ace6ae6dd0a4" + "sha256": "2ee6dd90ff3784e7b1781c680d690ac59118b4e3d72e8da3adf9e93d6e512bc7" }, "pipfile-spec": 6, "requires": { @@ -24,6 +24,49 @@ "index": "pypi", "version": "==1.0.0" }, + "click": { + "hashes": [ + "sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d", + "sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b" + ], + "version": "==6.7" + }, + "flask": { + "hashes": [ + "sha256:2271c0070dbcb5275fad4a82e29f23ab92682dc45f9dfbc22c02ba9b9322ce48", + "sha256:a080b744b7e345ccfcbc77954861cb05b3c63786e93f2b3875e0913d44b43f05" + ], + "index": "pypi", + "version": "==1.0.2" + }, + "flask-assets": { + "hashes": [ + "sha256:6031527b89fb3509d1581d932affa5a79dd348cfffb58d0aef99a43461d47847" + ], + "index": "pypi", + "version": "==0.12" + }, + "flask-sqlalchemy": { + "hashes": [ + "sha256:3bc0fac969dd8c0ace01b32060f0c729565293302f0c4269beed154b46bec50b", + "sha256:5971b9852b5888655f11db634e87725a9031e170f37c0ce7851cf83497f56e53" + ], + "index": "pypi", + "version": "==2.3.2" + }, + "itsdangerous": { + "hashes": [ + "sha256:cbb3fcf8d3e33df861709ecaf89d9e6629cff0a217bc2848f1b41cd30d360519" + ], + "version": "==0.24" + }, + "jinja2": { + "hashes": [ + "sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd", + "sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4" + ], + "version": "==2.10" + }, "mako": { "hashes": [ "sha256:4e02fde57bd4abb5ec400181e4c314f56ac3e49ba4fb8b0d50bba18cb27d25ae" @@ -38,19 +81,22 @@ }, "pendulum": { "hashes": [ - "sha256:0643d45824e6789b88187728337dfa6075a0233f6976c2abefba00d064156309", - "sha256:3cc271195d8054bec06f54ff7d56ea6c2e2b5ad5dd6b532d787b34d2cabe6a65", - "sha256:544e44d8a92954e5ef4db4fa8b662d3282f2ac7b7c2cbf4227dc193ba78b9e1e", - "sha256:846478ab5f7480b3d850a09e44fe03830d448633c84f0b1066615ff6c34293aa", - "sha256:8bb523f759daeecfc0649369f198cbeb27a6608347354f4f847d21d579003db6", - "sha256:a449142063100f1b3c1119453c7569667c9ba79897305a1c50ca83a8c790f1e4", - "sha256:b7ff156b3d7cccbdeeb63465578d9a4e6f57d463f6ff6d4474254208d08f8353", - "sha256:d8822a592bbc16576c44ec4625bff9187ed9b649d47714e4905a55adc5b25339", - "sha256:dd45c7b349faab69714df9835cdf8bf8bce50bf6fc471419d3b23ba33e1915a5", - "sha256:fac088b637b5db5a047a0e89194d8c3c9e9e9ce1665089240003bb7c05b92536" + "sha256:0ec5371949e147753661e1e98721273170638034dfceb578f29d69d93d3d474b", + "sha256:10ccdc8c6d004ba97883dd0f57503963ddf6cb83e849a16c4675ba18da657564", + "sha256:37bb54bcbb9d7fccd725f3fda69702e51ab3de9971b4c1c986505fbb3bc58bed", + "sha256:51803352e40778f914ff7af3494788b404260b415d9a9d607a8cf73e5e120994", + "sha256:5de295ca85761d9adf4020e6f3bed6eb933846ccf23b74e04b071f6d677f11a4", + "sha256:73f850265adcf0986fcc0af83ae9c8c5a7ca3c4a2525184110478a8bfd1a77b3", + "sha256:8fe289356322f6b0f4510082b4c412a1496a64054a37ae86b24411868a1901c6", + "sha256:c0401482dfa9fbd7005f2dfbf54ec61fd2c8130df37651ac2a3722d1f049ae4e", + "sha256:c358ee65ddb99c2b1bf301458e43ed09ff6d40465bcc9928265246912fad4d0f", + "sha256:d07962450e808556b3e6209a5830e2bbf8c7747129580c3b5b09e641f72617ab", + "sha256:dc05e6186c9c3b9969326aded9cba7a796744918581b25457f5148a5e3475d55", + "sha256:ee9466eea403e8e308c284d3055e285b97905a5ffb1566df0ef200b4f39c0f15", + "sha256:f7fa6220251a636112721e8158b9dd59018d818ec121047900934d80864eca62" ], "index": "pypi", - "version": "==2.0.2" + "version": "==2.0.3" }, "psycopg2-binary": { "hashes": [ @@ -106,7 +152,7 @@ "sha256:1d936da41ee06216d89fdc7ead1ee9a5da2811a8787515a976b646e110c3f622", "sha256:e4ef42e82b0b493c5849eed98b5ab49d6767caf982127e9a33167f1153b36cc5" ], - "markers": "python_version != '3.1.*' and python_version != '3.2.*' and python_version != '3.0.*' and python_version != '3.3.*' and python_version >= '2.7'", + "markers": "python_version != '3.0.*' and python_version != '3.1.*' and python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.3.*'", "version": "==2018.5" }, "redis": { @@ -133,14 +179,16 @@ }, "tornado": { "hashes": [ - "sha256:1b83d5c10550f2653380b4c77331d6f8850f287c4f67d7ce1e1c639d9222fbc7", - "sha256:408d129e9d13d3c55aa73f8084aa97d5f90ed84132e38d6932e63a67d5bec563", - "sha256:88ce0282cce70df9045e515f578c78f1ebc35dcabe1d70f800c3583ebda7f5f5", - "sha256:ba9fbb249ac5390bff8a1d6aa4b844fd400701069bda7d2e380dfe2217895101", - "sha256:c050089173c2e9272244bccfb6a8615fb9e53b79420a5551acfa76094ecc3111" + "sha256:1c0816fc32b7d31b98781bd8ebc7a9726d7dce67407dc353a2e66e697e138448", + "sha256:4f66a2172cb947387193ca4c2c3e19131f1c70fa8be470ddbbd9317fd0801582", + "sha256:5327ba1a6c694e0149e7d9126426b3704b1d9d520852a3e4aa9fc8fe989e4046", + "sha256:6a7e8657618268bb007646b9eae7661d0b57f13efc94faa33cd2588eae5912c9", + "sha256:a9b14804783a1d77c0bd6c66f7a9b1196cbddfbdf8bceb64683c5ae60bd1ec6f", + "sha256:c58757e37c4a3172949c99099d4d5106e4d7b63aa0617f9bb24bfbff712c7866", + "sha256:d8984742ce86c0855cccecd5c6f54a9f7532c983947cff06f3a0e2115b47f85c" ], "index": "pypi", - "version": "==5.0.2" + "version": "==5.1" }, "unipath": { "hashes": [ @@ -157,6 +205,13 @@ "index": "pypi", "version": "==0.12.1" }, + "werkzeug": { + "hashes": [ + "sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c", + "sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b" + ], + "version": "==0.14.1" + }, "wtforms": { "hashes": [ "sha256:0cdbac3e7f6878086c334aa25dc5a33869a3954e9d1e015130d65a69309b3b61", @@ -197,10 +252,10 @@ }, "astroid": { "hashes": [ - "sha256:0a0c484279a5f08c9bcedd6fa9b42e378866a7dcc695206b92d59dc9f2d9760d", - "sha256:218e36cf8d98a42f16214e8670819ce307fa707d1dcf7f9af84c7aede1febc7f" + "sha256:a48b57ede295c3188ef5c84273bc2a8eadc46e4cbb001eae0d49fb5d1fabbb19", + "sha256:d066cdeec5faeb51a4be5010da612680653d844b57afd86a5c8315f2f801b4cc" ], - "version": "==2.0.1" + "version": "==2.0.2" }, "atomicwrites": { "hashes": [ @@ -279,7 +334,7 @@ "sha256:0e9a1227a3a0f3297a485715e72ee6eb77081b17b629367042b586e38c03c867", "sha256:b4840807a94a3bad0217d6ed3f9b65a1cc6e1db1c99e1184673056ae2c0a4c4d" ], - "markers": "python_version != '3.2.*' and python_version != '3.0.*' and python_version >= '2.7' and python_version != '3.1.*'", + "markers": "python_version != '3.1.*' and python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.0.*'", "version": "==0.8.17" }, "gitdb2": { @@ -305,11 +360,11 @@ }, "ipython": { "hashes": [ - "sha256:a0c96853549b246991046f32d19db7140f5b1a644cc31f0dc1edc86713b7676f", - "sha256:eca537aa61592aca2fef4adea12af8e42f5c335004dfa80c78caf80e8b525e5c" + "sha256:007dcd929c14631f83daff35df0147ea51d1af420da303fd078343878bd5fb62", + "sha256:b0f2ef9eada4a68ef63ee10b6dde4f35c840035c50fd24265f8052c98947d5a4" ], "index": "pypi", - "version": "==6.4.0" + "version": "==6.5.0" }, "ipython-genutils": { "hashes": [ @@ -324,7 +379,7 @@ "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" ], - "markers": "python_version >= '2.7' and python_version != '3.1.*' and python_version != '3.3.*' and python_version != '3.0.*' and python_version != '3.2.*'", + "markers": "python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.1.*' and python_version != '3.0.*' and python_version != '3.2.*'", "version": "==4.3.4" }, "jedi": { @@ -377,11 +432,11 @@ }, "more-itertools": { "hashes": [ - "sha256:2b6b9893337bfd9166bee6a62c2b0c9fe7735dcf85948b387ec8cba30e85d8e8", - "sha256:6703844a52d3588f951883005efcf555e49566a48afd4db4e965d69b883980d3", - "sha256:a18d870ef2ffca2b8463c0070ad17b5978056f403fb64e3f15fe62a52db21cc0" + "sha256:c187a73da93e7a8acc0001572aebc7e3c69daf7bf6881a2cea10650bd4420092", + "sha256:c476b5d3a34e12d40130bc2f935028b5f636df8f372dc2c1c01dc19681b2039e", + "sha256:fcbfeaea0be121980e15bc97b3817b5202ca73d0eae185b4550cbfce2a3ebb3d" ], - "version": "==4.2.0" + "version": "==4.3.0" }, "parso": { "hashes": [ @@ -398,10 +453,10 @@ }, "pbr": { "hashes": [ - "sha256:754e766b4f4bad3aa68cfd532456298da1aa39375da8748392dbae90860d5f18", - "sha256:c6bddbad814f23c7faaf88d8a186e9965243cc6206a23361b73023648e645794" + "sha256:1b8be50d938c9bb75d0eaf7eda111eec1bf6dc88a62a6412e33bf077457e0f45", + "sha256:b486975c0cafb6beeb50ca0e17ba047647f229087bd74e37f4a7e2cac17d2caa" ], - "version": "==4.1.1" + "version": "==4.2.0" }, "pexpect": { "hashes": [ @@ -420,12 +475,11 @@ }, "pluggy": { "hashes": [ - "sha256:7f8ae7f5bdf75671a718d2daf0a64b7885f74510bcd98b1a0bb420eb9a9d0cff", - "sha256:d345c8fe681115900d6da8d048ba67c25df42973bda370783cd58826442dcd7c", - "sha256:e160a7fcf25762bb60efc7e171d4497ff1d8d2d75a3d0df7a21b76821ecbf5c5" + "sha256:6e3836e39f4d36ae72840833db137f7b7d35105079aee6ec4a62d9f80d594dd1", + "sha256:95eb8364a4708392bae89035f45341871286a333f749c3141c20573d2b3876e1" ], - "markers": "python_version >= '2.7' and python_version != '3.1.*' and python_version != '3.3.*' and python_version != '3.0.*' and python_version != '3.2.*'", - "version": "==0.6.0" + "markers": "python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.1.*' and python_version != '3.0.*' and python_version != '3.2.*'", + "version": "==0.7.1" }, "prompt-toolkit": { "hashes": [ @@ -447,7 +501,7 @@ "sha256:3fd59af7435864e1a243790d322d763925431213b6b8529c6ca71081ace3bbf7", "sha256:e31fb2767eb657cbde86c454f02e99cb846d3cd9d61b318525140214fdc0e98e" ], - "markers": "python_version >= '2.7' and python_version != '3.1.*' and python_version != '3.3.*' and python_version != '3.0.*' and python_version != '3.2.*'", + "markers": "python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.1.*' and python_version != '3.0.*' and python_version != '3.2.*'", "version": "==1.5.4" }, "pygments": { @@ -459,19 +513,19 @@ }, "pylint": { "hashes": [ - "sha256:2c90a24bee8fae22ac98061c896e61f45c5b73c2e0511a4bf53f99ba56e90434", - "sha256:454532779425098969b8f54ab0f056000b883909f69d05905ea114df886e3251" + "sha256:0edfec21270725c5aa8e8d8d06ef5666f766e0e748ed2f1ab23624727303b935", + "sha256:4cadcaa4f1fb19123d4baa758d9fbe6286c5b3aa513af6ea42a2d51d405db205" ], "index": "pypi", - "version": "==2.0.1" + "version": "==2.1.0" }, "pytest": { "hashes": [ - "sha256:39555d023af3200d004d09e51b4dd9fdd828baa863cded3fd6ba2f29f757ae2d", - "sha256:c76e93f3145a44812955e8d46cdd302d8a45fbfc7bf22be24fe231f9d8d8853a" + "sha256:8214ab8446104a1d0c17fbd218ec6aac743236c6ffbe23abc038e40213c60b88", + "sha256:e2b2c6e1560b8f9dc8dd600b0923183fbd68ba3d9bdecde04467be6dd296a384" ], "index": "pypi", - "version": "==3.6.0" + "version": "==3.7.0" }, "pytest-tornado": { "hashes": [ @@ -547,14 +601,16 @@ }, "tornado": { "hashes": [ - "sha256:1b83d5c10550f2653380b4c77331d6f8850f287c4f67d7ce1e1c639d9222fbc7", - "sha256:408d129e9d13d3c55aa73f8084aa97d5f90ed84132e38d6932e63a67d5bec563", - "sha256:88ce0282cce70df9045e515f578c78f1ebc35dcabe1d70f800c3583ebda7f5f5", - "sha256:ba9fbb249ac5390bff8a1d6aa4b844fd400701069bda7d2e380dfe2217895101", - "sha256:c050089173c2e9272244bccfb6a8615fb9e53b79420a5551acfa76094ecc3111" + "sha256:1c0816fc32b7d31b98781bd8ebc7a9726d7dce67407dc353a2e66e697e138448", + "sha256:4f66a2172cb947387193ca4c2c3e19131f1c70fa8be470ddbbd9317fd0801582", + "sha256:5327ba1a6c694e0149e7d9126426b3704b1d9d520852a3e4aa9fc8fe989e4046", + "sha256:6a7e8657618268bb007646b9eae7661d0b57f13efc94faa33cd2588eae5912c9", + "sha256:a9b14804783a1d77c0bd6c66f7a9b1196cbddfbdf8bceb64683c5ae60bd1ec6f", + "sha256:c58757e37c4a3172949c99099d4d5106e4d7b63aa0617f9bb24bfbff712c7866", + "sha256:d8984742ce86c0855cccecd5c6f54a9f7532c983947cff06f3a0e2115b47f85c" ], "index": "pypi", - "version": "==5.0.2" + "version": "==5.1" }, "traitlets": { "hashes": [ @@ -563,6 +619,42 @@ ], "version": "==4.3.2" }, + "typed-ast": { + "hashes": [ + "sha256:0948004fa228ae071054f5208840a1e88747a357ec1101c17217bfe99b299d58", + "sha256:10703d3cec8dcd9eef5a630a04056bbc898abc19bac5691612acba7d1325b66d", + "sha256:1f6c4bd0bdc0f14246fd41262df7dfc018d65bb05f6e16390b7ea26ca454a291", + "sha256:25d8feefe27eb0303b73545416b13d108c6067b846b543738a25ff304824ed9a", + "sha256:29464a177d56e4e055b5f7b629935af7f49c196be47528cc94e0a7bf83fbc2b9", + "sha256:2e214b72168ea0275efd6c884b114ab42e316de3ffa125b267e732ed2abda892", + "sha256:3e0d5e48e3a23e9a4d1a9f698e32a542a4a288c871d33ed8df1b092a40f3a0f9", + "sha256:519425deca5c2b2bdac49f77b2c5625781abbaf9a809d727d3a5596b30bb4ded", + "sha256:57fe287f0cdd9ceaf69e7b71a2e94a24b5d268b35df251a88fef5cc241bf73aa", + "sha256:668d0cec391d9aed1c6a388b0d5b97cd22e6073eaa5fbaa6d2946603b4871efe", + "sha256:68ba70684990f59497680ff90d18e756a47bf4863c604098f10de9716b2c0bdd", + "sha256:6de012d2b166fe7a4cdf505eee3aaa12192f7ba365beeefaca4ec10e31241a85", + "sha256:79b91ebe5a28d349b6d0d323023350133e927b4de5b651a8aa2db69c761420c6", + "sha256:8550177fa5d4c1f09b5e5f524411c44633c80ec69b24e0e98906dd761941ca46", + "sha256:898f818399cafcdb93cbbe15fc83a33d05f18e29fb498ddc09b0214cdfc7cd51", + "sha256:94b091dc0f19291adcb279a108f5d38de2430411068b219f41b343c03b28fb1f", + "sha256:a26863198902cda15ab4503991e8cf1ca874219e0118cbf07c126bce7c4db129", + "sha256:a8034021801bc0440f2e027c354b4eafd95891b573e12ff0418dec385c76785c", + "sha256:bc978ac17468fe868ee589c795d06777f75496b1ed576d308002c8a5756fb9ea", + "sha256:c05b41bc1deade9f90ddc5d988fe506208019ebba9f2578c622516fd201f5863", + "sha256:c9b060bd1e5a26ab6e8267fd46fc9e02b54eb15fffb16d112d4c7b1c12987559", + "sha256:edb04bdd45bfd76c8292c4d9654568efaedf76fe78eb246dde69bdb13b2dad87", + "sha256:f19f2a4f547505fe9072e15f6f4ae714af51b5a681a97f187971f50c283193b6" + ], + "version": "==1.1.0" + }, + "typing": { + "hashes": [ + "sha256:3a887b021a77b292e151afb75323dea88a7bc1b3dfa92176cff8e44c8b68bddf", + "sha256:b2c689d54e1144bbcfd191b0832980a21c2dbcf7b5ff7a66248a60c90e951eb8", + "sha256:d400a9344254803a2368533e4533a4200d21eb7b6b729c173bc38201a74db3f2" + ], + "version": "==3.6.4" + }, "watchdog": { "hashes": [ "sha256:7e65882adb7746039b6f3876ee174952f8eaaa34491ba34333ddf1fe35de4162" diff --git a/app.py b/app.py index b6d66f53..6d786f34 100755 --- a/app.py +++ b/app.py @@ -1,15 +1,11 @@ #!/usr/bin/env python -import tornado.ioloop - -from atst.app import make_app, make_deps, make_config +from atst.app import make_app, make_config config = make_config() -deps = make_deps(config) -app = make_app(config, deps) +app = make_app(config) if __name__ == "__main__": - port = int(config["default"]["PORT"]) - app.listen(port) + port = int(config["PORT"]) + app.run(port=port) print("Listening on http://localhost:%i" % port) - tornado.ioloop.IOLoop.current().start() diff --git a/atst/app.py b/atst/app.py index a94a93ec..5128117f 100644 --- a/atst/app.py +++ b/atst/app.py @@ -1,160 +1,166 @@ import os from configparser import ConfigParser -import tornado.web -from tornado.web import url from redis import StrictRedis +from flask import Flask +from unipath import Path -from atst.handlers.main import Main -from atst.handlers.root import Root -from atst.handlers.login_redirect import LoginRedirect -from atst.handlers.workspaces import Workspaces -from atst.handlers.workspace import Workspace -from atst.handlers.workspace_members import WorkspaceMembers -from atst.handlers.request import Request -from atst.handlers.request_financial_verification import RequestFinancialVerification -from atst.handlers.request_new import RequestNew -from atst.handlers.request_submit import RequestsSubmit -from atst.handlers.dev import Dev -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 -from atst.database import make_db +from atst.database import db +from atst.assets import assets +from atst.routes import bp ENV = os.getenv("TORNADO_ENV", "dev") -def make_app(config, deps, **kwargs): +def make_app(config): - routes = [ - url(r"/", Root, {"page": "root"}, name="root"), - url( - r"/login-redirect", - LoginRedirect, - { - "sessions": deps["sessions"], - "authnid_client": deps["authnid_client"], - "db_session": deps["db_session"], - }, - name="login_redirect", - ), - url(r"/home", Main, {"page": "home"}, name="home"), - url(r"/styleguide", Main, {"page": "styleguide"}, name="styleguide"), - url( - r"/workspaces/blank", - Main, - {"page": "workspaces_blank"}, - name="workspaces_blank", - ), - url( - r"/workspaces", - Workspaces, - {"page": "workspaces", "db_session": deps["db_session"]}, - name="workspaces", - ), - url( - r"/requests", - Request, - {"page": "requests", "db_session": deps["db_session"]}, - name="requests", - ), - url( - r"/requests/new", - RequestNew, - { - "page": "requests_new", - "db_session": deps["db_session"], - }, - name="request_new", - ), - url( - r"/requests/new/([0-9])", - RequestNew, - { - "page": "requests_new", - "db_session": deps["db_session"], - }, - name="request_form_new", - ), - url( - r"/requests/new/([0-9])/(\S+)", - RequestNew, - { - "page": "requests_new", - "db_session": deps["db_session"], - }, - name="request_form_update", - ), - url( - r"/requests/submit/(\S+)", - RequestsSubmit, - {"db_session": deps["db_session"]}, - name="requests_submit", - ), - # Dummy request/approval screen - url( - r"/request/approval", - Main, - {"page": "request_approval"}, - name="request_approval", - ), - url( - r"/requests/verify/(\S+)", - RequestFinancialVerification, - { - "page": "financial_verification", - "db_session": deps["db_session"], - }, - name="financial_verification", - ), - url( - r"/requests/financial_verification_submitted", - Main, - {"page": "requests/financial_verification_submitted"}, - name="financial_verification_submitted", - ), - url(r"/users", Main, {"page": "users"}, name="users"), - url(r"/reports", Main, {"page": "reports"}, name="reports"), - url(r"/calculator", Main, {"page": "calculator"}, name="calculator"), - url( - r"/workspaces/(\S+)/members", WorkspaceMembers, {}, name="workspace_members" - ), - url(r"/workspaces/(\S+)/projects", Workspace, {}, name="workspace_projects"), - url(r"/workspaces/123456/projects/789/edit", Main, {"page": "project_edit"}, name="project_edit"), - url(r"/workspaces/123456/members/789/edit", Main, {"page": "member_edit"}, name="member_edit"), - ] + parent_dir = Path().parent - if not ENV == "production": - routes += [ - url( - r"/login-dev", - Dev, - { - "action": "login", - "sessions": deps["sessions"], - "db_session": deps["db_session"], - }, - name="dev-login", - ) - ] - - app = tornado.web.Application( - routes, - login_url="/", - template_path=home.child("templates"), - static_path=home.child("static"), - cookie_secret=config["default"]["COOKIE_SECRET"], - debug=config["default"].getboolean("DEBUG"), - ui_modules=ui_modules, - ui_methods=ui_methods, - **kwargs + app = Flask( + __name__, + template_folder=parent_dir.child("templates").absolute(), + static_folder=parent_dir.child("static").absolute() ) - app.config = config - app.sessions = deps["sessions"] + app.config.update(config) + + db.init_app(app) + assets.init_app(app) + + app.register_blueprint(bp) + return app +# def make_app(config, deps, **kwargs): +# routes = [ +# url(r"/", Root, {"page": "root"}, name="root"), +# url( +# r"/login-redirect", +# LoginRedirect, +# { +# "sessions": deps["sessions"], +# "authnid_client": deps["authnid_client"], +# "db_session": deps["db_session"], +# }, +# name="login_redirect", +# ), +# url(r"/home", Main, {"page": "home"}, name="home"), +# url(r"/styleguide", Main, {"page": "styleguide"}, name="styleguide"), +# url( +# r"/workspaces/blank", +# Main, +# {"page": "workspaces_blank"}, +# name="workspaces_blank", +# ), +# url( +# r"/workspaces", +# Workspaces, +# {"page": "workspaces", "db_session": deps["db_session"]}, +# name="workspaces", +# ), +# url( +# r"/requests", +# Request, +# {"page": "requests", "db_session": deps["db_session"]}, +# name="requests", +# ), +# url( +# r"/requests/new", +# RequestNew, +# { +# "page": "requests_new", +# "db_session": deps["db_session"], +# }, +# name="request_new", +# ), +# url( +# r"/requests/new/([0-9])", +# RequestNew, +# { +# "page": "requests_new", +# "db_session": deps["db_session"], +# }, +# name="request_form_new", +# ), +# url( +# r"/requests/new/([0-9])/(\S+)", +# RequestNew, +# { +# "page": "requests_new", +# "db_session": deps["db_session"], +# }, +# name="request_form_update", +# ), +# url( +# r"/requests/submit/(\S+)", +# RequestsSubmit, +# {"db_session": deps["db_session"]}, +# name="requests_submit", +# ), +# # Dummy request/approval screen +# url( +# r"/request/approval", +# Main, +# {"page": "request_approval"}, +# name="request_approval", +# ), +# url( +# r"/requests/verify/(\S+)", +# RequestFinancialVerification, +# { +# "page": "financial_verification", +# "db_session": deps["db_session"], +# }, +# name="financial_verification", +# ), +# url( +# r"/requests/financial_verification_submitted", +# Main, +# {"page": "requests/financial_verification_submitted"}, +# name="financial_verification_submitted", +# ), +# url(r"/users", Main, {"page": "users"}, name="users"), +# url(r"/reports", Main, {"page": "reports"}, name="reports"), +# url(r"/calculator", Main, {"page": "calculator"}, name="calculator"), +# url( +# r"/workspaces/(\S+)/members", WorkspaceMembers, {}, name="workspace_members" +# ), +# url(r"/workspaces/(\S+)/projects", Workspace, {}, name="workspace_projects"), +# url(r"/workspaces/123456/projects/789/edit", Main, {"page": "project_edit"}, name="project_edit"), +# url(r"/workspaces/123456/members/789/edit", Main, {"page": "member_edit"}, name="member_edit"), +# ] + +# if not ENV == "production": +# routes += [ +# url( +# r"/login-dev", +# Dev, +# { +# "action": "login", +# "sessions": deps["sessions"], +# "db_session": deps["db_session"], +# }, +# name="dev-login", +# ) +# ] + +# app = tornado.web.Application( +# routes, +# login_url="/", +# template_path=home.child("templates"), +# static_path=home.child("static"), +# cookie_secret=config["default"]["COOKIE_SECRET"], +# debug=config["default"].getboolean("DEBUG"), +# ui_modules=ui_modules, +# ui_methods=ui_methods, +# **kwargs +# ) +# app.config = config +# app.sessions = deps["sessions"] +# return app + + def make_deps(config): # we do not want to do SSL verify services in test and development validate_cert = ENV == "production" @@ -206,4 +212,4 @@ def make_config(): ) config.set("default", "DATABASE_URI", database_uri) - return config + return config["default"] diff --git a/atst/assets.py b/atst/assets.py index 7f723f75..bed898ac 100644 --- a/atst/assets.py +++ b/atst/assets.py @@ -1,16 +1,13 @@ -from webassets import Environment, Bundle +from flask_assets import Environment, Bundle from atst.home import home -environment = Environment( - directory=home.child("scss"), - url="/static" -) +assets = Environment() css = Bundle( - "atat.scss", + "../scss/atat.scss", filters="scss", output="../static/assets/out.%(version)s.css", depends=("**/*.scss"), ) -environment.register("css", css) +assets.register("css", css) diff --git a/atst/database.py b/atst/database.py index 6191f62e..f0b13d6f 100644 --- a/atst/database.py +++ b/atst/database.py @@ -1,8 +1,3 @@ -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker, scoped_session +from flask_sqlalchemy import SQLAlchemy - -def make_db(config): - engine = create_engine(config['default']['DATABASE_URI']) - session = scoped_session(sessionmaker(bind=engine)) - return session +db = SQLAlchemy() diff --git a/atst/handler.py b/atst/handler.py index bcbcf879..bc931a8d 100644 --- a/atst/handler.py +++ b/atst/handler.py @@ -1,9 +1,8 @@ import tornado.web -from atst.assets import environment from atst.sessions import SessionNotFoundError from atst.domain.users import Users -helpers = {"assets": environment} +helpers = {"assets": None} class BaseHandler(tornado.web.RequestHandler): diff --git a/atst/routes/__init__.py b/atst/routes/__init__.py new file mode 100644 index 00000000..c6b03baf --- /dev/null +++ b/atst/routes/__init__.py @@ -0,0 +1,7 @@ +from flask import Blueprint, render_template + +bp = Blueprint("atst", __name__) + +@bp.route("/") +def home(): + return render_template("home.html") From 29c68151de9eb65253bbda163cbfe020bbfb91cc Mon Sep 17 00:00:00 2001 From: richard-dds Date: Wed, 1 Aug 2018 11:22:21 -0400 Subject: [PATCH 20/43] Convert ui methods into Flask globals --- atst/app.py | 31 +++++++++++++++++++++++++-- atst/routes/__init__.py | 2 +- templates/{base.html.to => base.html} | 22 +++++++++---------- templates/{home.html.to => home.html} | 4 ++-- 4 files changed, 42 insertions(+), 17 deletions(-) rename templates/{base.html.to => base.html} (60%) rename templates/{home.html.to => home.html} (68%) diff --git a/atst/app.py b/atst/app.py index 5128117f..f4cfe183 100644 --- a/atst/app.py +++ b/atst/app.py @@ -1,7 +1,8 @@ import os +import re from configparser import ConfigParser from redis import StrictRedis -from flask import Flask +from flask import Flask, request, g from unipath import Path from atst.api_client import ApiClient @@ -24,6 +25,8 @@ def make_app(config): ) app.config.update(config) + make_flask_callbacks(app) + db.init_app(app) assets.init_app(app) @@ -32,6 +35,21 @@ def make_app(config): return app +def make_flask_callbacks(app): + @app.before_request + def set_globals(): + g.navigationContext = 'workspace' if re.match('\/workspaces\/[A-Za-z0-9]*', request.url) else 'global' + g.dev = os.getenv("TORNADO_ENV", "dev") == "dev" + g.matchesPath = lambda href: re.match('^'+href, request.url) + g.modalOpen = request.args.get("modal", False) + + # TODO: Make me a macro + def modal(self, body): + return self.render_string( + "components/modal.html.to", + body=body) + + # def make_app(config, deps, **kwargs): # routes = [ # url(r"/", Root, {"page": "root"}, name="root"), @@ -180,6 +198,15 @@ def make_deps(config): ), } +def map_config(config): + return { + "ENV": config["default"]["ENVIRONMENT"], + "DEBUG": config["default"]["DEBUG"], + "PORT": int(config["default"]["PORT"]), + "SQLALCHEMY_DATABASE_URI": config["default"]["DATABASE_URI"], + "SQLALCHEMY_TRACK_MODIFICATIONS": False, + **config["default"] + } def make_config(): BASE_CONFIG_FILENAME = os.path.join(os.path.dirname(__file__), "../config/base.ini") @@ -212,4 +239,4 @@ def make_config(): ) config.set("default", "DATABASE_URI", database_uri) - return config["default"] + return map_config(config) diff --git a/atst/routes/__init__.py b/atst/routes/__init__.py index c6b03baf..93fde8e2 100644 --- a/atst/routes/__init__.py +++ b/atst/routes/__init__.py @@ -1,4 +1,4 @@ -from flask import Blueprint, render_template +from flask import Blueprint, render_template, g bp = Blueprint("atst", __name__) diff --git a/templates/base.html.to b/templates/base.html similarity index 60% rename from templates/base.html.to rename to templates/base.html index 3f0d5995..58c804cb 100644 --- a/templates/base.html.to +++ b/templates/base.html @@ -1,21 +1,21 @@ {# TODO: set this context elsewhere #} {# set context='workspace' #} -{% set context=navigationContext() %} +{% set context=g.navigationContext %} - {% block title %}JEDI{% end %} - {% for url in assets['css'].urls() %} - - {% end %} + {% block title %}JEDI{% endblock %} + {% assets "css" %} + + {% endassets %} - + - {% block template_vars %}{% end %} + {% block template_vars %}{% endblock %} {% include 'navigation/topbar.html.to' %} @@ -23,18 +23,16 @@ {% include 'navigation/global_navigation.html.to' %}
    - {% block sidenav %}{% end %} + {% block sidenav %}{% endblock %} {% block content %} these are not the droids you are looking for - {% end %} + {% endblock %}
    {% include 'footer.html.to' %} - {% block modal %}{% end %} + {% block modal %}{% endblock %} - - diff --git a/templates/home.html.to b/templates/home.html similarity index 68% rename from templates/home.html.to rename to templates/home.html index eb789f28..0df70037 100644 --- a/templates/home.html.to +++ b/templates/home.html @@ -1,4 +1,4 @@ -{% extends "base.html.to" %} +{% extends "base.html" %} {% block content %} @@ -8,7 +8,7 @@ -{% end %} +{% endblock %} From 0976aed77869b906fbad79c6a77d6a84f2039d11 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Wed, 1 Aug 2018 11:40:07 -0400 Subject: [PATCH 21/43] home.html is rendering - Converted Icon and SidenavItem into macros - Setting a mock user on g.current_user --- atst/app.py | 6 ++++ templates/components.html | 32 +++++++++++++++++++ .../navigation/global_navigation.html.to | 25 ++++++++------- templates/navigation/topbar.html.to | 9 +++--- 4 files changed, 56 insertions(+), 16 deletions(-) create mode 100644 templates/components.html diff --git a/atst/app.py b/atst/app.py index f4cfe183..23ad724b 100644 --- a/atst/app.py +++ b/atst/app.py @@ -42,6 +42,12 @@ def make_flask_callbacks(app): g.dev = os.getenv("TORNADO_ENV", "dev") == "dev" g.matchesPath = lambda href: re.match('^'+href, request.url) g.modalOpen = request.args.get("modal", False) + g.current_user = { + "id": "cce17030-4109-4719-b958-ed109dbb87c8", + "first_name": "Amanda", + "last_name": "Adamson", + "atat_role": "default" + } # TODO: Make me a macro def modal(self, body): diff --git a/templates/components.html b/templates/components.html new file mode 100644 index 00000000..06d487ea --- /dev/null +++ b/templates/components.html @@ -0,0 +1,32 @@ +{% macro Icon(name, classes="") -%} + {% autoescape false %} + + {% endautoescape %} +{%- endmacro %} + +{% macro SidenavItem(label, href, active=False, icon=None, subnav=None) -%} +
  • + + {% if icon %} + {{ Icon(icon, classes="sidenav__link-icon") }} + {% endif %} + + {{label}} + + + {% if subnav and active %} + + {% endif %} +
  • +{%- endmacro %} diff --git a/templates/navigation/global_navigation.html.to b/templates/navigation/global_navigation.html.to index 60991d47..594bf574 100644 --- a/templates/navigation/global_navigation.html.to +++ b/templates/navigation/global_navigation.html.to @@ -1,24 +1,25 @@ +{% from "components.html" import SidenavItem %} diff --git a/templates/navigation/topbar.html.to b/templates/navigation/topbar.html.to index 9af26690..10fc0c6d 100644 --- a/templates/navigation/topbar.html.to +++ b/templates/navigation/topbar.html.to @@ -1,18 +1,19 @@ +{% from "components.html" import Icon %}
    From 3a53fc122d4b832a3c67e75c77190efaae8ea58c Mon Sep 17 00:00:00 2001 From: dandds Date: Wed, 1 Aug 2018 13:15:07 -0400 Subject: [PATCH 22/43] switch workspace routes and templates to Flask and Jinja --- atst/app.py | 2 ++ atst/routes/workspaces.py | 31 ++++++++++++++++++ ..._workspace.html.to => base_workspace.html} | 6 ++-- templates/components.html | 12 +++++++ .../navigation/workspace_navigation.html.to | 32 ++++++++++--------- ...members.html.to => workspace_members.html} | 16 ++++++---- ...ojects.html.to => workspace_projects.html} | 16 ++++++---- .../{workspaces.html.to => workspaces.html} | 6 ++-- 8 files changed, 86 insertions(+), 35 deletions(-) create mode 100644 atst/routes/workspaces.py rename templates/{base_workspace.html.to => base_workspace.html} (70%) rename templates/{workspace_members.html.to => workspace_members.html} (92%) rename templates/{workspace_projects.html.to => workspace_projects.html} (72%) rename templates/{workspaces.html.to => workspaces.html} (91%) diff --git a/atst/app.py b/atst/app.py index 23ad724b..087277fc 100644 --- a/atst/app.py +++ b/atst/app.py @@ -10,6 +10,7 @@ from atst.sessions import RedisSessions from atst.database import db from atst.assets import assets from atst.routes import bp +from atst.routes.workspaces import bp as workspace_routes ENV = os.getenv("TORNADO_ENV", "dev") @@ -31,6 +32,7 @@ def make_app(config): assets.init_app(app) app.register_blueprint(bp) + app.register_blueprint(workspace_routes) return app diff --git a/atst/routes/workspaces.py b/atst/routes/workspaces.py new file mode 100644 index 00000000..1f3b47f2 --- /dev/null +++ b/atst/routes/workspaces.py @@ -0,0 +1,31 @@ +from flask import Blueprint, render_template + +from atst.domain.workspaces import Projects, Members +from atst.database import db + +bp = Blueprint("workspaces", __name__) + +mock_workspaces = [ + { + "name": "Unclassified IaaS and PaaS for Defense Digital Service (DDS)", + "id": "5966187a-eff9-44c3-aa15-4de7a65ac7ff", + "task_order": {"number": 123456}, + "user_count": 23, + } +] + +@bp.route("/workspaces") +def workspaces(): + return render_template("workspaces.html", page=5, workspaces=mock_workspaces) + +@bp.route("/workspaces//projects") +def workspace_projects(workspace_id): + projects_repo = Projects() + projects = projects_repo.get_many(workspace_id) + return render_template("workspace_projects.html", workspace_id=workspace_id, projects=projects) + +@bp.route("/workspaces//members") +def workspace_members(workspace_id): + members_repo = Members() + members = members_repo.get_many(workspace_id) + return render_template("workspace_members.html", workspace_id=workspace_id, members=members) diff --git a/templates/base_workspace.html.to b/templates/base_workspace.html similarity index 70% rename from templates/base_workspace.html.to rename to templates/base_workspace.html index 7803ace8..0ccad5ba 100644 --- a/templates/base_workspace.html.to +++ b/templates/base_workspace.html @@ -1,4 +1,4 @@ -{% extends "base.html.to" %} +{% extends "base.html" %} {% block content %} @@ -8,8 +8,8 @@
    - {% block workspace_content %}{% end %} + {% block workspace_content %}{% endblock %}
    -{% end %} +{% endblock %} diff --git a/templates/components.html b/templates/components.html index 06d487ea..0bc83991 100644 --- a/templates/components.html +++ b/templates/components.html @@ -30,3 +30,15 @@ {% endif %} {%- endmacro %} + +{% macro EmptyState(self, message, actionLabel, actionHref, icon=None) -%} +
    +

    {{ message }}

    + + {% if icon %} + {{ Icon(icon) }} + {% endif %} + + {{ actionLabel }} +
    +{%- endmacro %} diff --git a/templates/navigation/workspace_navigation.html.to b/templates/navigation/workspace_navigation.html.to index 65a9525b..d516c1b0 100644 --- a/templates/navigation/workspace_navigation.html.to +++ b/templates/navigation/workspace_navigation.html.to @@ -1,42 +1,44 @@ +{% from "components.html" import SidenavItem %} + diff --git a/templates/workspace_members.html.to b/templates/workspace_members.html similarity index 92% rename from templates/workspace_members.html.to rename to templates/workspace_members.html index 0f538b05..989d1d0e 100644 --- a/templates/workspace_members.html.to +++ b/templates/workspace_members.html @@ -1,15 +1,17 @@ -{% extends "base_workspace.html.to" %} +{% from "components.html" import EmptyState %} + +{% extends "base_workspace.html" %} {% block workspace_content %} {% if not members %} - {% module EmptyState( + {{ EmptyState( 'There are currently no members in this Workspace.', actionLabel='Invite a new Member', actionHref='/members/new', icon='avatar' - )%} + )}} {% else %} @@ -59,17 +61,17 @@ {% for m in members %} {{ m['first_name'] }} {{ m['last_name'] }} - {% if m['num_projects'] == '0' %} No Project Access {% end %} + {% if m['num_projects'] == '0' %} No Project Access {% endif %} {{ m['status'] }} {{ m['workspace_role'] }} - {% end %} + {% endfor %}
    -{% end %} +{% endif %} -{% end %} +{% endblock %} diff --git a/templates/workspace_projects.html.to b/templates/workspace_projects.html similarity index 72% rename from templates/workspace_projects.html.to rename to templates/workspace_projects.html index a9f1a8aa..91826bb6 100644 --- a/templates/workspace_projects.html.to +++ b/templates/workspace_projects.html @@ -1,13 +1,15 @@ -{% extends "base_workspace.html.to" %} +{% from "components.html" import Icon %} + +{% extends "base_workspace.html" %} {% block workspace_content %} {% for project in projects %}
    -

    {{ project['name'] }} ({{ len(project['environments'])}} environments)

    +

    {{ project['name'] }} ({{ project['environments']|length }} environments)

    - {% module Icon('edit') %} + {{ Icon('edit') }} edit
    @@ -15,7 +17,7 @@ {% for environment in project['environments'] %}
  • - {% module Icon('link') %} + {{ Icon('link') }} {{ environment["name"]}} @@ -24,10 +26,10 @@ members
  • - {% end %} + {% endfor %} -{% end %} +{% endfor %} -{% end %} +{% endblock %} diff --git a/templates/workspaces.html.to b/templates/workspaces.html similarity index 91% rename from templates/workspaces.html.to rename to templates/workspaces.html index 21bbad7d..5f407766 100644 --- a/templates/workspaces.html.to +++ b/templates/workspaces.html @@ -1,4 +1,4 @@ -{% extends "base.html.to" %} +{% extends "base.html" %} {% block content %}
    @@ -23,9 +23,9 @@ {{ w['user_count'] }}Users - {% end %} + {% endfor %}
    -{% end %} +{% endblock %} From 4ee662665e877ca125c44d72d250f2a22e91ac03 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Wed, 1 Aug 2018 14:17:43 -0400 Subject: [PATCH 23/43] Requests index rendering --- Pipfile | 1 + Pipfile.lock | 10 +++- atst/app.py | 9 +++- atst/domain/requests.py | 45 ++++++++-------- atst/routes/__init__.py | 3 ++ atst/routes/requests.py | 52 +++++++++++++++++++ templates/components.html | 50 +++++++++++++++++- templates/{requests.html.to => requests.html} | 36 +++++++------ 8 files changed, 164 insertions(+), 42 deletions(-) create mode 100644 atst/routes/requests.py rename templates/{requests.html.to => requests.html} (81%) diff --git a/Pipfile b/Pipfile index 4110ccbf..620553f1 100644 --- a/Pipfile +++ b/Pipfile @@ -16,6 +16,7 @@ alembic = "*" flask = "*" flask-sqlalchemy = "*" flask-assets = "*" +flask-session = "*" [dev-packages] bandit = "*" diff --git a/Pipfile.lock b/Pipfile.lock index d6e5cf64..d55c6fcf 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "2ee6dd90ff3784e7b1781c680d690ac59118b4e3d72e8da3adf9e93d6e512bc7" + "sha256": "f097384512537988c799b892830b52e78bcc19133327213e9c6e2876210d62d3" }, "pipfile-spec": 6, "requires": { @@ -46,6 +46,14 @@ "index": "pypi", "version": "==0.12" }, + "flask-session": { + "hashes": [ + "sha256:a31c27e0c3287f00c825b3d9625aba585f4df4cccedb1e7dd5a69a215881a731", + "sha256:b9b32126bfc52c3169089f2ed9a40e34b589527bda48b633428e07d39d9c8792" + ], + "index": "pypi", + "version": "==0.3.1" + }, "flask-sqlalchemy": { "hashes": [ "sha256:3bc0fac969dd8c0ace01b32060f0c729565293302f0c4269beed154b46bec50b", diff --git a/atst/app.py b/atst/app.py index 087277fc..7dac78b7 100644 --- a/atst/app.py +++ b/atst/app.py @@ -2,15 +2,18 @@ import os import re from configparser import ConfigParser from redis import StrictRedis -from flask import Flask, request, g +from flask import Flask, request, g, session from unipath import Path from atst.api_client import ApiClient from atst.sessions import RedisSessions from atst.database import db from atst.assets import assets + from atst.routes import bp from atst.routes.workspaces import bp as workspace_routes +from atst.routes.requests import requests_bp + ENV = os.getenv("TORNADO_ENV", "dev") @@ -33,6 +36,7 @@ def make_app(config): app.register_blueprint(bp) app.register_blueprint(workspace_routes) + app.register_blueprint(requests_bp) return app @@ -48,7 +52,8 @@ def make_flask_callbacks(app): "id": "cce17030-4109-4719-b958-ed109dbb87c8", "first_name": "Amanda", "last_name": "Adamson", - "atat_role": "default" + "atat_role": "default", + "atat_permissions": [] } # TODO: Make me a macro diff --git a/atst/domain/requests.py b/atst/domain/requests.py index aa37b932..a513d660 100644 --- a/atst/domain/requests.py +++ b/atst/domain/requests.py @@ -4,6 +4,8 @@ from sqlalchemy.orm.exc import NoResultFound from sqlalchemy.orm.attributes import flag_modified from atst.models import Request, RequestStatusEvent +from atst.database import db + from .exceptions import NotFoundError @@ -28,67 +30,68 @@ def deep_merge(source, destination: dict): class Requests(object): AUTO_APPROVE_THRESHOLD = 1000000 - def __init__(self, db_session): - self.db_session = db_session - - def create(self, creator_id, body): + @classmethod + def create(cls, creator_id, body): request = Request(creator=creator_id, body=body) status_event = RequestStatusEvent(new_status="incomplete") request.status_events.append(status_event) - self.db_session.add(request) - self.db_session.commit() + db.session.add(request) + db.session.commit() return request - def exists(self, request_id, creator_id): - return self.db_session.query( + @classmethod + def exists(cls, request_id, creator_id): + return db.session.query( exists().where( and_(Request.id == request_id, Request.creator == creator_id) ) ).scalar() - def get(self, request_id): + @classmethod + def get(cls, request_id): try: - request = self.db_session.query(Request).filter_by(id=request_id).one() + request = db.session.query(Request).filter_by(id=request_id).one() except NoResultFound: raise NotFoundError("request") return request - def get_many(self, creator_id=None): + @classmethod + def get_many(cls, creator_id=None): filters = [] if creator_id: filters.append(Request.creator == creator_id) requests = ( - self.db_session.query(Request) + db.session.query(Request) .filter(*filters) .order_by(Request.time_created.desc()) .all() ) return requests - @tornado.gen.coroutine - def submit(self, request): + @classmethod + def submit(cls, request): request.status_events.append(RequestStatusEvent(new_status="submitted")) if Requests.should_auto_approve(request): request.status_events.append(RequestStatusEvent(new_status="approved")) - self.db_session.add(request) - self.db_session.commit() + db.session.add(request) + db.session.commit() return request - @tornado.gen.coroutine - def update(self, request_id, request_delta): + @classmethod + def update(cls, request_id, request_delta): try: # Query for request matching id, acquiring a row-level write lock. # https://www.postgresql.org/docs/10/static/sql-select.html#SQL-FOR-UPDATE-SHARE request = ( - self.db_session.query(Request) + db.session.query(Request) .filter_by(id=request_id) .with_for_update(of=Request) .one() @@ -105,8 +108,8 @@ class Requests(object): # since it doesn't track dictionary mutations by default. flag_modified(request, "body") - self.db_session.add(request) - self.db_session.commit() + db.session.add(request) + db.session.commit() @classmethod def should_auto_approve(cls, request): diff --git a/atst/routes/__init__.py b/atst/routes/__init__.py index 93fde8e2..de181ce2 100644 --- a/atst/routes/__init__.py +++ b/atst/routes/__init__.py @@ -1,4 +1,7 @@ from flask import Blueprint, render_template, g +import pendulum + +from atst.domain.requests import Requests bp = Blueprint("atst", __name__) diff --git a/atst/routes/requests.py b/atst/routes/requests.py new file mode 100644 index 00000000..6c37344d --- /dev/null +++ b/atst/routes/requests.py @@ -0,0 +1,52 @@ +from flask import Blueprint, g, render_template +import pendulum + +from atst.domain.requests import Requests + +requests_bp = Blueprint("requests", __name__) + +def map_request(user, request): + time_created = pendulum.instance(request.time_created) + is_new = time_created.add(days=1) > pendulum.now() + + return { + "order_id": request.id, + "is_new": is_new, + "status": request.status, + "app_count": 1, + "date": time_created.format("M/DD/YYYY"), + "full_name": "{} {}".format(user["first_name"], user["last_name"]), + } + + +@requests_bp.route("/requests", methods=["GET"]) +def requests_index(): + requests = [] + if "review_and_approve_jedi_workspace_request" in g.current_user["atat_permissions"]: + requests = Requests.get_many() + else: + requests = Requests.get_many(creator_id=g.current_user["id"]) + + mapped_requests = [map_request(g.current_user, r) for r in requests] + + return render_template("requests.html", requests=mapped_requests) + + +@requests_bp.route("/requests/new/", methods=["GET"]) +def requests_new(): + pass + + +@requests_bp.route("/requests/new//", methods=["GET"]) +def requests_form_update(): + pass + + +@requests_bp.route("/requests/verify/", methods=["GET"]) +def financial_verification(): + pass + + +@requests_bp.route("/requests/verify/", methods=["POST"]) +def update_financial_verification(): + pass diff --git a/templates/components.html b/templates/components.html index 0bc83991..04f6be36 100644 --- a/templates/components.html +++ b/templates/components.html @@ -31,7 +31,17 @@ {%- endmacro %} -{% macro EmptyState(self, message, actionLabel, actionHref, icon=None) -%} +{% macro Modal() -%} + +{%- endmacro %} + +{% macro EmptyState(message, actionLabel, actionHref, icon=None) -%}

    {{ message }}

    @@ -42,3 +52,41 @@ {{ actionLabel }}
    {%- endmacro %} + +{% macro Alert(title, message=None, actions=None, level='info') -%} +{% set role = 'alertdialog' if actions else 'alert' %} +{% set levels = { + 'warning': { + 'icon': 'alert', + 'tone': 'assertive' + }, + 'error': { + 'icon': 'alert', + 'tone': 'assertive' + }, + 'info': { + 'icon': 'info', + 'tone': 'polite' + }, + 'success': { + 'icon': 'ok', + 'tone': 'polite' + } +} %} + +
    + {{ Icon(levels.get(level).get('icon'), classes='alert__icon icon--large') }} + +
    +

    {{title}}

    + + {% if message %} +
    {{ message | safe }}
    + {% endif %} + + {% if actions %} +
    {{ actions | safe }}
    + {% endif %} +
    +
    +{%- endmacro %} diff --git a/templates/requests.html.to b/templates/requests.html similarity index 81% rename from templates/requests.html.to rename to templates/requests.html index bd0c9e8c..5d27ff1e 100644 --- a/templates/requests.html.to +++ b/templates/requests.html @@ -1,8 +1,10 @@ -{% extends "base.html.to" %} +{% extends "base.html" %} + +{% from "components.html" import Modal, Alert, EmptyState %} {% block modal %} - {% if modalOpen() %} - {% apply modal %} + {% if g.modalOpen %} + {% call Modal() %}

    Your request is now approved!

    @@ -17,34 +19,34 @@ usage in sync with your budget.

    - {% module Alert("You'll need these details: ", + {{ Alert("You'll need these details: ", message="

    Task Order Number

    Contracting Officer: Name, E-mail and Office

    " - ) %} + ) }} - {% end %} - {% end %} -{% end %} + {% endcall %} + {% endif %} +{% endblock %} {% block content %} {% if not requests %} - {% module EmptyState( + {{ EmptyState( 'There are currently no active requests for you to see.', actionLabel='Create a new JEDI Cloud Request', actionHref='/requests/new', icon='document' - )%} + ) }} {% else %} - {% module Alert('Pending Financial Verification', + {{ Alert('Pending Financial Verification', message="

    Your next step is to create a Task Order (T.O.) associated with JEDI Cloud. Please consult a Contracting Officer (KO) or Contracting Officer Representative (COR) to help with this step.

    " - ) %} + ) }}
    @@ -84,10 +86,10 @@ {% for r in requests %} - {{ r['order_id'] }} + {{ r['order_id'] }} {% if r['is_new'] %}New - {% end %} + {% endif %} {{ r['date'] }} {{ r['full_name'] }} {{ r['app_count'] }} @@ -97,13 +99,13 @@ Approval - {% end %} + {% endfor %}
    -{% end %} +{% endif %} -{% end %} +{% endblock %} From 5d7dde35611edca7f4f46d7a6e14d2fe9936b500 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Wed, 1 Aug 2018 16:25:08 -0400 Subject: [PATCH 24/43] Financial verification form rendering --- Pipfile | 1 + Pipfile.lock | 10 ++- atst/app.py | 1 + atst/forms/forms.py | 4 +- atst/routes/requests.py | 7 ++- config/base.ini | 1 + ...on.html.to => financial_verification.html} | 61 ++++++++++--------- templates/requests_new.html.to | 2 +- 8 files changed, 51 insertions(+), 36 deletions(-) rename templates/requests/{financial_verification.html.to => financial_verification.html} (87%) diff --git a/Pipfile b/Pipfile index 620553f1..04e23572 100644 --- a/Pipfile +++ b/Pipfile @@ -17,6 +17,7 @@ flask = "*" flask-sqlalchemy = "*" flask-assets = "*" flask-session = "*" +flask-wtf = "*" [dev-packages] bandit = "*" diff --git a/Pipfile.lock b/Pipfile.lock index d55c6fcf..5dcf8f6b 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "f097384512537988c799b892830b52e78bcc19133327213e9c6e2876210d62d3" + "sha256": "e04e11d9bd5c1dcc725de48b20902f5c416417e73774e557e45af7bd0c147ff5" }, "pipfile-spec": 6, "requires": { @@ -62,6 +62,14 @@ "index": "pypi", "version": "==2.3.2" }, + "flask-wtf": { + "hashes": [ + "sha256:5d14d55cfd35f613d99ee7cba0fc3fbbe63ba02f544d349158c14ca15561cc36", + "sha256:d9a9e366b32dcbb98ef17228e76be15702cd2600675668bca23f63a7947fd5ac" + ], + "index": "pypi", + "version": "==0.14.2" + }, "itsdangerous": { "hashes": [ "sha256:cbb3fcf8d3e33df861709ecaf89d9e6629cff0a217bc2848f1b41cd30d360519" diff --git a/atst/app.py b/atst/app.py index 7dac78b7..babaff7a 100644 --- a/atst/app.py +++ b/atst/app.py @@ -229,6 +229,7 @@ def make_config(): OVERRIDE_CONFIG_FILENAME = os.getenv("OVERRIDE_CONFIG_FULLPATH") config = ConfigParser() + config.optionxform = str config_files = [BASE_CONFIG_FILENAME, ENV_CONFIG_FILENAME] if OVERRIDE_CONFIG_FILENAME: diff --git a/atst/forms/forms.py b/atst/forms/forms.py index c3ea02c4..48f03295 100644 --- a/atst/forms/forms.py +++ b/atst/forms/forms.py @@ -1,9 +1,9 @@ import tornado from tornado.gen import Return -from wtforms_tornado import Form +from flask_wtf import FlaskForm -class ValidatedForm(Form): +class ValidatedForm(FlaskForm): @tornado.gen.coroutine def perform_extra_validation(self, *args, **kwargs): diff --git a/atst/routes/requests.py b/atst/routes/requests.py index 6c37344d..5903524c 100644 --- a/atst/routes/requests.py +++ b/atst/routes/requests.py @@ -2,6 +2,7 @@ from flask import Blueprint, g, render_template import pendulum from atst.domain.requests import Requests +from atst.forms.financial import FinancialForm requests_bp = Blueprint("requests", __name__) @@ -43,8 +44,10 @@ def requests_form_update(): @requests_bp.route("/requests/verify/", methods=["GET"]) -def financial_verification(): - pass +def financial_verification(request_id=None): + request = Requests.get(request_id) + form = FinancialForm(data=request.body.get('financial_verification')) + return render_template("requests/financial_verification.html", f=form) @requests_bp.route("/requests/verify/", methods=["POST"]) diff --git a/config/base.ini b/config/base.ini index e8be9197..fddfa26f 100644 --- a/config/base.ini +++ b/config/base.ini @@ -5,6 +5,7 @@ DEBUG = true AUTHNID_BASE_URL= https://localhost:8001 COOKIE_SECRET = some-secret-please-replace SECRET = change_me_into_something_secret +SECRET_KEY = change_me_into_something_secret CAC_URL = https://localhost:8001 PE_NUMBER_CSV_URL = http://c95e1ebb198426ee57b8-174bb05a294821bedbf46b6384fe9b1f.r31.cf5.rackcdn.com/penumbers.csv REDIS_URI = redis://localhost:6379 diff --git a/templates/requests/financial_verification.html.to b/templates/requests/financial_verification.html similarity index 87% rename from templates/requests/financial_verification.html.to rename to templates/requests/financial_verification.html index df5b12ab..0f0d8110 100644 --- a/templates/requests/financial_verification.html.to +++ b/templates/requests/financial_verification.html @@ -1,4 +1,4 @@ -{% extends "../base.html.to" %} +{% extends "base.html" %} {% block content %} @@ -14,15 +14,15 @@ {% block form_action %} -
    - {% end %} + + {% endblock %} - {% module xsrf_form_html() %} + {{ f.csrf_token }} {% block form %} - {% autoescape None %} + {% autoescape false %} {% if f.errors %} There were some errors, see below. - {% end %} + {% endif %}

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

    @@ -32,7 +32,7 @@
    {{ e }}
    - {% end %} + {% endfor %} {{ f.uii_ids.label }} {{ f.uii_ids(placeholder="Example: \nDI 0CVA5786950 \nUN1945326361234786950") }} @@ -40,7 +40,7 @@
    {{ e }}
    - {% end %} + {% endfor %} {{ f.pe_id.label }} {{ f.pe_id(placeholder="Example: 0203752A") }} @@ -48,7 +48,7 @@
    {{ e }}
    - {% end %} + {% endfor %} {{ f.treasury_code.label }} {{ f.treasury_code(placeholder="Example: 1200") }} @@ -56,7 +56,7 @@
    {{ e }}
    - {% end %} + {% endfor %} {{ f.ba_code.label }} {{ f.ba_code(placeholder="Example: 02") }} @@ -64,7 +64,7 @@
    {{ e }}
    - {% end %} + {% endfor %} @@ -76,7 +76,7 @@
    {{ e }}
    - {% end %} + {% endfor %} {{ f.lname_co.label }} {{ f.lname_co(placeholder="Contracting Officer last name") }} @@ -84,7 +84,7 @@
    {{ e }}
    - {% end %} + {% endfor %} {{ f.email_co.label }} {{ f.email_co(placeholder="jane@mail.mil") }} @@ -92,7 +92,7 @@
    {{ e }}
    - {% end %} + {% endfor %} {{ f.office_co.label }} {{ f.office_co(placeholder="Example: WHS") }} @@ -100,7 +100,7 @@
    {{ e }}
    - {% end %} + {% endfor %} @@ -113,7 +113,7 @@
    {{ e }}
    - {% end %} + {% endfor %} {{ f.lname_cor.label }} {{ f.lname_cor(placeholder="Contracting Officer Representative last name") }} @@ -121,7 +121,7 @@
    {{ e }}
    - {% end %} + {% endfor %} {{ f.email_cor.label }} {{ f.email_cor(placeholder="jane@mail.mil") }} @@ -129,7 +129,7 @@
    {{ e }}
    - {% end %} + {% endfor %} {{ f.office_cor.label }} {{ f.office_cor(placeholder="Example: WHS") }} @@ -137,7 +137,7 @@
    {{ e }}
    - {% end %} + {% endfor %}

    ↓ FIELDS NEEDED FOR MANUAL ENTRY OF TASK ORDER INFORMATION (only necessary if EDA info not available) @@ -149,7 +149,7 @@
    {{ e }}
    - {% end %} + {% endfor %} {{ f.funding_type_other.label }} {{ f.funding_type_other(placeholder="") }} @@ -157,7 +157,7 @@
    {{ e }}
    - {% end %} + {% endfor %} {{ f.clin_0001.label }} {{ f.clin_0001(placeholder="50,000") }} @@ -165,7 +165,7 @@
    {{ e }}
    - {% end %} + {% endfor %} {{ f.clin_0003.label }} {{ f.clin_0003(placeholder="13,000") }} @@ -173,7 +173,7 @@
    {{ e }}
    - {% end %} + {% endfor %} {{ f.clin_1001.label }} {{ f.clin_1001(placeholder="30,000") }} @@ -181,7 +181,7 @@
    {{ e }}
    - {% end %} + {% endfor %} {{ f.clin_1003.label }} {{ f.clin_1003(placeholder="7,000") }} @@ -189,7 +189,7 @@
    {{ e }}
    - {% end %} + {% endfor %} {{ f.clin_2001.label }} {{ f.clin_2001(placeholder="30,000") }} @@ -197,7 +197,7 @@
    {{ e }}
    - {% end %} + {% endfor %} {{ f.clin_2003.label }} {{ f.clin_2003(placeholder="7,000") }} @@ -205,15 +205,16 @@
    {{ e }}
    - {% end %} - {% end %} + {% endfor %} + {% endautoescape %} + {% endblock form %} {% block next %} - {% end %} + {% endblock %}
    -{% end %} +{% endblock %} diff --git a/templates/requests_new.html.to b/templates/requests_new.html.to index 5a309f26..c5f96ed5 100644 --- a/templates/requests_new.html.to +++ b/templates/requests_new.html.to @@ -22,7 +22,7 @@ {% end %} {% end %} - {% module xsrf_form_html() %} + {{ form.csrf_token }} {% block form %} form goes here {% end %} From 6f0ff7990415004134b74b625c3e9cbb44705719 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Thu, 2 Aug 2018 10:21:26 -0400 Subject: [PATCH 25/43] First page of request form rendering, but flow is still WIP --- atst/handlers/request_new.py | 3 +- atst/routes/requests.py | 199 +++++++++++++++++- templates/components.html | 53 +++++ templates/requests/menu.html | 13 ++ templates/requests/menu.html.to | 13 -- templates/requests/screen-0.html | 12 ++ templates/requests/screen-0.html.to | 12 -- templates/requests/screen-1.html | 46 ++++ templates/requests/screen-1.html.to | 45 ---- templates/requests/screen-2.html | 32 +++ templates/requests/screen-2.html.to | 31 --- .../{screen-3.html.to => screen-3.html} | 0 .../{screen-4.html.to => screen-4.html} | 0 .../{screen-5.html.to => screen-5.html} | 0 .../{sidebar.html.to => sidebar.html} | 0 templates/requests_new.html | 45 ++++ templates/requests_new.html.to | 46 ---- 17 files changed, 398 insertions(+), 152 deletions(-) create mode 100644 templates/requests/menu.html delete mode 100644 templates/requests/menu.html.to create mode 100644 templates/requests/screen-0.html delete mode 100644 templates/requests/screen-0.html.to create mode 100644 templates/requests/screen-1.html delete mode 100644 templates/requests/screen-1.html.to create mode 100644 templates/requests/screen-2.html delete mode 100644 templates/requests/screen-2.html.to rename templates/requests/{screen-3.html.to => screen-3.html} (100%) rename templates/requests/{screen-4.html.to => screen-4.html} (100%) rename templates/requests/{screen-5.html.to => screen-5.html} (100%) rename templates/requests/{sidebar.html.to => sidebar.html} (100%) create mode 100644 templates/requests_new.html delete mode 100644 templates/requests_new.html.to diff --git a/atst/handlers/request_new.py b/atst/handlers/request_new.py index 1d33daa0..30549e63 100644 --- a/atst/handlers/request_new.py +++ b/atst/handlers/request_new.py @@ -101,7 +101,6 @@ class RequestNew(BaseHandler): class JEDIRequestFlow(object): def __init__( self, - requests_repo, pe_numbers_repo, current_step, request=None, @@ -110,6 +109,8 @@ class JEDIRequestFlow(object): current_user=None, existing_request=None, ): + self.pe_numbers_repo = pe_numbers_repo + self.requests_repo = requests_repo self.pe_numbers_repo = pe_numbers_repo diff --git a/atst/routes/requests.py b/atst/routes/requests.py index 5903524c..565c268e 100644 --- a/atst/routes/requests.py +++ b/atst/routes/requests.py @@ -1,8 +1,14 @@ -from flask import Blueprint, g, render_template +from flask import Blueprint, g, render_template, url_for, redirect, request import pendulum +from collections import defaultdict from atst.domain.requests import Requests from atst.forms.financial import FinancialForm +from atst.forms.request import RequestForm +from atst.forms.org import OrgForm +from atst.forms.poc import POCForm +from atst.forms.review import ReviewForm + requests_bp = Blueprint("requests", __name__) @@ -34,13 +40,72 @@ def requests_index(): @requests_bp.route("/requests/new/", methods=["GET"]) -def requests_new(): +def requests_form_new(): pass @requests_bp.route("/requests/new//", methods=["GET"]) -def requests_form_update(): - pass +def requests_form_update(screen=1, request_id=None): + request = Requests.get(request_id) if request_id is not None else None + jedi_flow = JEDIRequestFlow(screen, request, request_id=request_id) + + return render_template( + "requests/screen-%d.html" % int(screen), + f=jedi_flow.form, + data=jedi_flow.current_step_data, + screens=jedi_flow.screens, + current=screen, + next_screen=screen + 1, + request_id=request_id, + can_submit=jedi_flow.can_submit + ) + +@requests_bp.route("/requests/new//", methods=["POST"]) +def requests_update(screen=1, request_id=None): + screen = int(screen) + post_data = str(request.data) + current_user = g.current_user + existing_request = Requests.get(request_id) if request_id is not None else None + jedi_flow = JEDIRequestFlow( + screen, + post_data=post_data, + request_id=request_id, + current_user=current_user, + existing_request=existing_request, + ) + + rerender_args = dict( + f=jedi_flow.form, + data=post_data, + screens=jedi_flow.screens, + current=screen, + next_screen=jedi_flow.next_screen, + request_id=jedi_flow.request_id, + ) + + if jedi_flow.validate(): + jedi_flow.create_or_update_request() + valid = jedi_flow.validate_warnings() + if valid: + if jedi_flow.next_screen > len(jedi_flow.screens): + where = "/requests" + else: + where = url_for( + "requests.requests_Form_update", screen=jedi_flow.next_screen, request_id=jedi_flow.request_id + ) + return redirect(where) + else: + return render_template( + "requests/screen-%d.html" % int(screen), + **rerender_args + ) + else: + return render_template( + "requests/screen-%d.html" % int(screen), + **rerender_args + ) + + @requests_bp.route("/requests/verify/", methods=["GET"]) @@ -53,3 +118,129 @@ def financial_verification(request_id=None): @requests_bp.route("/requests/verify/", methods=["POST"]) def update_financial_verification(): pass + + +class JEDIRequestFlow(object): + def __init__( + self, + current_step, + request=None, + post_data=None, + request_id=None, + current_user=None, + existing_request=None, + ): + self.current_step = current_step + self.request = request + + self.post_data = post_data + self.is_post = self.post_data is not None + + 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) + elif self.request: + return self.form_class()(data=self.current_step_data) + else: + return self.form_class()() + + def validate(self): + return self.form.validate() + + def validate_warnings(self): + existing_request_data = ( + self.existing_request + and self.existing_request.body.get(self.form_section) + ) or None + + valid = self.form.perform_extra_validation( + existing_request_data, + ) + return valid + + @property + def current_screen(self): + return self.screens[self.current_step - 1] + + @property + def form_section(self): + return self.current_screen["section"] + + def form_class(self): + return self.current_screen["form"] + + @property + def current_step_data(self): + data = {} + + if self.is_post: + data = self.post_data + + if self.request: + if self.form_section == "review_submit": + data = self.request.body + else: + data = self.request.body.get(self.form_section, {}) + + return defaultdict(lambda: defaultdict(lambda: 'Input required'), data) + + @property + def can_submit(self): + return self.request and self.request.status != "incomplete" + + @property + def next_screen(self): + return self.current_step + 1 + + @property + def screens(self): + return [ + { + "title": "Details of Use", + "section": "details_of_use", + "form": RequestForm, + "subitems": [ + { + "title": "Overall request details", + "id": "overall-request-details", + }, + {"title": "Cloud Resources", "id": "cloud-resources"}, + {"title": "Support Staff", "id": "support-staff"}, + ], + "show": True, + }, + { + "title": "Information About You", + "section": "information_about_you", + "form": OrgForm, + "show": True, + }, + { + "title": "Primary Point of Contact", + "section": "primary_poc", + "form": POCForm, + "show": True, + }, + { + "title": "Review & Submit", + "section": "review_submit", + "form": ReviewForm, + "show":True, + }, + ] + + def create_or_update_request(self): + request_data = { + self.form_section: self.form.data + } + if self.request_id: + Requests.update(request_id, request_data) + else: + request = Requests.create(self.current_user["id"], request_data) + self.request_id = request.id diff --git a/templates/components.html b/templates/components.html index 04f6be36..027ede6f 100644 --- a/templates/components.html +++ b/templates/components.html @@ -90,3 +90,56 @@ {%- endmacro %} + +{% macro TextInput(field, placeholder='') -%} +
    + + + {{ field(placeholder=placeholder) | safe }} + + {% if field.errors %} + {% for error in field.errors %} + {{ error }} + {% endfor %} + {% endif %} +
    +{%- endmacro %} + +{% macro OptionsInput(field, inline=False) -%} +
    + +
    + + {{ field.label }} + + {% if field.description %} + {{ field.description | safe }} + {% endif %} + + {% if field.errors %} + {{ Icon('alert') }} + {% endif %} + + + {{ field() }} + + {% if field.errors %} + {% for error in field.errors %} + {{ error }} + {% endfor %} + {% endif %} + +
    +
    + +{%- endmacro %} diff --git a/templates/requests/menu.html b/templates/requests/menu.html new file mode 100644 index 00000000..f1642e46 --- /dev/null +++ b/templates/requests/menu.html @@ -0,0 +1,13 @@ +
    + +
    diff --git a/templates/requests/menu.html.to b/templates/requests/menu.html.to deleted file mode 100644 index 4bc77431..00000000 --- a/templates/requests/menu.html.to +++ /dev/null @@ -1,13 +0,0 @@ -
    - -
    \ No newline at end of file diff --git a/templates/requests/screen-0.html b/templates/requests/screen-0.html new file mode 100644 index 00000000..19f57d2e --- /dev/null +++ b/templates/requests/screen-0.html @@ -0,0 +1,12 @@ +{% extends '../requests_new.html.to' %} + +{% block form %} +

    New JEDI Request

    + +

    Lorem ipsum dolor sit amet, consectetur adipisicing elit. Natus error omnis a, tenetur similique quo officiis voluptates eum recusandae dolorem minus dignissimos, magni consequatur, maxime debitis reprehenderit sint non iusto?

    + +New Application +Existing Application +Sandbox Environment + +{% endblock %} diff --git a/templates/requests/screen-0.html.to b/templates/requests/screen-0.html.to deleted file mode 100644 index 6fe78374..00000000 --- a/templates/requests/screen-0.html.to +++ /dev/null @@ -1,12 +0,0 @@ -{% extends '../requests_new.html.to' %} - -{% block form %} -

    New JEDI Request

    - -

    Lorem ipsum dolor sit amet, consectetur adipisicing elit. Natus error omnis a, tenetur similique quo officiis voluptates eum recusandae dolorem minus dignissimos, magni consequatur, maxime debitis reprehenderit sint non iusto?

    - -New Application -Existing Application -Sandbox Environment - -{% end %} diff --git a/templates/requests/screen-1.html b/templates/requests/screen-1.html new file mode 100644 index 00000000..da6c5ae0 --- /dev/null +++ b/templates/requests/screen-1.html @@ -0,0 +1,46 @@ +{% extends 'requests_new.html' %} + +{% from "components.html" import Alert, TextInput, OptionsInput %} + +{% block subtitle %} +

    Details of Use

    +{% endblock %} + +{% block form %} + +{% if f.errors %} + {{ Alert('There were some errors', + message="

    Please see below.

    ", + level='error' + ) }} +{% endif %} + + +

    We’d like to know a little about how you plan to use JEDI Cloud services to process your request. Please answer the following questions to the best of your ability. Note that the CCPO does not directly help with migrating systems to JEDI Cloud. These questions are for learning about your cloud readiness and financial usage of the JEDI Cloud; your estimates will not be used for any department level reporting.

    +

    All fields are required, unless specified optional.

    + +

    General

    +{{ TextInput(f.dod_component) }} +{{ TextInput(f.jedi_usage,placeholder="e.g. We are migrating XYZ application to the cloud so that...") }} + +

    Cloud Readiness

    +{{ TextInput(f.num_software_systems,placeholder="Number of systems") }} +{{ OptionsInput(f.jedi_migration) }} +{{ OptionsInput(f.rationalization_software_systems) }} +{{ OptionsInput(f.technical_support_team) }} +{{ OptionsInput(f.organization_providing_assistance) }} +{{ OptionsInput(f.engineering_assessment) }} +{{ TextInput(f.data_transfers) }} +{{ TextInput(f.expected_completion_date) }} +{{ OptionsInput(f.cloud_native) }} + +

    Financial Usage

    +{{ TextInput(f.estimated_monthly_spend) }} +

    So this means you are spending approximately $X annually

    +{{ TextInput(f.dollar_value) }} +{{ TextInput(f.number_user_sessions) }} +{{ TextInput(f.average_daily_traffic) }} +{{ TextInput(f.start_date) }} + + +{% endblock %} diff --git a/templates/requests/screen-1.html.to b/templates/requests/screen-1.html.to deleted file mode 100644 index a8ed7887..00000000 --- a/templates/requests/screen-1.html.to +++ /dev/null @@ -1,45 +0,0 @@ -{% extends '../requests_new.html.to' %} - -{% block subtitle %} -

    Details of Use

    -{% end %} - -{% block form %} - -{% autoescape None %} -{% if f.errors %} - {% module Alert('There were some errors', - message="

    Please see below.

    ", - level='error' - ) %} -{% end %} - - -

    We’d like to know a little about how you plan to use JEDI Cloud services to process your request. Please answer the following questions to the best of your ability. Note that the CCPO does not directly help with migrating systems to JEDI Cloud. These questions are for learning about your cloud readiness and financial usage of the JEDI Cloud; your estimates will not be used for any department level reporting.

    -

    All fields are required, unless specified optional.

    - -

    General

    -{% module TextInput(f.dod_component) %} -{% module TextInput(f.jedi_usage,placeholder="e.g. We are migrating XYZ application to the cloud so that...") %} - -

    Cloud Readiness

    -{% module TextInput(f.num_software_systems,placeholder="Number of systems") %} -{% module OptionsInput(f.jedi_migration) %} -{% module OptionsInput(f.rationalization_software_systems) %} -{% module OptionsInput(f.technical_support_team) %} -{% module OptionsInput(f.organization_providing_assistance) %} -{% module OptionsInput(f.engineering_assessment) %} -{% module TextInput(f.data_transfers) %} -{% module TextInput(f.expected_completion_date) %} -{% module OptionsInput(f.cloud_native) %} - -

    Financial Usage

    -{% module TextInput(f.estimated_monthly_spend) %} -

    So this means you are spending approximately $X annually

    -{% module TextInput(f.dollar_value) %} -{% module TextInput(f.number_user_sessions) %} -{% module TextInput(f.average_daily_traffic) %} -{% module TextInput(f.start_date) %} - - -{% end %} diff --git a/templates/requests/screen-2.html b/templates/requests/screen-2.html new file mode 100644 index 00000000..24c8f749 --- /dev/null +++ b/templates/requests/screen-2.html @@ -0,0 +1,32 @@ +{% extends 'requests_new.html' %} + +{% from "components.html" import Alert, TextInput, OptionsInput %} + +{% block subtitle %} +

    Information About You

    +{% endblock %} + +{% block form %} + +{% if f.errors %} + {{ Alert('There were some errors', + message="

    Please see below.

    ", + level='error' + ) }} +{% endif %} + +

    Please tell us more about you.

    + +{{ TextInput(f.fname_request,placeholder='First Name') }} +{{ TextInput(f.lname_request,placeholder='Last Name') }} +{{ TextInput(f.email_request,placeholder='jane@mail.mil') }} +{{ TextInput(f.phone_number,placeholder='(123) 456-7890') }} + +

    We want to collect the following information from you for security auditing and determining priviledged user access

    + +{{ TextInput(f.service_branch,placeholder='e.g. US Air Force, US Army, US Navy, Marine Corps, Defense Media Agency') }} +{{ OptionsInput(f.citizenship) }} +{{ OptionsInput(f.designation) }} +{{ TextInput(f.date_latest_training) }} + +{% endblock %} diff --git a/templates/requests/screen-2.html.to b/templates/requests/screen-2.html.to deleted file mode 100644 index fb667dc5..00000000 --- a/templates/requests/screen-2.html.to +++ /dev/null @@ -1,31 +0,0 @@ -{% extends '../requests_new.html.to' %} - -{% block subtitle %} -

    Information About You

    -{% end %} - -{% block form %} - -{% autoescape None %} -{% if f.errors %} - {% module Alert('There were some errors', - message="

    Please see below.

    ", - level='error' - ) %} -{% end %} - -

    Please tell us more about you.

    - -{% module TextInput(f.fname_request,placeholder='First Name') %} -{% module TextInput(f.lname_request,placeholder='Last Name') %} -{% module TextInput(f.email_request,placeholder='jane@mail.mil') %} -{% module TextInput(f.phone_number,placeholder='(123) 456-7890') %} - -

    We want to collect the following information from you for security auditing and determining priviledged user access

    - -{% module TextInput(f.service_branch,placeholder='e.g. US Air Force, US Army, US Navy, Marine Corps, Defense Media Agency') %} -{% module OptionsInput(f.citizenship) %} -{% module OptionsInput(f.designation) %} -{% module TextInput(f.date_latest_training) %} - -{% end %} \ No newline at end of file diff --git a/templates/requests/screen-3.html.to b/templates/requests/screen-3.html similarity index 100% rename from templates/requests/screen-3.html.to rename to templates/requests/screen-3.html diff --git a/templates/requests/screen-4.html.to b/templates/requests/screen-4.html similarity index 100% rename from templates/requests/screen-4.html.to rename to templates/requests/screen-4.html diff --git a/templates/requests/screen-5.html.to b/templates/requests/screen-5.html similarity index 100% rename from templates/requests/screen-5.html.to rename to templates/requests/screen-5.html diff --git a/templates/requests/sidebar.html.to b/templates/requests/sidebar.html similarity index 100% rename from templates/requests/sidebar.html.to rename to templates/requests/sidebar.html diff --git a/templates/requests_new.html b/templates/requests_new.html new file mode 100644 index 00000000..559c2355 --- /dev/null +++ b/templates/requests_new.html @@ -0,0 +1,45 @@ +{% extends "base.html" %} + +{% block content %} + +
    + + {% include 'requests/menu.html' %} + +
    + +
    +

    New Request

    +
    {% block subtitle %}{% endblock %}
    +
    + +
    + {% block form_action %} + {% if request_id %} +
    + {% else %} + + {% endif %} + {% endblock %} + + {{ f.csrf_token }} + {% block form %} + form goes here + {% endblock %} + +
    + +
    + + {% block next %} + +
    + +
    + + {% endblock %} + + +
    + +{% endblock %} diff --git a/templates/requests_new.html.to b/templates/requests_new.html.to deleted file mode 100644 index c5f96ed5..00000000 --- a/templates/requests_new.html.to +++ /dev/null @@ -1,46 +0,0 @@ -{% extends "base.html.to" %} - -{% block content %} - -
    - - {% include 'requests/menu.html.to' %} - -
    - -
    -

    New Request

    -
    {% block subtitle %}{% end %}
    -
    - -
    - {% block form_action %} - {% if request_id %} -
    - {% else %} - - {% end %} - {% end %} - - {{ form.csrf_token }} - {% block form %} - form goes here - {% end %} - -
    - -
    - - {% block next %} - -
    - -
    - - {% end %} - - -
    - -{% end %} - From ed0893b4a0735490560ef0814d537a25256758b1 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Thu, 2 Aug 2018 10:36:25 -0400 Subject: [PATCH 26/43] Converted all form pages. Flow still not working --- atst/forms/financial.py | 4 +-- atst/forms/forms.py | 5 ++-- atst/routes/requests.py | 11 +++++-- templates/requests/screen-3.html | 23 +++++++------- templates/requests/screen-4.html | 33 +++++++++++---------- templates/requests/screen-5.html | 51 ++++++++++++++++---------------- 6 files changed, 65 insertions(+), 62 deletions(-) diff --git a/atst/forms/financial.py b/atst/forms/financial.py index 52b4ef28..fb66282b 100644 --- a/atst/forms/financial.py +++ b/atst/forms/financial.py @@ -36,7 +36,6 @@ def suggest_pe_id(pe_id): return None -@tornado.gen.coroutine def validate_pe_id(field, existing_request, pe_numbers_repo): try: pe_number = pe_numbers_repo.get(field.data) @@ -55,12 +54,11 @@ def validate_pe_id(field, existing_request, pe_numbers_repo): class FinancialForm(ValidatedForm): - @tornado.gen.coroutine def perform_extra_validation(self, existing_request, pe_numbers_repo): 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, pe_numbers_repo) - raise Return(valid) + 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 48f03295..d60e3a2c 100644 --- a/atst/forms/forms.py +++ b/atst/forms/forms.py @@ -5,8 +5,7 @@ from flask_wtf import FlaskForm class ValidatedForm(FlaskForm): - @tornado.gen.coroutine def perform_extra_validation(self, *args, **kwargs): - """A coroutine that performs any applicable extra validation. Must + """Performs any applicable extra validation. Must return True if the form is valid or False otherwise.""" - raise Return(True) + return True diff --git a/atst/routes/requests.py b/atst/routes/requests.py index 565c268e..0c225a7f 100644 --- a/atst/routes/requests.py +++ b/atst/routes/requests.py @@ -63,7 +63,7 @@ def requests_form_update(screen=1, request_id=None): @requests_bp.route("/requests/new//", methods=["POST"]) def requests_update(screen=1, request_id=None): screen = int(screen) - post_data = str(request.data) + post_data = request.form current_user = g.current_user existing_request = Requests.get(request_id) if request_id is not None else None jedi_flow = JEDIRequestFlow( @@ -91,7 +91,7 @@ def requests_update(screen=1, request_id=None): where = "/requests" else: where = url_for( - "requests.requests_Form_update", screen=jedi_flow.next_screen, request_id=jedi_flow.request_id + "requests.requests_form_update", screen=jedi_flow.next_screen, request_id=jedi_flow.request_id ) return redirect(where) else: @@ -120,6 +120,11 @@ def update_financial_verification(): pass +@requests_bp.route("/requests/submit/", methods=["POST"]) +def requests_submit(request_id=None): + pass + + class JEDIRequestFlow(object): def __init__( self, @@ -240,7 +245,7 @@ class JEDIRequestFlow(object): self.form_section: self.form.data } if self.request_id: - Requests.update(request_id, request_data) + Requests.update(self.request_id, request_data) else: request = Requests.create(self.current_user["id"], request_data) self.request_id = request.id diff --git a/templates/requests/screen-3.html b/templates/requests/screen-3.html index af4ea66f..5bdbaf43 100644 --- a/templates/requests/screen-3.html +++ b/templates/requests/screen-3.html @@ -1,18 +1,19 @@ -{% extends '../requests_new.html.to' %} +{% extends 'requests_new.html' %} + +{% from "components.html" import Alert, TextInput %} {% block subtitle %}

    Primary Government/Military
    Point of Contact (POC)

    -{% end %} +{% endblock %} {% block form %} -{% autoescape None %} {% if f.errors %} - {% module Alert('There were some errors', + {{ Alert('There were some errors', message="

    Please see below.

    ", level='error' - ) %} -{% end %} + ) }} +{% endif %}

    Please designate a Primary Point of Contact that will be responsible for owning the workspace in the JEDI Cloud.

    The Point of Contact will become the primary owner of the workspace created to use the JEDI Cloud. As a workspace owner, this person will have the ability to: @@ -26,9 +27,9 @@ This POC may be you.

    -{% module TextInput(f.fname_poc,placeholder='First Name') %} -{% module TextInput(f.lname_poc,placeholder='Last Name') %} -{% module TextInput(f.email_poc,placeholder='jane@mail.mil') %} -{% module TextInput(f.dodid_poc,placeholder='10-digit number on the back of the CAC') %} +{{ TextInput(f.fname_poc,placeholder='First Name') }} +{{ TextInput(f.lname_poc,placeholder='Last Name') }} +{{ TextInput(f.email_poc,placeholder='jane@mail.mil') }} +{{ TextInput(f.dodid_poc,placeholder='10-digit number on the back of the CAC') }} -{% end %} \ No newline at end of file +{% endblock %} diff --git a/templates/requests/screen-4.html b/templates/requests/screen-4.html index 60dd06ac..37cfc5de 100644 --- a/templates/requests/screen-4.html +++ b/templates/requests/screen-4.html @@ -1,27 +1,28 @@ -{% extends '../requests_new.html.to' %} +{% extends 'requests_new.html' %} + +{% from "components.html" import Alert, TextInput %} {% block subtitle %}

    Review & Submit

    -{% end %} +{% endblock %} {% block form_action %} -
    - {% end %} + +{% endblock %} {% block form %} - {% autoescape None %} {% if f.errors %} - {% module Alert('There were some errors', + {{ Alert('There were some errors', message="

    Please complete all required fields before submitting.

    ", level='error' - ) %} - {% end %} + ) }} + {% endif %}

    Before you can submit your request, please take a moment to review the information entered in the form. You may make changes by clicking the edit link on each section. When all information looks right, go ahead and submit.

    -

    Details of Use Edit

    +

    Details of Use Edit

    @@ -107,7 +108,7 @@
    -

    Information About You Edit

    +

    Information About You Edit

    @@ -152,7 +153,7 @@
    -

    Primary Point of Contact Edit

    +

    Primary Point of Contact Edit

    @@ -178,15 +179,15 @@ -{% end %} +{% endblock %} {% block next %} {% if not can_submit %} - {% module Alert('There were some errors', + {{ Alert('There were some errors', message="

    Please complete all required fields before submitting.

    ", level='error' - ) %} -{% end %} + ) }} +{% endif %}
    @@ -194,4 +195,4 @@ -{% end %} +{% endblock %} diff --git a/templates/requests/screen-5.html b/templates/requests/screen-5.html index e8642f1e..845ed133 100644 --- a/templates/requests/screen-5.html +++ b/templates/requests/screen-5.html @@ -2,13 +2,12 @@ {% block form %} -{% autoescape None %} {% if f.errors %} - {% module Alert('There were some errors', + {{ Alert('There were some errors', message="

    Please complete all the fields before submitting.

    ", level='error' - ) %} -{% end %} + ) }} +{% endif %}

    Financial Verification

    @@ -20,7 +19,7 @@
    {{ e }}
    -{% end %} +{% endfor %} {{ f.uii_ids.label }} {{ f.uii_ids(placeholder="Example: \nDI 0CVA5786950 \nUN1945326361234786950") }} @@ -28,7 +27,7 @@
    {{ e }}
    -{% end %} +{% endfor %} {{ f.pe_id.label }} {{ f.pe_id(placeholder="Example: 0203752A") }} @@ -36,7 +35,7 @@
    {{ e }}
    -{% end %} +{% endfor %} {{ f.treasury_code.label }} {{ f.treasury_code(placeholder="Example: 1200") }} @@ -44,7 +43,7 @@
    {{ e }}
    -{% end %} +{% endfor %} {{ f.ba_code.label }} {{ f.ba_code(placeholder="Example: 02") }} @@ -52,7 +51,7 @@
    {{ e }}
    -{% end %} +{% endfor %} @@ -64,7 +63,7 @@
    {{ e }}
    -{% end %} +{% endfor %} {{ f.lname_co.label }} {{ f.lname_co(placeholder="Contracting Officer last name") }} @@ -72,7 +71,7 @@
    {{ e }}
    -{% end %} +{% endfor %} {{ f.email_co.label }} {{ f.email_co(placeholder="jane@mail.mil") }} @@ -80,7 +79,7 @@
    {{ e }}
    -{% end %} +{% endfor %} {{ f.office_co.label }} {{ f.office_co(placeholder="Example: WHS") }} @@ -88,7 +87,7 @@
    {{ e }}
    -{% end %} +{% endfor %} @@ -101,7 +100,7 @@
    {{ e }}
    -{% end %} +{% endfor %} {{ f.lname_cor.label }} {{ f.lname_cor(placeholder="Contracting Officer Representative last name") }} @@ -109,7 +108,7 @@
    {{ e }}
    -{% end %} +{% endfor %} {{ f.email_cor.label }} {{ f.email_cor(placeholder="jane@mail.mil") }} @@ -117,7 +116,7 @@
    {{ e }}
    -{% end %} +{% endfor %} {{ f.office_cor.label }} {{ f.office_cor(placeholder="Example: WHS") }} @@ -125,7 +124,7 @@
    {{ e }}
    -{% end %} +{% endfor %}

    ↓ FIELDS NEEDED FOR MANUAL ENTRY OF TASK ORDER INFORMATION (only necessary if EDA info not available) @@ -137,7 +136,7 @@
    {{ e }}
    -{% end %} +{% endfor %} {{ f.funding_type_other.label }} {{ f.funding_type_other(placeholder="") }} @@ -145,7 +144,7 @@
    {{ e }}
    -{% end %} +{% endfor %} {{ f.clin_0001.label }} {{ f.clin_0001(placeholder="50,000") }} @@ -153,7 +152,7 @@
    {{ e }}
    -{% end %} +{% endfor %} {{ f.clin_0003.label }} {{ f.clin_0003(placeholder="13,000") }} @@ -161,7 +160,7 @@
    {{ e }}
    -{% end %} +{% endfor %} {{ f.clin_1001.label }} {{ f.clin_1001(placeholder="30,000") }} @@ -169,7 +168,7 @@
    {{ e }}
    -{% end %} +{% endfor %} {{ f.clin_1003.label }} {{ f.clin_1003(placeholder="7,000") }} @@ -177,7 +176,7 @@
    {{ e }}
    -{% end %} +{% endfor %} {{ f.clin_2001.label }} {{ f.clin_2001(placeholder="30,000") }} @@ -185,7 +184,7 @@
    {{ e }}
    -{% end %} +{% endfor %} {{ f.clin_2003.label }} {{ f.clin_2003(placeholder="7,000") }} @@ -193,7 +192,7 @@
    {{ e }}
    -{% end %} +{% endfor %} -{% end %} +{% endblock %} From 4ffbbb34714dfd1b047fba784fa5a329676dcf4a Mon Sep 17 00:00:00 2001 From: richard-dds Date: Thu, 2 Aug 2018 10:59:05 -0400 Subject: [PATCH 27/43] Request flow seems to be working --- atst/app.py | 2 +- atst/routes/requests/__init__.py | 126 ++++++++++++++++++++ atst/routes/requests/jedi_request_flow.py | 135 ++++++++++++++++++++++ templates/components.html | 6 +- templates/requests/menu.html | 2 +- 5 files changed, 266 insertions(+), 5 deletions(-) create mode 100644 atst/routes/requests/__init__.py create mode 100644 atst/routes/requests/jedi_request_flow.py diff --git a/atst/app.py b/atst/app.py index babaff7a..cd432ea9 100644 --- a/atst/app.py +++ b/atst/app.py @@ -46,7 +46,7 @@ def make_flask_callbacks(app): def set_globals(): g.navigationContext = 'workspace' if re.match('\/workspaces\/[A-Za-z0-9]*', request.url) else 'global' g.dev = os.getenv("TORNADO_ENV", "dev") == "dev" - g.matchesPath = lambda href: re.match('^'+href, request.url) + g.matchesPath = lambda href: re.match('^'+href, request.path) g.modalOpen = request.args.get("modal", False) g.current_user = { "id": "cce17030-4109-4719-b958-ed109dbb87c8", diff --git a/atst/routes/requests/__init__.py b/atst/routes/requests/__init__.py new file mode 100644 index 00000000..aa747e71 --- /dev/null +++ b/atst/routes/requests/__init__.py @@ -0,0 +1,126 @@ +from flask import Blueprint, g, render_template, url_for, redirect, request +import pendulum + +from atst.routes.requests.jedi_request_flow import JEDIRequestFlow +from atst.domain.requests import Requests + + +requests_bp = Blueprint("requests", __name__) + +def map_request(user, request): + time_created = pendulum.instance(request.time_created) + is_new = time_created.add(days=1) > pendulum.now() + + return { + "order_id": request.id, + "is_new": is_new, + "status": request.status, + "app_count": 1, + "date": time_created.format("M/DD/YYYY"), + "full_name": "{} {}".format(user["first_name"], user["last_name"]), + } + + +@requests_bp.route("/requests", methods=["GET"]) +def requests_index(): + requests = [] + if "review_and_approve_jedi_workspace_request" in g.current_user["atat_permissions"]: + requests = Requests.get_many() + else: + requests = Requests.get_many(creator_id=g.current_user["id"]) + + mapped_requests = [map_request(g.current_user, r) for r in requests] + + return render_template("requests.html", requests=mapped_requests) + + +@requests_bp.route("/requests/new/", methods=["GET"]) +def requests_form_new(): + pass + + +@requests_bp.route("/requests/new//", methods=["GET"]) +def requests_form_update(screen=1, request_id=None): + request = Requests.get(request_id) if request_id is not None else None + jedi_flow = JEDIRequestFlow(screen, request, request_id=request_id) + + return render_template( + "requests/screen-%d.html" % int(screen), + f=jedi_flow.form, + data=jedi_flow.current_step_data, + screens=jedi_flow.screens, + current=screen, + next_screen=screen + 1, + request_id=request_id, + can_submit=jedi_flow.can_submit + ) + +@requests_bp.route("/requests/new//", methods=["POST"]) +def requests_update(screen=1, request_id=None): + screen = int(screen) + post_data = request.form + current_user = g.current_user + existing_request = Requests.get(request_id) if request_id is not None else None + jedi_flow = JEDIRequestFlow( + screen, + post_data=post_data, + request_id=request_id, + current_user=current_user, + existing_request=existing_request, + ) + + rerender_args = dict( + f=jedi_flow.form, + data=post_data, + screens=jedi_flow.screens, + current=screen, + next_screen=jedi_flow.next_screen, + request_id=jedi_flow.request_id, + ) + + if jedi_flow.validate(): + jedi_flow.create_or_update_request() + valid = jedi_flow.validate_warnings() + if valid: + if jedi_flow.next_screen > len(jedi_flow.screens): + where = "/requests" + else: + where = url_for( + "requests.requests_form_update", screen=jedi_flow.next_screen, request_id=jedi_flow.request_id + ) + return redirect(where) + else: + return render_template( + "requests/screen-%d.html" % int(screen), + **rerender_args + ) + else: + return render_template( + "requests/screen-%d.html" % int(screen), + **rerender_args + ) + + + + +@requests_bp.route("/requests/verify/", methods=["GET"]) +def financial_verification(request_id=None): + request = Requests.get(request_id) + form = FinancialForm(data=request.body.get('financial_verification')) + return render_template("requests/financial_verification.html", f=form) + + +@requests_bp.route("/requests/verify/", methods=["POST"]) +def update_financial_verification(): + pass + + +@requests_bp.route("/requests/submit/", methods=["POST"]) +def requests_submit(request_id=None): + request = Requests.get(request_id) + Requests.submit(request) + + if request.status == "approved": + return redirect("/requests?modal=True") + else: + return redirect("/requests") diff --git a/atst/routes/requests/jedi_request_flow.py b/atst/routes/requests/jedi_request_flow.py new file mode 100644 index 00000000..4befe5f5 --- /dev/null +++ b/atst/routes/requests/jedi_request_flow.py @@ -0,0 +1,135 @@ +from collections import defaultdict + +from atst.domain.requests import Requests +from atst.forms.financial import FinancialForm +from atst.forms.request import RequestForm +from atst.forms.org import OrgForm +from atst.forms.poc import POCForm +from atst.forms.review import ReviewForm + + + +class JEDIRequestFlow(object): + def __init__( + self, + current_step, + request=None, + post_data=None, + request_id=None, + current_user=None, + existing_request=None, + ): + self.current_step = current_step + self.request = request + + self.post_data = post_data + self.is_post = self.post_data is not None + + 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) + elif self.request: + return self.form_class()(data=self.current_step_data) + else: + return self.form_class()() + + def validate(self): + return self.form.validate() + + def validate_warnings(self): + existing_request_data = ( + self.existing_request + and self.existing_request.body.get(self.form_section) + ) or None + + valid = self.form.perform_extra_validation( + existing_request_data, + ) + return valid + + @property + def current_screen(self): + return self.screens[self.current_step - 1] + + @property + def form_section(self): + return self.current_screen["section"] + + def form_class(self): + return self.current_screen["form"] + + @property + def current_step_data(self): + data = {} + + if self.is_post: + data = self.post_data + + if self.request: + if self.form_section == "review_submit": + data = self.request.body + else: + data = self.request.body.get(self.form_section, {}) + + return defaultdict(lambda: defaultdict(lambda: 'Input required'), data) + + @property + def can_submit(self): + return self.request and self.request.status != "incomplete" + + @property + def next_screen(self): + return self.current_step + 1 + + @property + def screens(self): + return [ + { + "title": "Details of Use", + "section": "details_of_use", + "form": RequestForm, + "subitems": [ + { + "title": "Overall request details", + "id": "overall-request-details", + }, + {"title": "Cloud Resources", "id": "cloud-resources"}, + {"title": "Support Staff", "id": "support-staff"}, + ], + "show": True, + }, + { + "title": "Information About You", + "section": "information_about_you", + "form": OrgForm, + "show": True, + }, + { + "title": "Primary Point of Contact", + "section": "primary_poc", + "form": POCForm, + "show": True, + }, + { + "title": "Review & Submit", + "section": "review_submit", + "form": ReviewForm, + "show":True, + }, + ] + + def create_or_update_request(self): + request_data = { + self.form_section: self.form.data + } + if self.request_id: + Requests.update(self.request_id, request_data) + else: + request = Requests.create(self.current_user["id"], request_data) + self.request_id = request.id diff --git a/templates/components.html b/templates/components.html index 027ede6f..ea214631 100644 --- a/templates/components.html +++ b/templates/components.html @@ -7,11 +7,11 @@ {% macro SidenavItem(label, href, active=False, icon=None, subnav=None) -%}
  • - {% if icon %} + {% if icon %} {{ Icon(icon, classes="sidenav__link-icon") }} - {% endif %} + {% endif %} - {{label}} + {{label}} {% if subnav and active %} diff --git a/templates/requests/menu.html b/templates/requests/menu.html index f1642e46..24b28d4f 100644 --- a/templates/requests/menu.html +++ b/templates/requests/menu.html @@ -2,7 +2,7 @@
  • - {% end %} - {% end %} -{% end %} + {% endcall %} + {% endif %} +{% endblock %} {% block content %} -{% module Alert('A Warning Alert', +{{ Alert('A Warning Alert', message="\

    This is a message. It is a very important message. Please note, proper semantic markup is required here, such as paragraph tags. Don't omit paragraph tags!

    \

    Also note the same for actions below. You'll need to include the full link markup.

    \ ", actions="Open a Modal Dialog", level='warning' -) %} +) }} -{% module Alert('A Success Alert', +{{ Alert('A Success Alert', message="

    Congratulations! You did a thing.

    ", level='success' -) %} +) }} -{% module Alert('An Error Alert', +{{ Alert('An Error Alert', message="

    Booooo. You're the worst.

    ", level='error' -) %} +) }} -{% module Alert('An Info Alert', +{{ Alert('An Info Alert', message="

    The more you know.

    " -) %} +) }}
    @@ -214,7 +216,7 @@
    - Inline Checkboxes {% module Icon('alert') %} + Inline Checkboxes {{ Icon('alert') }} @@ -234,7 +236,7 @@
    - Problem Radio Buttons {% module Icon('alert') %} + Problem Radio Buttons {{ Icon('alert') }} @@ -279,30 +281,30 @@
    Icons
    - {% module Icon('trash') %} 'trash'    - {% module Icon('document') %} 'document'    - {% module Icon('cloud') %} 'cloud'    - {% module Icon('chart') %} 'chart'    - {% module Icon('caret_up') %} 'caret_up'    - {% module Icon('caret_down') %} 'caret_down'    - {% module Icon('caret_right') %} 'caret_right'    - {% module Icon('caret_left') %} 'caret_left'    - {% module Icon('x') %} 'x'    - {% module Icon('search') %} 'search'    - {% module Icon('avatar') %} 'avatar'    - {% module Icon('download') %} 'download'    - {% module Icon('briefcase') %} 'briefcase'    - {% module Icon('bell') %} 'bell'    - {% module Icon('folder') %} 'folder'    - {% module Icon('help') %} 'help'    - {% module Icon('shield') %} 'shield'    - {% module Icon('info') %} 'info'    - {% module Icon('alert') %} 'alert'    - {% module Icon('link') %} 'link'    - {% module Icon('ok') %} 'ok'    - {% module Icon('checkmark') %} 'checkmark'    - {% module Icon('arrow-right') %} 'arrow-right'    - {% module Icon('arrow-down') %} 'arrow-down'    + {{ Icon('trash') }} 'trash'    + {{ Icon('document') }} 'document'    + {{ Icon('cloud') }} 'cloud'    + {{ Icon('chart') }} 'chart'    + {{ Icon('caret_up') }} 'caret_up'    + {{ Icon('caret_down') }} 'caret_down'    + {{ Icon('caret_right') }} 'caret_right'    + {{ Icon('caret_left') }} 'caret_left'    + {{ Icon('x') }} 'x'    + {{ Icon('search') }} 'search'    + {{ Icon('avatar') }} 'avatar'    + {{ Icon('download') }} 'download'    + {{ Icon('briefcase') }} 'briefcase'    + {{ Icon('bell') }} 'bell'    + {{ Icon('folder') }} 'folder'    + {{ Icon('help') }} 'help'    + {{ Icon('shield') }} 'shield'    + {{ Icon('info') }} 'info'    + {{ Icon('alert') }} 'alert'    + {{ Icon('link') }} 'link'    + {{ Icon('ok') }} 'ok'    + {{ Icon('checkmark') }} 'checkmark'    + {{ Icon('arrow-right') }} 'arrow-right'    + {{ Icon('arrow-down') }} 'arrow-down'   
    @@ -360,4 +362,4 @@ Action group link
    -{% end %} +{% endblock %} From bc8bd719b60047eec642d0589523fc851652cbc0 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Thu, 2 Aug 2018 13:44:01 -0400 Subject: [PATCH 36/43] Rename assets environment to play nicely with script/setup --- atst/app.py | 4 ++-- atst/assets.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/atst/app.py b/atst/app.py index cd54f15d..46b017bb 100644 --- a/atst/app.py +++ b/atst/app.py @@ -5,7 +5,7 @@ from flask import Flask, request, g from unipath import Path from atst.database import db -from atst.assets import assets +from atst.assets import environment as assets_environment from atst.routes import bp from atst.routes.workspaces import bp as workspace_routes @@ -29,7 +29,7 @@ def make_app(config): make_flask_callbacks(app) db.init_app(app) - assets.init_app(app) + assets_environment.init_app(app) app.register_blueprint(bp) app.register_blueprint(workspace_routes) diff --git a/atst/assets.py b/atst/assets.py index bed898ac..aef6b35e 100644 --- a/atst/assets.py +++ b/atst/assets.py @@ -1,7 +1,7 @@ from flask_assets import Environment, Bundle from atst.home import home -assets = Environment() +environment = Environment() css = Bundle( "../scss/atat.scss", @@ -10,4 +10,4 @@ css = Bundle( depends=("**/*.scss"), ) -assets.register("css", css) +environment.register("css", css) From f8cee60baa6a09f5297ad3d29f56b13d9ec8ff11 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Thu, 2 Aug 2018 13:44:54 -0400 Subject: [PATCH 37/43] Formatting --- atst/routes/requests/financial_verification.py | 5 ++++- atst/routes/requests/requests_form.py | 9 ++++++--- atst/routes/workspaces.py | 11 +++++++++-- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/atst/routes/requests/financial_verification.py b/atst/routes/requests/financial_verification.py index e0d08a37..4471b524 100644 --- a/atst/routes/requests/financial_verification.py +++ b/atst/routes/requests/financial_verification.py @@ -4,6 +4,7 @@ from . import requests_bp from atst.domain.requests import Requests from atst.forms.financial import FinancialForm + @requests_bp.route("/requests/verify/", methods=["GET"]) def financial_verification(request_id=None): request = Requests.get(request_id) @@ -30,7 +31,9 @@ def update_financial_verification(request_id): if valid: redirect(url_for("requests.financial_verification_submitted")) else: - return render_template("requests/financial_verification.html", **rerender_args) + return render_template( + "requests/financial_verification.html", **rerender_args + ) else: return render_template("requests/financial_verification.html", **rerender_args) diff --git a/atst/routes/requests/requests_form.py b/atst/routes/requests/requests_form.py index 4e9b5018..cd12fbcd 100644 --- a/atst/routes/requests/requests_form.py +++ b/atst/routes/requests/requests_form.py @@ -21,7 +21,9 @@ def requests_form_new(screen): ) -@requests_bp.route("/requests/new/", methods=["GET"], defaults={"request_id": None}) +@requests_bp.route( + "/requests/new/", methods=["GET"], defaults={"request_id": None} +) @requests_bp.route("/requests/new//", methods=["GET"]) def requests_form_update(screen=1, request_id=None): request = Requests.get(request_id) if request_id is not None else None @@ -39,7 +41,9 @@ def requests_form_update(screen=1, request_id=None): ) -@requests_bp.route("/requests/new/", methods=["POST"], defaults={"request_id": None}) +@requests_bp.route( + "/requests/new/", methods=["POST"], defaults={"request_id": None} +) @requests_bp.route("/requests/new//", methods=["POST"]) def requests_update(screen=1, request_id=None): screen = int(screen) @@ -84,7 +88,6 @@ def requests_update(screen=1, request_id=None): return render_template("requests/screen-%d.html" % int(screen), **rerender_args) - @requests_bp.route("/requests/submit/", methods=["POST"]) def requests_submit(request_id=None): request = Requests.get(request_id) diff --git a/atst/routes/workspaces.py b/atst/routes/workspaces.py index 1f3b47f2..43b3b050 100644 --- a/atst/routes/workspaces.py +++ b/atst/routes/workspaces.py @@ -14,18 +14,25 @@ mock_workspaces = [ } ] + @bp.route("/workspaces") def workspaces(): return render_template("workspaces.html", page=5, workspaces=mock_workspaces) + @bp.route("/workspaces//projects") def workspace_projects(workspace_id): projects_repo = Projects() projects = projects_repo.get_many(workspace_id) - return render_template("workspace_projects.html", workspace_id=workspace_id, projects=projects) + return render_template( + "workspace_projects.html", workspace_id=workspace_id, projects=projects + ) + @bp.route("/workspaces//members") def workspace_members(workspace_id): members_repo = Members() members = members_repo.get_many(workspace_id) - return render_template("workspace_members.html", workspace_id=workspace_id, members=members) + return render_template( + "workspace_members.html", workspace_id=workspace_id, members=members + ) From 92e65e75951db4d8bf927f07cebc380c3f9bd96b Mon Sep 17 00:00:00 2001 From: Luis Cielak Date: Mon, 30 Jul 2018 17:26:07 -0400 Subject: [PATCH 38/43] Add member placeholder info --- templates/member_edit.html.to | 73 ++++++++----------- .../navigation/workspace_navigation.html.to | 32 ++++---- templates/workspace_members.html | 16 ++-- 3 files changed, 51 insertions(+), 70 deletions(-) diff --git a/templates/member_edit.html.to b/templates/member_edit.html.to index 24893bf2..d454152c 100644 --- a/templates/member_edit.html.to +++ b/templates/member_edit.html.to @@ -16,40 +16,39 @@ level="info" ) %} -
    -
    -

    {{ member_name }}

    -
    Workspace Role
    {{member_workspace_role}}
    -
    -
    -
    -
    -
    DOD ID:
    -
    {{ member_id }}
    -
    -
    -
    Email:
    -
    {{ member_email }}
    -
    -
    - edit account details + +
    +
    +

    + {% if is_new_member %} + Add new member + {% else %} + {{ member_name }} + {% end %} +

    +
    Workspace Role {{member_workspace_role}}
    -
    {%- endmacro %} - -{% macro TextInput(field, placeholder='') -%} -
    - - - {{ field(placeholder=placeholder) | safe }} - - {% if field.errors %} - {% for error in field.errors %} - {{ error }} - {% endfor %} - {% endif %} -
    -{%- endmacro %} - -{% macro OptionsInput(field, inline=False) -%} -
    - -
    - - {{ field.label }} - - {% if field.description %} - {{ field.description | safe }} - {% endif %} - - {% if field.errors %} - {{ Icon('alert') }} - {% endif %} - - - {{ field() }} - - {% if field.errors %} - {% for error in field.errors %} - {{ error }} - {% endfor %} - {% endif %} - -
    -
    - -{%- endmacro %} From 45b47c41bfc21f1afdf1bbba00c830c2386603d4 Mon Sep 17 00:00:00 2001 From: dandds Date: Thu, 2 Aug 2018 13:49:39 -0400 Subject: [PATCH 41/43] update tests for Flask --- Pipfile | 2 +- Pipfile.lock | 68 ++++++--- alembic/env.py | 5 +- atst/domain/pe_numbers.py | 15 +- atst/forms/financial.py | 13 +- atst/routes/__init__.py | 10 ++ atst/routes/requests.py | 52 ------- .../routes/requests/financial_verification.py | 3 +- config/test.ini | 2 + .../{calculator.html.to => calculator.html} | 4 +- templates/components.html | 59 +++++++- templates/{reports.html.to => reports.html} | 4 +- templates/{root.html.to => root.html} | 14 +- templates/{users.html.to => users.html} | 4 +- tests/conftest.py | 76 +++++++--- tests/domain/test_pe_numbers.py | 26 ++-- tests/domain/test_requests.py | 19 +-- tests/domain/test_roles.py | 4 +- tests/domain/test_task_orders.py | 10 +- tests/domain/test_users.py | 4 +- tests/domain/test_workspace_users.py | 8 +- tests/handlers/test_financial_verification.py | 89 ------------ tests/handlers/test_request_new.py | 54 ------- tests/handlers/test_request_submit.py | 57 -------- tests/routes/test_financial_verification.py | 76 ++++++++++ tests/routes/test_request_new.py | 35 +++++ tests/routes/test_request_submit.py | 37 +++++ tests/test_api_client.py | 10 -- tests/test_auth.py | 137 +++++++++--------- tests/test_basic.py | 10 +- tests/test_integration.py | 32 ++-- tests/test_routes.py | 11 +- 32 files changed, 468 insertions(+), 482 deletions(-) delete mode 100644 atst/routes/requests.py create mode 100644 config/test.ini rename templates/{calculator.html.to => calculator.html} (69%) rename templates/{reports.html.to => reports.html} (68%) rename templates/{root.html.to => root.html} (63%) rename templates/{users.html.to => users.html} (68%) delete mode 100644 tests/handlers/test_financial_verification.py delete mode 100644 tests/handlers/test_request_new.py delete mode 100644 tests/handlers/test_request_submit.py create mode 100644 tests/routes/test_financial_verification.py create mode 100644 tests/routes/test_request_new.py create mode 100644 tests/routes/test_request_submit.py delete mode 100644 tests/test_api_client.py diff --git a/Pipfile b/Pipfile index 620553f1..31379b86 100644 --- a/Pipfile +++ b/Pipfile @@ -21,13 +21,13 @@ flask-session = "*" [dev-packages] bandit = "*" pytest = "*" -pytest-tornado = "*" ipython = "*" ipdb = "*" pylint = "*" black = "*" pytest-watch = "*" factory-boy = "*" +pytest-flask = "*" [requires] python_version = "3.6" diff --git a/Pipfile.lock b/Pipfile.lock index d55c6fcf..ead0c686 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "f097384512537988c799b892830b52e78bcc19133327213e9c6e2876210d62d3" + "sha256": "9f17530cb96833c424369b9cac305cb43a817cdf19605aaedeb2d98566302857" }, "pipfile-spec": 6, "requires": { @@ -160,7 +160,6 @@ "sha256:1d936da41ee06216d89fdc7ead1ee9a5da2811a8787515a976b646e110c3f622", "sha256:e4ef42e82b0b493c5849eed98b5ab49d6767caf982127e9a33167f1153b36cc5" ], - "markers": "python_version != '3.0.*' and python_version != '3.1.*' and python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.3.*'", "version": "==2018.5" }, "redis": { @@ -342,9 +341,16 @@ "sha256:0e9a1227a3a0f3297a485715e72ee6eb77081b17b629367042b586e38c03c867", "sha256:b4840807a94a3bad0217d6ed3f9b65a1cc6e1db1c99e1184673056ae2c0a4c4d" ], - "markers": "python_version != '3.1.*' and python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.0.*'", "version": "==0.8.17" }, + "flask": { + "hashes": [ + "sha256:2271c0070dbcb5275fad4a82e29f23ab92682dc45f9dfbc22c02ba9b9322ce48", + "sha256:a080b744b7e345ccfcbc77954861cb05b3c63786e93f2b3875e0913d44b43f05" + ], + "index": "pypi", + "version": "==1.0.2" + }, "gitdb2": { "hashes": [ "sha256:87783b7f4a8f6b71c7fe81d32179b3c8781c1a7d6fa0c69bff2f315b00aff4f8", @@ -387,9 +393,14 @@ "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" ], - "markers": "python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.1.*' and python_version != '3.0.*' and python_version != '3.2.*'", "version": "==4.3.4" }, + "itsdangerous": { + "hashes": [ + "sha256:cbb3fcf8d3e33df861709ecaf89d9e6629cff0a217bc2848f1b41cd30d360519" + ], + "version": "==0.24" + }, "jedi": { "hashes": [ "sha256:b409ed0f6913a701ed474a614a3bb46e6953639033e31f769ca7581da5bd1ec1", @@ -397,6 +408,13 @@ ], "version": "==0.12.1" }, + "jinja2": { + "hashes": [ + "sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd", + "sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4" + ], + "version": "==2.10" + }, "lazy-object-proxy": { "hashes": [ "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", @@ -431,6 +449,12 @@ ], "version": "==1.3.1" }, + "markupsafe": { + "hashes": [ + "sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665" + ], + "version": "==1.0" + }, "mccabe": { "hashes": [ "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", @@ -486,7 +510,6 @@ "sha256:6e3836e39f4d36ae72840833db137f7b7d35105079aee6ec4a62d9f80d594dd1", "sha256:95eb8364a4708392bae89035f45341871286a333f749c3141c20573d2b3876e1" ], - "markers": "python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.1.*' and python_version != '3.0.*' and python_version != '3.2.*'", "version": "==0.7.1" }, "prompt-toolkit": { @@ -509,7 +532,6 @@ "sha256:3fd59af7435864e1a243790d322d763925431213b6b8529c6ca71081ace3bbf7", "sha256:e31fb2767eb657cbde86c454f02e99cb846d3cd9d61b318525140214fdc0e98e" ], - "markers": "python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.1.*' and python_version != '3.0.*' and python_version != '3.2.*'", "version": "==1.5.4" }, "pygments": { @@ -535,13 +557,13 @@ "index": "pypi", "version": "==3.7.0" }, - "pytest-tornado": { + "pytest-flask": { "hashes": [ - "sha256:214fc59d06fb81696fce3028b56dff522168ac1cfc784cfc0077b7b1e425b4cd", - "sha256:687c1f9c0f5bda7808c1e53c14bbebfe4fb9452e34cc95b440e598d4724265e0" + "sha256:2c5a36f9033ef8b6f85ddbefaebdd4f89197fc283f94b20dfe1a1beba4b77f03", + "sha256:657c7de386215ab0230bee4d76ace0339ae82fcbb34e134e17a29f65032eef03" ], "index": "pypi", - "version": "==0.5.0" + "version": "==0.10.0" }, "pytest-watch": { "hashes": [ @@ -559,11 +581,15 @@ }, "pyyaml": { "hashes": [ + "sha256:1cbc199009e78f92d9edf554be4fe40fb7b0bef71ba688602a00e97a51909110", "sha256:254bf6fda2b7c651837acb2c718e213df29d531eebf00edb54743d10bcb694eb", "sha256:3108529b78577327d15eec243f0ff348a0640b0c3478d67ad7f5648f93bac3e2", "sha256:3c17fb92c8ba2f525e4b5f7941d850e7a48c3a59b32d331e2502a3cdc6648e76", + "sha256:6f89b5c95e93945b597776163403d47af72d243f366bf4622ff08bdfd1c950b7", "sha256:8d6d96001aa7f0a6a4a95e8143225b5d06e41b1131044913fecb8f85a125714b", - "sha256:c8a88edd93ee29ede719080b2be6cb2333dfee1dccba213b422a9c8e97f2967b" + "sha256:be622cc81696e24d0836ba71f6272a2b5767669b0d79fdcf0295d51ac2e156c8", + "sha256:c8a88edd93ee29ede719080b2be6cb2333dfee1dccba213b422a9c8e97f2967b", + "sha256:f39411e380e2182ad33be039e8ee5770a5d9efe01a2bfb7ae58d9ba31c4a2a9d" ], "version": "==4.2b4" }, @@ -607,19 +633,6 @@ ], "version": "==0.9.4" }, - "tornado": { - "hashes": [ - "sha256:1c0816fc32b7d31b98781bd8ebc7a9726d7dce67407dc353a2e66e697e138448", - "sha256:4f66a2172cb947387193ca4c2c3e19131f1c70fa8be470ddbbd9317fd0801582", - "sha256:5327ba1a6c694e0149e7d9126426b3704b1d9d520852a3e4aa9fc8fe989e4046", - "sha256:6a7e8657618268bb007646b9eae7661d0b57f13efc94faa33cd2588eae5912c9", - "sha256:a9b14804783a1d77c0bd6c66f7a9b1196cbddfbdf8bceb64683c5ae60bd1ec6f", - "sha256:c58757e37c4a3172949c99099d4d5106e4d7b63aa0617f9bb24bfbff712c7866", - "sha256:d8984742ce86c0855cccecd5c6f54a9f7532c983947cff06f3a0e2115b47f85c" - ], - "index": "pypi", - "version": "==5.1" - }, "traitlets": { "hashes": [ "sha256:9c4bd2d267b7153df9152698efb1050a5d84982d3384a37b2c1f7723ba3e7835", @@ -676,6 +689,13 @@ ], "version": "==0.1.7" }, + "werkzeug": { + "hashes": [ + "sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c", + "sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b" + ], + "version": "==0.14.1" + }, "wrapt": { "hashes": [ "sha256:d4d560d479f2c21e1b5443bbd15fe7ec4b37fe7e53d335d3b9b0a7b1226fe3c6" diff --git a/alembic/env.py b/alembic/env.py index 6a9d9749..91b96364 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -30,11 +30,10 @@ sys.path.append(parent_dir) from atst.app import make_config app_config = make_config() -config.set_main_option('sqlalchemy.url', app_config['default']['DATABASE_URI']) +config.set_main_option('sqlalchemy.url', app_config['DATABASE_URI']) -from atst.database import make_db +from atst.database import db from atst.models import * -db = make_db(app_config) target_metadata = Base.metadata diff --git a/atst/domain/pe_numbers.py b/atst/domain/pe_numbers.py index de362d20..62f8326a 100644 --- a/atst/domain/pe_numbers.py +++ b/atst/domain/pe_numbers.py @@ -1,24 +1,25 @@ from sqlalchemy.dialects.postgresql import insert +from atst.database import db from atst.models.pe_number import PENumber from .exceptions import NotFoundError class PENumbers(object): - def __init__(self, db_session): - self.db_session = db_session - def get(self, number): - pe_number = self.db_session.query(PENumber).get(number) + @classmethod + def get(cls, number): + pe_number = db.session.query(PENumber).get(number) if not pe_number: raise NotFoundError("pe_number") return pe_number - def create_many(self, list_of_pe_numbers): + @classmethod + def create_many(cls, list_of_pe_numbers): stmt = insert(PENumber).values(list_of_pe_numbers) do_update = stmt.on_conflict_do_update( index_elements=["number"], set_=dict(description=stmt.excluded.description) ) - self.db_session.execute(do_update) - self.db_session.commit() + db.session.execute(do_update) + db.session.commit() diff --git a/atst/forms/financial.py b/atst/forms/financial.py index 240a8274..68196837 100644 --- a/atst/forms/financial.py +++ b/atst/forms/financial.py @@ -1,12 +1,11 @@ import re -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 atst.domain.exceptions import NotFoundError +from atst.domain.pe_numbers import PENumbers from .fields import NewlineListField from .forms import ValidatedForm @@ -40,9 +39,9 @@ def suggest_pe_id(pe_id): return None -def validate_pe_id(field, existing_request, pe_numbers_repo): +def validate_pe_id(field, existing_request): try: - pe_number = pe_numbers_repo.get(field.data) + pe_number = PENumbers.get(field.data) except NotFoundError: suggestion = suggest_pe_id(field.data) error_str = ( @@ -50,17 +49,17 @@ def validate_pe_id(field, existing_request, pe_numbers_repo): "If you have double checked it you can submit anyway. " "Your request will need to go through a manual review." ).format('Did you mean "{}"? '.format(suggestion) if suggestion else "") - field.errors.append(error_str) + field.errors += (error_str,) return False return True class FinancialForm(ValidatedForm): - def perform_extra_validation(self, existing_request, pe_numbers_repo): + def perform_extra_validation(self, existing_request): 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, pe_numbers_repo) + valid = validate_pe_id(self.pe_id, existing_request) return valid task_order_id = StringField( diff --git a/atst/routes/__init__.py b/atst/routes/__init__.py index ab7ec866..3c1f99ac 100644 --- a/atst/routes/__init__.py +++ b/atst/routes/__init__.py @@ -7,6 +7,11 @@ bp = Blueprint("atst", __name__) @bp.route("/") +def root(): + return render_template("root.html") + + +@bp.route("/home") def home(): return render_template("home.html") @@ -14,3 +19,8 @@ def home(): @bp.route("/styleguide") def styleguide(): return render_template("styleguide.html") + + +@bp.route('/') +def catch_all(path): + return render_template("{}.html".format(path)) diff --git a/atst/routes/requests.py b/atst/routes/requests.py deleted file mode 100644 index 6c37344d..00000000 --- a/atst/routes/requests.py +++ /dev/null @@ -1,52 +0,0 @@ -from flask import Blueprint, g, render_template -import pendulum - -from atst.domain.requests import Requests - -requests_bp = Blueprint("requests", __name__) - -def map_request(user, request): - time_created = pendulum.instance(request.time_created) - is_new = time_created.add(days=1) > pendulum.now() - - return { - "order_id": request.id, - "is_new": is_new, - "status": request.status, - "app_count": 1, - "date": time_created.format("M/DD/YYYY"), - "full_name": "{} {}".format(user["first_name"], user["last_name"]), - } - - -@requests_bp.route("/requests", methods=["GET"]) -def requests_index(): - requests = [] - if "review_and_approve_jedi_workspace_request" in g.current_user["atat_permissions"]: - requests = Requests.get_many() - else: - requests = Requests.get_many(creator_id=g.current_user["id"]) - - mapped_requests = [map_request(g.current_user, r) for r in requests] - - return render_template("requests.html", requests=mapped_requests) - - -@requests_bp.route("/requests/new/", methods=["GET"]) -def requests_new(): - pass - - -@requests_bp.route("/requests/new//", methods=["GET"]) -def requests_form_update(): - pass - - -@requests_bp.route("/requests/verify/", methods=["GET"]) -def financial_verification(): - pass - - -@requests_bp.route("/requests/verify/", methods=["POST"]) -def update_financial_verification(): - pass diff --git a/atst/routes/requests/financial_verification.py b/atst/routes/requests/financial_verification.py index 4471b524..38420287 100644 --- a/atst/routes/requests/financial_verification.py +++ b/atst/routes/requests/financial_verification.py @@ -1,4 +1,5 @@ from flask import render_template, redirect, url_for +from flask import request as http_request from . import requests_bp from atst.domain.requests import Requests @@ -29,7 +30,7 @@ def update_financial_verification(request_id): existing_request.body.get("financial_verification") ) if valid: - redirect(url_for("requests.financial_verification_submitted")) + return redirect(url_for("requests.financial_verification_submitted")) else: return render_template( "requests/financial_verification.html", **rerender_args diff --git a/config/test.ini b/config/test.ini new file mode 100644 index 00000000..0074f60b --- /dev/null +++ b/config/test.ini @@ -0,0 +1,2 @@ +[default] +PGDATABASE = atat_test diff --git a/templates/calculator.html.to b/templates/calculator.html similarity index 69% rename from templates/calculator.html.to rename to templates/calculator.html index bf19f0a5..4f1c9a3d 100644 --- a/templates/calculator.html.to +++ b/templates/calculator.html @@ -1,4 +1,4 @@ -{% extends "base.html.to" %} +{% extends "base.html" %} {% block content %} @@ -8,5 +8,5 @@ -{% end %} +{% endblock %} diff --git a/templates/components.html b/templates/components.html index 04f6be36..ea214631 100644 --- a/templates/components.html +++ b/templates/components.html @@ -7,11 +7,11 @@ {% macro SidenavItem(label, href, active=False, icon=None, subnav=None) -%}
  • - {% if icon %} + {% if icon %} {{ Icon(icon, classes="sidenav__link-icon") }} - {% endif %} + {% endif %} - {{label}} + {{label}} {% if subnav and active %} @@ -90,3 +90,56 @@
  • {%- endmacro %} + +{% macro TextInput(field, placeholder='') -%} +
    + + + {{ field(placeholder=placeholder) | safe }} + + {% if field.errors %} + {% for error in field.errors %} + {{ error }} + {% endfor %} + {% endif %} +
    +{%- endmacro %} + +{% macro OptionsInput(field, inline=False) -%} +
    + +
    + + {{ field.label }} + + {% if field.description %} + {{ field.description | safe }} + {% endif %} + + {% if field.errors %} + {{ Icon('alert') }} + {% endif %} + + + {{ field() }} + + {% if field.errors %} + {% for error in field.errors %} + {{ error }} + {% endfor %} + {% endif %} + +
    +
    + +{%- endmacro %} diff --git a/templates/reports.html.to b/templates/reports.html similarity index 68% rename from templates/reports.html.to rename to templates/reports.html index 4cfebd3e..090b6bfe 100644 --- a/templates/reports.html.to +++ b/templates/reports.html @@ -1,4 +1,4 @@ -{% extends "base.html.to" %} +{% extends "base.html" %} {% block content %} @@ -8,5 +8,5 @@ -{% end %} +{% endblock %} diff --git a/templates/root.html.to b/templates/root.html similarity index 63% rename from templates/root.html.to rename to templates/root.html index 40bef97e..a206bc7b 100644 --- a/templates/root.html.to +++ b/templates/root.html @@ -3,10 +3,10 @@ - {% block title %}JEDI{% end %} - {% for url in assets['css'].urls() %} - - {% end %} + {% block title %}JEDI{% endblock %} + {% assets "css" %} + + {% endassets %} @@ -17,11 +17,11 @@

    JEDI

    - Sign In with CAC + Sign In with CAC - {% if dev() %} + {% if g.dev %} DEV Login - {% end %} + {% endif %} diff --git a/templates/users.html.to b/templates/users.html similarity index 68% rename from templates/users.html.to rename to templates/users.html index ee050f69..9d6523f7 100644 --- a/templates/users.html.to +++ b/templates/users.html @@ -1,4 +1,4 @@ -{% extends "base.html.to" %} +{% extends "base.html" %} {% block content %} @@ -8,5 +8,5 @@ -{% end %} +{% endblock %} diff --git a/tests/conftest.py b/tests/conftest.py index 0921f2bf..f9857ec3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,44 +1,72 @@ +import os import pytest +import alembic.config +import alembic.command +from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker, scoped_session -from atst.app import make_app, make_deps, make_config -from atst.database import make_db +from atst.app import make_app, make_config from tests.mocks import MockApiClient from atst.sessions import DictSessions +from atst.models import Base +from atst.database import db as _db -@pytest.fixture -def app(db): - TEST_DEPS = { - "authnid_client": MockApiClient("authnid"), - "sessions": DictSessions(), - "db_session": db - } - +@pytest.fixture(scope='session') +def app(request): config = make_config() - deps = make_deps(config) - deps.update(TEST_DEPS) - return make_app(config, deps) + _app = make_app(config) + + ctx = _app.app_context() + ctx.push() + + def teardown(): + ctx.pop() + + return _app -@pytest.fixture(scope='function') -def db(): +def apply_migrations(): + """Applies all alembic migrations.""" + alembic_config = os.path.join(os.path.dirname(__file__), "../", "alembic.ini") + config = alembic.config.Config(alembic_config) + app_config = make_config() + config.set_main_option('sqlalchemy.url', app_config["DATABASE_URI"]) + alembic.command.upgrade(config, 'head') - # Override db with a new SQLAlchemy session so that we can rollback - # each test's transaction. - # Inspiration: https://docs.sqlalchemy.org/en/latest/orm/session_transaction.html#session-external-transaction - config = make_config() - database = make_db(config) - connection = database.get_bind().connect() + +@pytest.fixture(scope='session') +def db(app, request): + + def teardown(): + _db.drop_all() + + _db.app = app + + apply_migrations() + + yield _db + + _db.drop_all() + + +@pytest.fixture(scope='function', autouse=True) +def session(db, request): + """Creates a new database session for a test.""" + connection = db.engine.connect() transaction = connection.begin() - db = scoped_session(sessionmaker(bind=connection)) - yield db + options = dict(bind=connection, binds={}) + session = db.create_scoped_session(options=options) + + db.session = session + + yield session - db.close() transaction.rollback() connection.close() + session.remove() class DummyForm(dict): diff --git a/tests/domain/test_pe_numbers.py b/tests/domain/test_pe_numbers.py index 028aa712..60c410b5 100644 --- a/tests/domain/test_pe_numbers.py +++ b/tests/domain/test_pe_numbers.py @@ -6,36 +6,32 @@ from atst.domain.pe_numbers import PENumbers from tests.factories import PENumberFactory -@pytest.fixture() -def pe_numbers(db): - return PENumbers(db) - @pytest.fixture(scope="function") -def new_pe_number(db): +def new_pe_number(session): def make_pe_number(**kwargs): pen = PENumberFactory.create(**kwargs) - db.add(pen) - db.commit() + session.add(pen) + session.commit() return pen return make_pe_number -def test_can_get_pe_number(pe_numbers, new_pe_number): +def test_can_get_pe_number(new_pe_number): new_pen = new_pe_number(number="0701367F", description="Combat Support - Offensive") - pen = pe_numbers.get(new_pen.number) + pen = PENumbers.get(new_pen.number) assert pen.number == new_pen.number -def test_nonexistent_pe_number_raises(pe_numbers): +def test_nonexistent_pe_number_raises(): with pytest.raises(NotFoundError): - pe_numbers.get("some fake number") + PENumbers.get("some fake number") -def test_create_many(pe_numbers): +def test_create_many(): pen_list = [['123456', 'Land Speeder'], ['7891011', 'Lightsaber']] - pe_numbers.create_many(pen_list) + PENumbers.create_many(pen_list) - assert pe_numbers.get(pen_list[0][0]) - assert pe_numbers.get(pen_list[1][0]) + assert PENumbers.get(pen_list[0][0]) + assert PENumbers.get(pen_list[1][0]) diff --git a/tests/domain/test_requests.py b/tests/domain/test_requests.py index f45d5be2..6553cfe6 100644 --- a/tests/domain/test_requests.py +++ b/tests/domain/test_requests.py @@ -8,14 +8,14 @@ from tests.factories import RequestFactory @pytest.fixture() -def requests(db): - return Requests(db) +def requests(session): + return Requests() @pytest.fixture(scope="function") -def new_request(db): +def new_request(session): created_request = RequestFactory.create() - db.add(created_request) - db.commit() + session.add(created_request) + session.commit() return created_request @@ -31,25 +31,22 @@ def test_nonexistent_request_raises(requests): requests.get(uuid4()) -@pytest.mark.gen_test def test_auto_approve_less_than_1m(requests, new_request): new_request.body = {"details_of_use": {"dollar_value": 999999}} - request = yield requests.submit(new_request) + request = requests.submit(new_request) assert request.status == 'approved' -@pytest.mark.gen_test def test_dont_auto_approve_if_dollar_value_is_1m_or_above(requests, new_request): new_request.body = {"details_of_use": {"dollar_value": 1000000}} - request = yield requests.submit(new_request) + request = requests.submit(new_request) assert request.status == 'submitted' -@pytest.mark.gen_test def test_dont_auto_approve_if_no_dollar_value_specified(requests, new_request): new_request.body = {"details_of_use": {}} - request = yield requests.submit(new_request) + request = requests.submit(new_request) assert request.status == 'submitted' diff --git a/tests/domain/test_roles.py b/tests/domain/test_roles.py index 1280292f..e513b09a 100644 --- a/tests/domain/test_roles.py +++ b/tests/domain/test_roles.py @@ -4,8 +4,8 @@ from atst.domain.exceptions import NotFoundError @pytest.fixture() -def roles_repo(db): - return Roles(db) +def roles_repo(session): + return Roles(session) def test_get_all_roles(roles_repo): diff --git a/tests/domain/test_task_orders.py b/tests/domain/test_task_orders.py index 9b7c0882..4062682b 100644 --- a/tests/domain/test_task_orders.py +++ b/tests/domain/test_task_orders.py @@ -7,15 +7,15 @@ from tests.factories import TaskOrderFactory @pytest.fixture() -def task_orders(db): - return TaskOrders(db) +def task_orders(session): + return TaskOrders(session) @pytest.fixture(scope="function") -def new_task_order(db): +def new_task_order(session): def make_task_order(**kwargs): to = TaskOrderFactory.create(**kwargs) - db.add(to) - db.commit() + session.add(to) + session.commit() return to diff --git a/tests/domain/test_users.py b/tests/domain/test_users.py index 9a2245b9..5121c369 100644 --- a/tests/domain/test_users.py +++ b/tests/domain/test_users.py @@ -6,8 +6,8 @@ from atst.domain.exceptions import NotFoundError, AlreadyExistsError @pytest.fixture() -def users_repo(db): - return Users(db) +def users_repo(session): + return Users(session) @pytest.fixture(scope="function") diff --git a/tests/domain/test_workspace_users.py b/tests/domain/test_workspace_users.py index f76bdf3e..f1e67840 100644 --- a/tests/domain/test_workspace_users.py +++ b/tests/domain/test_workspace_users.py @@ -6,13 +6,13 @@ from atst.domain.users import Users @pytest.fixture() -def users_repo(db): - return Users(db) +def users_repo(session): + return Users(session) @pytest.fixture() -def workspace_users_repo(db): - return WorkspaceUsers(db) +def workspace_users_repo(session): + return WorkspaceUsers(session) def test_can_create_new_workspace_user(users_repo, workspace_users_repo): diff --git a/tests/handlers/test_financial_verification.py b/tests/handlers/test_financial_verification.py deleted file mode 100644 index d84f76bd..00000000 --- a/tests/handlers/test_financial_verification.py +++ /dev/null @@ -1,89 +0,0 @@ -import re -import pytest -import tornado -import urllib -from tests.mocks import MOCK_REQUEST, MOCK_USER -from tests.factories import PENumberFactory - - -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_financial_verification.RequestFinancialVerification.get_current_user", lambda s: MOCK_USER - ) - monkeypatch.setattr( - "atst.handlers.request_financial_verification.RequestFinancialVerification.check_xsrf_cookie", lambda s: True - ) - monkeypatch.setattr("atst.forms.request.RequestForm.validate", lambda s: True) - monkeypatch.setattr("atst.domain.requests.Requests.get", lambda s, i: MOCK_REQUEST) - - @tornado.gen.coroutine - def submit_data(self, http_client, base_url, data): - response = yield http_client.fetch( - base_url + "/requests/verify/{}".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/verify" 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/financial_verification_submitted" - - @pytest.mark.gen_test - def test_submit_request_form_with_new_valid_pe_id(self, db, monkeypatch, http_client, base_url): - self._set_monkeypatches(monkeypatch) - pe = PENumberFactory.create(number="8675309U", description="sample PE number") - db.add(pe) - db.commit() - - data = dict(self.required_data) - data['pe_id'] = pe.number - - response = yield self.submit_data(http_client, base_url, data) - - assert response.code == 302 - assert response.headers.get("Location") == "/requests/financial_verification_submitted" diff --git a/tests/handlers/test_request_new.py b/tests/handlers/test_request_new.py deleted file mode 100644 index 8409fc13..00000000 --- a/tests/handlers/test_request_new.py +++ /dev/null @@ -1,54 +0,0 @@ -import re -import pytest -import tornado -import urllib -from tests.mocks import MOCK_USER -from tests.factories import RequestFactory - -ERROR_CLASS = "alert--error" -MOCK_REQUEST = RequestFactory.create( - creator=MOCK_USER["id"], - body={ - "financial_verification": { - "pe_id": "0203752A", - }, - } -) - -@pytest.mark.gen_test -def test_submit_invalid_request_form(monkeypatch, http_client, base_url): - 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 - ) - # this just needs to send a known invalid form value - response = yield http_client.fetch( - base_url + "/requests/new", - method="POST", - headers={"Content-Type": "application/x-www-form-urlencoded"}, - body="total_ram=5", - ) - assert response.effective_url == base_url + "/requests/new" - assert re.search(ERROR_CLASS, response.body.decode()) - - -@pytest.mark.gen_test -def test_submit_valid_request_form(monkeypatch, http_client, base_url): - 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) - - # this just needs to send a known invalid form value - response = yield http_client.fetch( - base_url + "/requests/new", - method="POST", - headers={"Content-Type": "application/x-www-form-urlencoded"}, - body="meaning=42", - ) - assert "/requests/new/2" in response.effective_url diff --git a/tests/handlers/test_request_submit.py b/tests/handlers/test_request_submit.py deleted file mode 100644 index 635ff157..00000000 --- a/tests/handlers/test_request_submit.py +++ /dev/null @@ -1,57 +0,0 @@ -import pytest -import tornado -from tests.mocks import MOCK_USER -from tests.factories import RequestFactory - - -@tornado.gen.coroutine -def _mock_func(*args, **kwargs): - return RequestFactory.create() - - -@pytest.mark.gen_test -def test_submit_reviewed_request(monkeypatch, http_client, base_url): - monkeypatch.setattr( - "atst.handlers.request_submit.RequestsSubmit.get_current_user", - lambda s: MOCK_USER, - ) - monkeypatch.setattr( - "atst.handlers.request_submit.RequestsSubmit.check_xsrf_cookie", lambda s: True - ) - monkeypatch.setattr("atst.domain.requests.Requests.get", _mock_func) - monkeypatch.setattr("atst.domain.requests.Requests.submit", _mock_func) - monkeypatch.setattr("atst.models.request.Request.status", "pending") - # this just needs to send a known invalid form value - response = yield http_client.fetch( - base_url + "/requests/submit/1", - method="POST", - headers={"Content-Type": "application/x-www-form-urlencoded"}, - body="", - raise_error=False, - follow_redirects=False, - ) - assert response.headers["Location"] == "/requests" - - -@pytest.mark.gen_test -def test_submit_autoapproved_reviewed_request(monkeypatch, http_client, base_url): - monkeypatch.setattr( - "atst.handlers.request_submit.RequestsSubmit.get_current_user", - lambda s: MOCK_USER, - ) - monkeypatch.setattr( - "atst.handlers.request_submit.RequestsSubmit.check_xsrf_cookie", lambda s: True - ) - monkeypatch.setattr("atst.domain.requests.Requests.get", _mock_func) - monkeypatch.setattr("atst.domain.requests.Requests.submit", _mock_func) - monkeypatch.setattr("atst.models.request.Request.status", "approved") - # this just needs to send a known invalid form value - response = yield http_client.fetch( - base_url + "/requests/submit/1", - method="POST", - headers={"Content-Type": "application/x-www-form-urlencoded"}, - body="", - raise_error=False, - follow_redirects=False, - ) - assert response.headers["Location"] == "/requests?modal=True" diff --git a/tests/routes/test_financial_verification.py b/tests/routes/test_financial_verification.py new file mode 100644 index 00000000..35502523 --- /dev/null +++ b/tests/routes/test_financial_verification.py @@ -0,0 +1,76 @@ +import re +import pytest +import tornado +import urllib +from tests.mocks import MOCK_REQUEST, MOCK_USER +from tests.factories import PENumberFactory + + +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.forms.financial.FinancialForm.validate", lambda s: True) + monkeypatch.setattr("atst.domain.requests.Requests.get", lambda i: MOCK_REQUEST) + + def submit_data(self, client, data): + response = client.post( + "/requests/verify/{}".format(MOCK_REQUEST.id), + headers={"Content-Type": "application/x-www-form-urlencoded"}, + data=urllib.parse.urlencode(data), + follow_redirects=False, + ) + return response + + def test_submit_request_form_with_invalid_pe_id(self, monkeypatch, client): + self._set_monkeypatches(monkeypatch) + + response = self.submit_data(client, self.required_data) + + assert "We couldn\'t find that PE number" in response.data.decode() + assert response.status_code == 200 + + def test_submit_request_form_with_unchanged_pe_id(self, monkeypatch, client): + self._set_monkeypatches(monkeypatch) + + data = dict(self.required_data) + data['pe_id'] = MOCK_REQUEST.body['financial_verification']['pe_id'] + + response = self.submit_data(client, data) + + assert response.status_code == 302 + assert "/requests/financial_verification_submitted" in response.headers.get("Location") + + def test_submit_request_form_with_new_valid_pe_id(self, session, monkeypatch, client): + self._set_monkeypatches(monkeypatch) + pe = PENumberFactory.create(number="8675309U", description="sample PE number") + session.add(pe) + session.commit() + + data = dict(self.required_data) + data['pe_id'] = pe.number + + response = self.submit_data(client, data) + + assert response.status_code == 302 + assert "/requests/financial_verification_submitted" in response.headers.get("Location") diff --git a/tests/routes/test_request_new.py b/tests/routes/test_request_new.py new file mode 100644 index 00000000..5e9b6c19 --- /dev/null +++ b/tests/routes/test_request_new.py @@ -0,0 +1,35 @@ +import re +import pytest +import urllib +from tests.mocks import MOCK_USER +from tests.factories import RequestFactory + +ERROR_CLASS = "alert--error" +MOCK_REQUEST = RequestFactory.create( + creator=MOCK_USER["id"], + body={ + "financial_verification": { + "pe_id": "0203752A", + }, + } +) + + +def test_submit_invalid_request_form(monkeypatch, client): + response = client.post( + "/requests/new/1", + headers={"Content-Type": "application/x-www-form-urlencoded"}, + data="total_ram=5", + ) + assert re.search(ERROR_CLASS, response.data.decode()) + + +def test_submit_valid_request_form(monkeypatch, client): + monkeypatch.setattr("atst.forms.request.RequestForm.validate", lambda s: True) + + response = client.post( + "/requests/new/1", + headers={"Content-Type": "application/x-www-form-urlencoded"}, + data="meaning=42", + ) + assert "/requests/new/2" in response.headers.get("Location") diff --git a/tests/routes/test_request_submit.py b/tests/routes/test_request_submit.py new file mode 100644 index 00000000..44078cb8 --- /dev/null +++ b/tests/routes/test_request_submit.py @@ -0,0 +1,37 @@ +import pytest +import tornado +from tests.mocks import MOCK_USER +from tests.factories import RequestFactory + + +def _mock_func(*args, **kwargs): + return RequestFactory.create() + + +def test_submit_reviewed_request(monkeypatch, client): + monkeypatch.setattr("atst.domain.requests.Requests.get", _mock_func) + monkeypatch.setattr("atst.domain.requests.Requests.submit", _mock_func) + monkeypatch.setattr("atst.models.request.Request.status", "pending") + # this just needs to send a known invalid form value + response = client.post( + "/requests/submit/1", + headers={"Content-Type": "application/x-www-form-urlencoded"}, + data="", + follow_redirects=False, + ) + assert "/requests" in response.headers["Location"] + assert "modal" not in response.headers["Location"] + + +def test_submit_autoapproved_reviewed_request(monkeypatch, client): + monkeypatch.setattr("atst.domain.requests.Requests.get", _mock_func) + monkeypatch.setattr("atst.domain.requests.Requests.submit", _mock_func) + monkeypatch.setattr("atst.models.request.Request.status", "approved") + # this just needs to send a known invalid form value + response = client.post( + "/requests/submit/1", + headers={"Content-Type": "application/x-www-form-urlencoded"}, + data="", + follow_redirects=False, + ) + assert "/requests?modal=True" in response.headers["Location"] diff --git a/tests/test_api_client.py b/tests/test_api_client.py deleted file mode 100644 index 5b1880cb..00000000 --- a/tests/test_api_client.py +++ /dev/null @@ -1,10 +0,0 @@ -import pytest - -from atst.api_client import ApiClient - - -@pytest.mark.gen_test -def test_api_client(http_client, base_url): - client = ApiClient(base_url) - response = yield client.get("") - assert response.code == 200 diff --git a/tests/test_auth.py b/tests/test_auth.py index 7480015e..91fdd741 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -1,81 +1,78 @@ import re import pytest -import tornado.web -import tornado.gen MOCK_USER = {"id": "438567dd-25fa-4d83-a8cc-8aa8366cb24a"} -@tornado.gen.coroutine def _fetch_user_info(c, t): return MOCK_USER -@pytest.mark.gen_test -def test_redirects_when_not_logged_in(http_client, base_url): - response = yield http_client.fetch( - base_url + "/home", raise_error=False, follow_redirects=False - ) - location = response.headers["Location"] - assert response.code == 302 - assert response.error - assert re.match("/\??", location) +@pytest.mark.skip +def test_redirects_when_not_logged_in(): + pass + # response = yield http_client.fetch( + # base_url + "/home", raise_error=False, follow_redirects=False + # ) + # location = response.headers["Location"] + # assert response.code == 302 + # assert response.error + # assert re.match("/\??", location) -@pytest.mark.gen_test -def test_redirects_when_session_does_not_exist(monkeypatch, http_client, base_url): - monkeypatch.setattr("atst.handlers.main.Main.get_secure_cookie", lambda s,c: 'stale cookie!') - response = yield http_client.fetch( - base_url + "/home", raise_error=False, follow_redirects=False - ) - location = response.headers["Location"] - cookie = response.headers._dict.get('Set-Cookie') - # should clear session cookie - assert 'atat=""' in cookie - assert response.code == 302 - assert response.error - assert re.match("/\??", location) +# @pytest.mark.skip +# def test_redirects_when_session_does_not_exist(): + # monkeypatch.setattr("atst.handlers.main.Main.get_secure_cookie", lambda s,c: 'stale cookie!') + # response = yield http_client.fetch( + # base_url + "/home", raise_error=False, follow_redirects=False + # ) + # location = response.headers["Location"] + # cookie = response.headers._dict.get('Set-Cookie') + # # should clear session cookie + # assert 'atat=""' in cookie + # assert response.code == 302 + # assert response.error + # assert re.match("/\??", location) -@pytest.mark.gen_test -def test_login_with_valid_bearer_token(app, monkeypatch, http_client, base_url): - monkeypatch.setattr("atst.handlers.login_redirect.LoginRedirect._fetch_user_info", _fetch_user_info) - response = yield http_client.fetch( - base_url + "/login-redirect?bearer-token=abc-123", - follow_redirects=False, - raise_error=False, - ) - assert response.headers["Set-Cookie"].startswith("atat") - assert response.headers["Location"] == "/home" - assert response.code == 302 - - -@pytest.mark.gen_test -def test_login_via_dev_endpoint(app, http_client, base_url): - response = yield http_client.fetch( - base_url + "/login-dev", raise_error=False, follow_redirects=False - ) - assert response.headers["Set-Cookie"].startswith("atat") - assert response.code == 302 - assert response.headers["Location"] == "/home" - - -@pytest.mark.gen_test -@pytest.mark.skip(reason="need to work out auth error user paths") -def test_login_with_invalid_bearer_token(http_client, base_url): - _response = yield http_client.fetch( - base_url + "/home", - raise_error=False, - headers={"Cookie": "bearer-token=anything"}, - ) - -@pytest.mark.gen_test -def test_valid_login_creates_session(app, monkeypatch, http_client, base_url): - monkeypatch.setattr("atst.handlers.login_redirect.LoginRedirect._fetch_user_info", _fetch_user_info) - assert len(app.sessions.sessions) == 0 - yield http_client.fetch( - base_url + "/login-redirect?bearer-token=abc-123", - follow_redirects=False, - raise_error=False, - ) - assert len(app.sessions.sessions) == 1 - session = list(app.sessions.sessions.values())[0] - assert "atat_permissions" in session["user"] - assert isinstance(session["user"]["atat_permissions"], list) +# @pytest.mark.skip +# def test_login_with_valid_bearer_token(): +# monkeypatch.setattr("atst.handlers.login_redirect.LoginRedirect._fetch_user_info", _fetch_user_info) +# response = client.fetch( +# base_url + "/login-redirect?bearer-token=abc-123", +# follow_redirects=False, +# raise_error=False, +# ) +# assert response.headers["Set-Cookie"].startswith("atat") +# assert response.headers["Location"] == "/home" +# assert response.code == 302 +# +# +# @pytest.mark.skip +# def test_login_via_dev_endpoint(): +# response = yield http_client.fetch( +# base_url + "/login-dev", raise_error=False, follow_redirects=False +# ) +# assert response.headers["Set-Cookie"].startswith("atat") +# assert response.code == 302 +# assert response.headers["Location"] == "/home" +# +# +# @pytest.mark.skip +# def test_login_with_invalid_bearer_token(): +# _response = yield http_client.fetch( +# base_url + "/home", +# raise_error=False, +# headers={"Cookie": "bearer-token=anything"}, +# ) +# +# @pytest.mark.skip +# def test_valid_login_creates_session(): +# monkeypatch.setattr("atst.handlers.login_redirect.LoginRedirect._fetch_user_info", _fetch_user_info) +# assert len(app.sessions.sessions) == 0 +# yield http_client.fetch( +# base_url + "/login-redirect?bearer-token=abc-123", +# follow_redirects=False, +# raise_error=False, +# ) +# assert len(app.sessions.sessions) == 1 +# session = list(app.sessions.sessions.values())[0] +# assert "atat_permissions" in session["user"] +# assert isinstance(session["user"]["atat_permissions"], list) diff --git a/tests/test_basic.py b/tests/test_basic.py index dd2b9a21..67df4c9f 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -1,7 +1,3 @@ -import pytest - - -@pytest.mark.gen_test -def test_hello_world(http_client, base_url): - response = yield http_client.fetch(base_url) - assert response.code == 200 +def test_hello_world(client): + response = client.get("/") + assert response.status_code == 200 diff --git a/tests/test_integration.py b/tests/test_integration.py index aa97c2d6..e8fa6348 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -1,13 +1,14 @@ import pytest -import tornado from tests.mocks import MOCK_USER -from atst.handlers.request_new import JEDIRequestFlow +from atst.routes.requests.jedi_request_flow import JEDIRequestFlow -SCREENS = JEDIRequestFlow(None, None, 3).screens +@pytest.fixture +def screens(app): + return JEDIRequestFlow(3).screens -@pytest.mark.gen_test -def test_stepthrough_request_form(monkeypatch, http_client, base_url): +@pytest.mark.skip() +def test_stepthrough_request_form(monkeypatch, screens, client): monkeypatch.setattr( "atst.handlers.request_new.RequestNew.get_current_user", lambda s: MOCK_USER ) @@ -18,29 +19,28 @@ def test_stepthrough_request_form(monkeypatch, http_client, base_url): "atst.handlers.request_new.JEDIRequestFlow.validate", lambda s: True ) - @tornado.gen.coroutine def take_a_step(inc, req=None): - req_url = base_url + "/requests/new/{}".format(inc) + req_url = "/requests/new/{}".format(inc) if req: req_url += "/" + req - response = yield http_client.fetch( + response = client.post( req_url, - method="POST", headers={"Content-Type": "application/x-www-form-urlencoded"}, - body="meaning=42", + data="meaning=42", ) return response # GET the initial form - response = yield http_client.fetch(base_url + "/requests/new", method="GET") - assert SCREENS[0]["title"] in response.body.decode() + response = client.get("/requests/new") + assert screens[0]["title"] in response.data.decode() # POST to each of the form pages up until review and submit req_id = None - for i in range(1, len(SCREENS)): - resp = yield take_a_step(i, req=req_id) + for i in range(1, len(screens)): + resp = take_a_step(i, req=req_id) + __import__('ipdb').set_trace() req_id = resp.effective_url.split("/")[-1] - screen_title = SCREENS[i]["title"].replace("&", "&") + screen_title = screens[i]["title"].replace("&", "&") assert "/requests/new/{}/{}".format(i + 1, req_id) in resp.effective_url - assert screen_title in resp.body.decode() + assert screen_title in resp.data.decode() diff --git a/tests/test_routes.py b/tests/test_routes.py index e6a9122c..fedffb21 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -1,18 +1,19 @@ import pytest -@pytest.mark.gen_test -def test_routes(http_client, base_url): +def test_routes(client): for path in ( "/", "/home", "/workspaces", "/requests", "/requests/new", - "/requests/new/1", + "/requests/new/2", "/users", "/reports", "/calculator", ): - response = yield http_client.fetch(base_url + path) - assert response.code == 200 + response = client.get(path) + if response.status_code == 404: + __import__('ipdb').set_trace() + assert response.status_code == 200 From 8ec27cbb31a043dccee1c74b90da20fbc643bae4 Mon Sep 17 00:00:00 2001 From: dandds Date: Thu, 2 Aug 2018 16:14:18 -0400 Subject: [PATCH 42/43] pylint fixes --- atst/app.py | 4 ++-- atst/assets.py | 1 - atst/domain/requests.py | 1 - atst/handlers/request_financial_verification.py | 1 + atst/routes/requests/jedi_request_flow.py | 1 - atst/routes/workspaces.py | 2 +- 6 files changed, 4 insertions(+), 6 deletions(-) diff --git a/atst/app.py b/atst/app.py index 54ad7f87..ca6ad7e2 100644 --- a/atst/app.py +++ b/atst/app.py @@ -1,7 +1,7 @@ import os import re from configparser import ConfigParser -from flask import Flask, request, g, session +from flask import Flask, request, g from unipath import Path from atst.database import db @@ -40,7 +40,7 @@ def make_app(config): def make_flask_callbacks(app): @app.before_request - def set_globals(): + def _set_globals(): g.navigationContext = ( "workspace" if re.match("\/workspaces\/[A-Za-z0-9]*", request.url) diff --git a/atst/assets.py b/atst/assets.py index aef6b35e..72ad5d5e 100644 --- a/atst/assets.py +++ b/atst/assets.py @@ -1,5 +1,4 @@ from flask_assets import Environment, Bundle -from atst.home import home environment = Environment() diff --git a/atst/domain/requests.py b/atst/domain/requests.py index cdb7f5c0..5c04d3c7 100644 --- a/atst/domain/requests.py +++ b/atst/domain/requests.py @@ -1,4 +1,3 @@ -import tornado.gen from sqlalchemy import exists, and_ from sqlalchemy.orm.exc import NoResultFound from sqlalchemy.orm.attributes import flag_modified diff --git a/atst/handlers/request_financial_verification.py b/atst/handlers/request_financial_verification.py index ea4dcb35..e6061c39 100644 --- a/atst/handlers/request_financial_verification.py +++ b/atst/handlers/request_financial_verification.py @@ -47,6 +47,7 @@ class RequestFinancialVerification(BaseHandler): if form.validate(): yield self.update_request(request_id, form.data) + # pylint: disable=E1121 valid = yield form.perform_extra_validation( existing_request.body.get("financial_verification"), self.pe_numbers_repo, diff --git a/atst/routes/requests/jedi_request_flow.py b/atst/routes/requests/jedi_request_flow.py index de60fb29..e1c646e6 100644 --- a/atst/routes/requests/jedi_request_flow.py +++ b/atst/routes/requests/jedi_request_flow.py @@ -1,7 +1,6 @@ from collections import defaultdict from atst.domain.requests import Requests -from atst.forms.financial import FinancialForm from atst.forms.request import RequestForm from atst.forms.org import OrgForm from atst.forms.poc import POCForm diff --git a/atst/routes/workspaces.py b/atst/routes/workspaces.py index 43b3b050..e614c08c 100644 --- a/atst/routes/workspaces.py +++ b/atst/routes/workspaces.py @@ -1,7 +1,7 @@ from flask import Blueprint, render_template from atst.domain.workspaces import Projects, Members -from atst.database import db + bp = Blueprint("workspaces", __name__) From 649ac89e2e13eced31ca1e451e8ecb53673240c1 Mon Sep 17 00:00:00 2001 From: dandds Date: Thu, 2 Aug 2018 16:20:54 -0400 Subject: [PATCH 43/43] configure test environment --- README.md | 6 ++++++ atst/app.py | 2 +- atst/ui_methods.py | 2 +- script/test | 2 ++ 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 455d5baa..5bfee424 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,12 @@ To log in as one of them, navigate to `/login-dev?username=`. Fo ## Testing +Tests require a test database: + +``` +createdb atat_test +``` + To run lint, static analysis, and unit tests: script/test diff --git a/atst/app.py b/atst/app.py index ca6ad7e2..5e45af43 100644 --- a/atst/app.py +++ b/atst/app.py @@ -12,7 +12,7 @@ from atst.routes.workspaces import bp as workspace_routes from atst.routes.requests import requests_bp -ENV = os.getenv("TORNADO_ENV", "dev") +ENV = os.getenv("FLASK_ENV", "dev") def make_app(config): diff --git a/atst/ui_methods.py b/atst/ui_methods.py index c863ee55..a3ccbbba 100644 --- a/atst/ui_methods.py +++ b/atst/ui_methods.py @@ -11,7 +11,7 @@ def navigationContext(self): def dev(self): - return os.getenv("TORNADO_ENV", "dev") == "dev" + return os.getenv("FLASK_ENV", "dev") == "dev" def matchesPath(self, href): diff --git a/script/test b/script/test index 1f6c3380..ceb1c5c2 100755 --- a/script/test +++ b/script/test @@ -4,6 +4,8 @@ source "$(dirname "${0}")"/../script/include/global_header.inc.sh +export FLASK_ENV=test + # Define all relevant python files and directories for this app PYTHON_FILES="./app.py ./atst ./config"