From 2f673bbe810fefffdd2d090b4367f71055e8137b Mon Sep 17 00:00:00 2001 From: "Jay R. Newlin" Date: Tue, 5 Feb 2019 16:00:36 -0500 Subject: [PATCH 001/174] Added note about Python versions --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7f6a28c2..d770eb58 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,10 @@ Before running the setup scripts, a couple of dependencies need to be installed locally: * `python` == 3.6 - Python version 3.6 must be installed on your machine before installing `pipenv`. + Python version 3.6 **must** be installed on your machine before installing `pipenv`. You can download Python 3.6 [from python.org](https://www.python.org/downloads/) - or use your preferred system package manager. + or use your preferred system package manager. Multiple versions of Python can exist on one + computer, but 3.6 is required for ATAT. * `pipenv` ATST requires `pipenv` to be installed for python dependency management. `pipenv` From 54a855962911b11ca595f8ddcc666fe60ebbfc85 Mon Sep 17 00:00:00 2001 From: "Jay R. Newlin" Date: Tue, 5 Feb 2019 17:18:04 -0500 Subject: [PATCH 002/174] Corrected missing punctuation --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d770eb58..c0e6bb2f 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ locally: * `pipenv` ATST requires `pipenv` to be installed for python dependency management. `pipenv` will create the virtual environment that the app requires. [See - `pipenv`'s documentation for instructions on installing `pipenv]( + `pipenv`'s documentation for instructions on installing `pipenv`]( https://pipenv.readthedocs.io/en/latest/install/#installing-pipenv). * `yarn` From dbf7e1e130ba7d3b16f2540398dec86870f8f42b Mon Sep 17 00:00:00 2001 From: "Jay R. Newlin" Date: Tue, 5 Feb 2019 17:25:18 -0500 Subject: [PATCH 003/174] Added commentary about installing PostgreSQL --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c0e6bb2f..ad2f1bb7 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,10 @@ locally: * `postgres` >= 9.6 ATST requires a PostgreSQL instance (>= 9.6) for persistence. Have PostgresSQL installed - and running on the default port of 5432. You can verify that PostgresSQL is running + and running on the default port of 5432. (A good resource for installing and running + PostgreSQL for Macs is [Postgres.app](https://postgresapp.com/). Follow the instructions, + including the optional Step 3, and add `/Applications/Postgres.app/Contents/Versions/latest/bin` + to your `PATH` environment variable.) You can verify that PostgresSQL is running by executing `psql` and ensuring that a connection is successfully made. * `redis` From fb5d3f6ee06cbeb6c01bb049c29707fded8eaeaa Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Wed, 6 Feb 2019 15:23:42 -0500 Subject: [PATCH 004/174] Update styling --- styles/components/_footer.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/styles/components/_footer.scss b/styles/components/_footer.scss index f6871dd5..0915d67b 100644 --- a/styles/components/_footer.scss +++ b/styles/components/_footer.scss @@ -1,6 +1,6 @@ .app-footer { background-color: $color-white; - border-top: 1px solid $color-black; + border-top: 1px solid $color-gray-lightest; display: flex; flex-direction: row; justify-content: space-between; @@ -18,6 +18,7 @@ .app-footer__info__link { margin: (-$gap * 2) (-$gap); + font-weight: normal; .icon--footer { @include icon-size(16); From 9dc74f7d9332d7501c42b0c79e1517c50ce29f9d Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Wed, 6 Feb 2019 15:55:15 -0500 Subject: [PATCH 005/174] Update redirect to point to portfolios index --- atst/routes/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/atst/routes/__init__.py b/atst/routes/__init__.py index ac05105f..56473e3d 100644 --- a/atst/routes/__init__.py +++ b/atst/routes/__init__.py @@ -55,7 +55,7 @@ def home(): num_portfolios = len(user.portfolio_roles) if num_portfolios == 0: - return redirect(url_for("requests.requests_index")) + return redirect(url_for("portfolios.portfolios")) elif num_portfolios == 1: portfolio_role = user.portfolio_roles[0] portfolio_id = portfolio_role.portfolio.id From 0b9d00e100edf60810db81ebb301bd0840a8f86d Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Wed, 6 Feb 2019 14:40:56 -0500 Subject: [PATCH 006/174] Add templates for breadcrumbs/headers in portfolio page --- templates/portfolios/base.html | 10 ++++++---- templates/portfolios/breadcrumbs.html | 1 + templates/portfolios/header.html | 1 + 3 files changed, 8 insertions(+), 4 deletions(-) create mode 100644 templates/portfolios/breadcrumbs.html create mode 100644 templates/portfolios/header.html diff --git a/templates/portfolios/base.html b/templates/portfolios/base.html index 280e0d0b..9178a9e8 100644 --- a/templates/portfolios/base.html +++ b/templates/portfolios/base.html @@ -3,11 +3,13 @@ {% block content %}
-
- {% include 'navigation/portfolio_navigation.html' %} -
-
+ {% block portfolio_breadcrumbs %} + {% include "portfolios/breadcrumbs.html" %} + {% endblock %} + {% block portfolio_header %} + {% include "portfolios/header.html" %} + {% endblock %} {% block portfolio_content %}{% endblock %}
diff --git a/templates/portfolios/breadcrumbs.html b/templates/portfolios/breadcrumbs.html new file mode 100644 index 00000000..cc8894de --- /dev/null +++ b/templates/portfolios/breadcrumbs.html @@ -0,0 +1 @@ +{{ portfolio.name }} diff --git a/templates/portfolios/header.html b/templates/portfolios/header.html new file mode 100644 index 00000000..cc8894de --- /dev/null +++ b/templates/portfolios/header.html @@ -0,0 +1 @@ +{{ portfolio.name }} From 159b5f9838e9bb18826bc3c3809210271de4093d Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Wed, 6 Feb 2019 14:59:42 -0500 Subject: [PATCH 007/174] Add rudimentary breadcrumb at top of page --- styles/components/_portfolio_layout.scss | 9 +++++++++ styles/core/_variables.scss | 2 +- templates/portfolios/breadcrumbs.html | 14 +++++++++++++- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/styles/components/_portfolio_layout.scss b/styles/components/_portfolio_layout.scss index e74481a3..5872aa58 100644 --- a/styles/components/_portfolio_layout.scss +++ b/styles/components/_portfolio_layout.scss @@ -4,6 +4,15 @@ } } +.portfolio-breadcrumbs { + margin-bottom: $gap * 2; + color: $color-gray-medium; + font-size: $h5-font-size; + .icon { + @include icon-color($color-gray-medium); + } +} + .portfolio-navigation { @include panel-margin; margin-bottom: $gap * 4; diff --git a/styles/core/_variables.scss b/styles/core/_variables.scss index 3586faa9..95cfba10 100644 --- a/styles/core/_variables.scss +++ b/styles/core/_variables.scss @@ -62,7 +62,7 @@ $color-black-light: #212121; $color-gray-dark: #323a45; $color-gray: #5b616b; -$color-gray-medium: #757575; +$color-gray-medium: #9b9b9b; $color-gray-light: #aeb0b5; $color-gray-lighter: #d6d7d9; $color-gray-lightest: #f1f1f1; diff --git a/templates/portfolios/breadcrumbs.html b/templates/portfolios/breadcrumbs.html index cc8894de..b6aec6c1 100644 --- a/templates/portfolios/breadcrumbs.html +++ b/templates/portfolios/breadcrumbs.html @@ -1 +1,13 @@ -{{ portfolio.name }} +{% from "components/icon.html" import Icon %} + +
+
+ {{ Icon("briefcase") }} + + {{ portfolio.name }} Portfolio + +
+
+
+
From 9986fd4289b2c3ada9074729001ac5669df9385a Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Wed, 6 Feb 2019 15:13:52 -0500 Subject: [PATCH 008/174] Add some separators on portfolio pages --- styles/components/_global_layout.scss | 2 +- styles/components/_portfolio_layout.scss | 7 +++++++ templates/portfolios/base.html | 2 ++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/styles/components/_global_layout.scss b/styles/components/_global_layout.scss index a80b4ca5..b46526b5 100644 --- a/styles/components/_global_layout.scss +++ b/styles/components/_global_layout.scss @@ -1,5 +1,5 @@ #app-root { - background-color: $color-gray-lightest; + background-color: $color-white; display: flex; flex-direction: column; justify-content: flex-start; diff --git a/styles/components/_portfolio_layout.scss b/styles/components/_portfolio_layout.scss index 5872aa58..fe1e941e 100644 --- a/styles/components/_portfolio_layout.scss +++ b/styles/components/_portfolio_layout.scss @@ -2,6 +2,13 @@ @include media($large-screen) { @include grid-row; } + + .line { + box-sizing: border-box; + height: 2px; + width: 100%; + border: 1px solid $color-gray-lightest; + } } .portfolio-breadcrumbs { diff --git a/templates/portfolios/base.html b/templates/portfolios/base.html index 9178a9e8..61eef44e 100644 --- a/templates/portfolios/base.html +++ b/templates/portfolios/base.html @@ -7,9 +7,11 @@ {% block portfolio_breadcrumbs %} {% include "portfolios/breadcrumbs.html" %} {% endblock %} +
{% block portfolio_header %} {% include "portfolios/header.html" %} {% endblock %} +
{% block portfolio_content %}{% endblock %} From 4862bf078dd0ed1b77cb9984f08d8d37681df5b0 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Wed, 6 Feb 2019 20:33:27 -0500 Subject: [PATCH 009/174] Initial header row in portfolio nav --- styles/components/_portfolio_layout.scss | 35 ++++++++++++++++++ templates/portfolios/header.html | 45 +++++++++++++++++++++++- 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/styles/components/_portfolio_layout.scss b/styles/components/_portfolio_layout.scss index fe1e941e..40efda84 100644 --- a/styles/components/_portfolio_layout.scss +++ b/styles/components/_portfolio_layout.scss @@ -20,6 +20,41 @@ } } +.portfolio-header { + margin: 2 * $gap 0; + + .portfolio-header__name { + @include h1; + } + + .portfolio-header__budget { + font-size: $small-font-size; + align-items: center; + + button { + margin: 0; + padding: 0; + } + + .portfolio-header__budget--dollars { + font-size: $h2-font-size; + font-weight: bold; + } + } + + .links { + font-size: $small-font-size; + + .link--icon { + text-align: center; + } + + .icon { + @include icon-size(20); + } + } +} + .portfolio-navigation { @include panel-margin; margin-bottom: $gap * 4; diff --git a/templates/portfolios/header.html b/templates/portfolios/header.html index cc8894de..f5d8d2bf 100644 --- a/templates/portfolios/header.html +++ b/templates/portfolios/header.html @@ -1 +1,44 @@ -{{ portfolio.name }} +{% from "components/icon.html" import Icon %} + +{% macro Link(icon, text, url, active=False) %} + +
+ + +
+
+{% endmacro %} + +
+
+
+ {{ portfolio.name }} +
+
+ Available budget + + + {{ portfolio.task_orders | sum(attribute='budget') | dollars }} + +
+
+ +
From 2d971a458b5de47a2d73324e4e788159b0231e16 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Wed, 6 Feb 2019 20:49:38 -0500 Subject: [PATCH 010/174] Indicate active section in portfolio sub-nav --- styles/components/_portfolio_layout.scss | 20 +++++++++++++++----- templates/portfolios/header.html | 21 ++++++++++++--------- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/styles/components/_portfolio_layout.scss b/styles/components/_portfolio_layout.scss index 40efda84..fa02d16f 100644 --- a/styles/components/_portfolio_layout.scss +++ b/styles/components/_portfolio_layout.scss @@ -45,13 +45,23 @@ .links { font-size: $small-font-size; - .link--icon { - text-align: center; + .icon-link { + &.active { + color: $color-gray; + .icon { + @include icon-color($color-gray); + } + } + + .icon-link--icon { + text-align: center; + } + + .icon { + @include icon-size(20); + } } - .icon { - @include icon-size(20); - } } } diff --git a/templates/portfolios/header.html b/templates/portfolios/header.html index f5d8d2bf..e59a9f23 100644 --- a/templates/portfolios/header.html +++ b/templates/portfolios/header.html @@ -1,10 +1,10 @@ {% from "components/icon.html" import Icon %} {% macro Link(icon, text, url, active=False) %} - +
- - + +
{% endmacro %} @@ -28,17 +28,20 @@ {{ Link( icon='chart', text='Reports', - url=url_for("portfolios.portfolio_reports", portfolio_id=portfolio.id)) - }} + url=url_for("portfolios.portfolio_reports", portfolio_id=portfolio.id), + active=request.url_rule.endpoint == "portfolios.portfolio_reports", + ) }} {{ Link( icon='dollar-sign', text='Funding', - url=url_for("portfolios.portfolio_funding", portfolio_id=portfolio.id)) - }} + url=url_for("portfolios.portfolio_funding", portfolio_id=portfolio.id), + active=request.url_rule.endpoint == "portfolios.portfolio_funding", + ) }} {{ Link( icon='time', text='Admin', - url=url_for("portfolios.portfolio", portfolio_id=portfolio.id)) - }} + url=url_for("portfolios.portfolio", portfolio_id=portfolio.id), + active=request.url_rule.endpoint == "portfolios.portfolio", + ) }} From aa8cc45fd562fa7e9010ad1fff0afe496ea64287 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Wed, 6 Feb 2019 21:42:11 -0500 Subject: [PATCH 011/174] Update breadcrumbs in portfolio header nav --- atst/routes/portfolios/__init__.py | 17 ++++++++++++++++- styles/components/_portfolio_layout.scss | 21 +++++++++++++++++++++ templates/portfolios/breadcrumbs.html | 10 ++++++++-- 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/atst/routes/portfolios/__init__.py b/atst/routes/portfolios/__init__.py index ad935378..574e9072 100644 --- a/atst/routes/portfolios/__init__.py +++ b/atst/routes/portfolios/__init__.py @@ -13,6 +13,16 @@ from atst.domain.authz import Authorization from atst.models.permissions import Permissions +def get_breadcrumb_from_request(request): + if request.url_rule.rule.startswith("/portfolios//task_order"): + return "Funding" + if request.url_rule.endpoint == "portfolios.portfolio": + return "Admin" + if request.url_rule.endpoint == "portfolios.portfolio_reports": + return "Reports" + return None + + @portfolios_bp.context_processor def portfolio(): portfolio = None @@ -31,4 +41,9 @@ def portfolio(): ) return False - return {"portfolio": portfolio, "permissions": Permissions, "user_can": user_can} + return { + "portfolio": portfolio, + "permissions": Permissions, + "user_can": user_can, + "secondary_breadcrumb": get_breadcrumb_from_request(http_request), + } diff --git a/styles/components/_portfolio_layout.scss b/styles/components/_portfolio_layout.scss index fa02d16f..eb39a27f 100644 --- a/styles/components/_portfolio_layout.scss +++ b/styles/components/_portfolio_layout.scss @@ -15,9 +15,30 @@ margin-bottom: $gap * 2; color: $color-gray-medium; font-size: $h5-font-size; + + .icon-link { + color: $color-gray-medium; + font-weight: normal; + } + + .icon--tiny { + padding: $gap 0; + } + .icon { @include icon-color($color-gray-medium); } + + .portfolio-breadcrumbs__crumb { + .icon { + @include icon-color($color-blue); + } + + .icon-link { + color: $color-blue; + pointer-events: none; + } + } } .portfolio-header { diff --git a/templates/portfolios/breadcrumbs.html b/templates/portfolios/breadcrumbs.html index b6aec6c1..87d82005 100644 --- a/templates/portfolios/breadcrumbs.html +++ b/templates/portfolios/breadcrumbs.html @@ -1,13 +1,19 @@ {% from "components/icon.html" import Icon %}
- +
+ {% if secondary_breadcrumb %} + {{ Icon("caret_right", classes="icon--tiny") }} + + {% endif %}
From 7904569a0db393388fe1bd479fec9e3c8ebbc3b8 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Wed, 6 Feb 2019 21:45:08 -0500 Subject: [PATCH 012/174] Add in some permission checks in portfolio nav header --- templates/portfolios/header.html | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/templates/portfolios/header.html b/templates/portfolios/header.html index e59a9f23..dbec6c77 100644 --- a/templates/portfolios/header.html +++ b/templates/portfolios/header.html @@ -25,23 +25,27 @@ From 09818720c19ab2b7efe0a518fffdffcb98562549 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Wed, 6 Feb 2019 21:49:04 -0500 Subject: [PATCH 013/174] Give portfolio content some head space --- styles/components/_portfolio_layout.scss | 5 ++++- templates/portfolios/base.html | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/styles/components/_portfolio_layout.scss b/styles/components/_portfolio_layout.scss index eb39a27f..d437a667 100644 --- a/styles/components/_portfolio_layout.scss +++ b/styles/components/_portfolio_layout.scss @@ -82,10 +82,13 @@ @include icon-size(20); } } - } } +.portfolio-content { + margin-top: 6 * $gap; +} + .portfolio-navigation { @include panel-margin; margin-bottom: $gap * 4; diff --git a/templates/portfolios/base.html b/templates/portfolios/base.html index 61eef44e..92479d0f 100644 --- a/templates/portfolios/base.html +++ b/templates/portfolios/base.html @@ -12,7 +12,9 @@ {% include "portfolios/header.html" %} {% endblock %}
- {% block portfolio_content %}{% endblock %} +
+ {% block portfolio_content %}{% endblock %} +
From 0085b4e5088fd27046cd6a2dd690eea2f1d5bd70 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Wed, 6 Feb 2019 21:56:09 -0500 Subject: [PATCH 014/174] Temporarily skip some irrelevant tests --- tests/routes/portfolios/test_applications.py | 7 ++++++- tests/routes/portfolios/test_members.py | 3 +++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/routes/portfolios/test_applications.py b/tests/routes/portfolios/test_applications.py index 724a2a6d..5fe52ddf 100644 --- a/tests/routes/portfolios/test_applications.py +++ b/tests/routes/portfolios/test_applications.py @@ -1,3 +1,4 @@ +import pytest from flask import url_for from tests.factories import ( @@ -20,7 +21,7 @@ def test_user_with_permission_has_budget_report_link(client, user_session): user_session(portfolio.owner) response = client.get("/portfolios/{}/applications".format(portfolio.id)) assert ( - 'href="/portfolios/{}/reports"'.format(portfolio.id).encode() in response.data + "href='/portfolios/{}/reports'".format(portfolio.id).encode() in response.data ) @@ -38,6 +39,7 @@ def test_user_without_permission_has_no_budget_report_link(client, user_session) ) +@pytest.mark.skip(reason="Temporarily no add activity log link") def test_user_with_permission_has_activity_log_link(client, user_session): portfolio = PortfolioFactory.create() ccpo = UserFactory.from_atat_role("ccpo") @@ -69,6 +71,7 @@ def test_user_with_permission_has_activity_log_link(client, user_session): ) +@pytest.mark.skip(reason="Temporarily no add activity log link") def test_user_without_permission_has_no_activity_log_link(client, user_session): portfolio = PortfolioFactory.create() developer = UserFactory.create() @@ -87,6 +90,7 @@ def test_user_without_permission_has_no_activity_log_link(client, user_session): ) +@pytest.mark.skip(reason="Temporarily no add application link") def test_user_with_permission_has_add_application_link(client, user_session): portfolio = PortfolioFactory.create() user_session(portfolio.owner) @@ -97,6 +101,7 @@ def test_user_with_permission_has_add_application_link(client, user_session): ) +@pytest.mark.skip(reason="Temporarily no add application link") def test_user_without_permission_has_no_add_application_link(client, user_session): user = UserFactory.create() portfolio = PortfolioFactory.create() diff --git a/tests/routes/portfolios/test_members.py b/tests/routes/portfolios/test_members.py index 5cdf33bd..c8d97b61 100644 --- a/tests/routes/portfolios/test_members.py +++ b/tests/routes/portfolios/test_members.py @@ -1,3 +1,4 @@ +import pytest from flask import url_for from tests.factories import ( @@ -36,6 +37,7 @@ def create_portfolio_and_invite_user( return portfolio +@pytest.mark.skip(reason="Temporarily no add member link") def test_user_with_permission_has_add_member_link(client, user_session): portfolio = PortfolioFactory.create() user_session(portfolio.owner) @@ -46,6 +48,7 @@ def test_user_with_permission_has_add_member_link(client, user_session): ) +@pytest.mark.skip(reason="Temporarily no add member link") def test_user_without_permission_has_no_add_member_link(client, user_session): user = UserFactory.create() portfolio = PortfolioFactory.create() From 9fa3fd92f7d71f52a63f7bfbb4f78145d4f8f636 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Wed, 6 Feb 2019 22:03:37 -0500 Subject: [PATCH 015/174] Add missing icons --- static/icons/chart-pie.svg | 1 + static/icons/cog.svg | 1 + static/icons/home.svg | 1 + templates/portfolios/breadcrumbs.html | 2 +- templates/portfolios/header.html | 4 ++-- 5 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 static/icons/chart-pie.svg create mode 100644 static/icons/cog.svg create mode 100644 static/icons/home.svg diff --git a/static/icons/chart-pie.svg b/static/icons/chart-pie.svg new file mode 100644 index 00000000..e1b476bd --- /dev/null +++ b/static/icons/chart-pie.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/icons/cog.svg b/static/icons/cog.svg new file mode 100644 index 00000000..fb5bd35a --- /dev/null +++ b/static/icons/cog.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/icons/home.svg b/static/icons/home.svg new file mode 100644 index 00000000..27ee7ab0 --- /dev/null +++ b/static/icons/home.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/templates/portfolios/breadcrumbs.html b/templates/portfolios/breadcrumbs.html index 87d82005..bc4c16d7 100644 --- a/templates/portfolios/breadcrumbs.html +++ b/templates/portfolios/breadcrumbs.html @@ -2,7 +2,7 @@
- {{ Icon("briefcase") }} + {{ Icon("home") }} {{ portfolio.name }} Portfolio diff --git a/templates/portfolios/header.html b/templates/portfolios/header.html index dbec6c77..157cfd82 100644 --- a/templates/portfolios/header.html +++ b/templates/portfolios/header.html @@ -27,7 +27,7 @@ + {% endblock %} diff --git a/translations.yaml b/translations.yaml index 1f5202b6..1ea7e207 100644 --- a/translations.yaml +++ b/translations.yaml @@ -424,6 +424,7 @@ task_orders: ko_info_title: Contracting Officer (KO) Information ko_info_paragraph: Your KO will need to approve funding for this Task Order by logging into the JEDI Cloud Portal, submitting the Task Order documents within their official system of record, and electronically signing. You might want to work with your program Financial Manager to get your TO documents moving in the right dirction. skip_ko_label: "Skip for now (We'll remind you to enter one later)" + dod_id_tooltip: "The DoD ID is needed to verify the identity of the indicated officer or representative." cor_info_title: Contracting Officer Representative (COR) Information cor_info_paragraph: Your COR may assist in submitting the Task Order documents within their official system of record. They may also be invited to log in and manage the Task Order entry within the JEDI Cloud portal. so_info_title: Security Officer Information From d4fd3fb262d60c1787ef84246345cb6ed91ad0c4 Mon Sep 17 00:00:00 2001 From: Montana Date: Thu, 24 Jan 2019 16:37:46 -0500 Subject: [PATCH 021/174] Fix merge conflicts --- atst/domain/authz.py | 9 ++++ atst/routes/task_orders/new.py | 2 + templates/components/datepicker.html | 50 ++++++++++++++++++++ templates/portfolios/task_orders/review.html | 7 ++- templates/task_orders/new/app_info.html | 12 +++++ 5 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 templates/components/datepicker.html diff --git a/atst/domain/authz.py b/atst/domain/authz.py index de3c2156..f0c44afe 100644 --- a/atst/domain/authz.py +++ b/atst/domain/authz.py @@ -36,6 +36,15 @@ class Authorization(object): def is_ccpo(cls, user): return user.atat_role.name == "ccpo" + @classmethod + def check_is_mo_or_cor(cls, user, task_order): + if ( + task_order.contracting_officer_representative != user + and task_order.creator != user + ): + message = "build Task Order {}".format(task_order.id) + raise UnauthorizedError(user, message) + @classmethod def check_is_ko(cls, user, task_order): if task_order.contracting_officer != user: diff --git a/atst/routes/task_orders/new.py b/atst/routes/task_orders/new.py index 8892ba05..62647ce0 100644 --- a/atst/routes/task_orders/new.py +++ b/atst/routes/task_orders/new.py @@ -261,6 +261,7 @@ def get_started(): @task_orders_bp.route("/portfolios//task_orders/new/") def new(screen, task_order_id=None, portfolio_id=None): workflow = ShowTaskOrderWorkflow(g.current_user, screen, task_order_id) + Authorization.check_is_mo_or_cor(g.current_user, task_order) return render_template( workflow.template, current=screen, @@ -283,6 +284,7 @@ def update(screen, task_order_id=None, portfolio_id=None): workflow = UpdateTaskOrderWorkflow( g.current_user, form_data, screen, task_order_id, portfolio_id ) + Authorization.check_is_mo_or_cor(g.current_user, task_order) if workflow.validate(): workflow.update() return redirect( diff --git a/templates/components/datepicker.html b/templates/components/datepicker.html new file mode 100644 index 00000000..c597aeb5 --- /dev/null +++ b/templates/components/datepicker.html @@ -0,0 +1,50 @@ +{% from "components/icon.html" import Icon %} + +{% macro DatePicker(field) -%} + + +
+ + + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ {{ Icon("ok", classes="icon--green") }} +
+ +
+
+ +{%- endmacro %} diff --git a/templates/portfolios/task_orders/review.html b/templates/portfolios/task_orders/review.html index 61b50e8c..73e43186 100644 --- a/templates/portfolios/task_orders/review.html +++ b/templates/portfolios/task_orders/review.html @@ -16,8 +16,11 @@ {% include "fragments/flash.html" %} -
- {{ form.csrf_token }} + {% block form_action %} + {% if task_order_id %} + + {% endif %} + {% endblock %} {% block form %} diff --git a/templates/task_orders/new/app_info.html b/templates/task_orders/new/app_info.html index a5cd23d7..fc819d93 100644 --- a/templates/task_orders/new/app_info.html +++ b/templates/task_orders/new/app_info.html @@ -4,6 +4,7 @@ {% from "components/options_input.html" import OptionsInput %} {% from "components/date_input.html" import DateInput %} {% from "components/multi_checkbox_input.html" import MultiCheckboxInput %} +{% from "components/datepicker.html" import DatePicker %} {% block heading %} {{ "task_orders.new.app_info.section_title"| translate }} @@ -11,6 +12,17 @@ {% block form %} +
+ + + For example: 04 28 1986 + + {{ DatePicker() }} +
+ +

{{ "task_orders.new.app_info.basic_info_title"| translate }}

{{ TextInput(form.portfolio_name, placeholder="The name of your office or organization", validation="portfolioName") }} From ce2b4b6ea12e0ea65479ee3da7afdbc987af009a Mon Sep 17 00:00:00 2001 From: Montana Date: Tue, 5 Feb 2019 09:06:23 -0500 Subject: [PATCH 022/174] Add pdf column and uploader --- .../1f690989e38e_add_pdf_to_task_order.py | 36 +++++++++++++ atst/domain/authz.py | 9 ---- atst/models/task_order.py | 21 +++++++- atst/routes/task_orders/new.py | 2 - js/components/upload.js | 42 ++++++++++++++++ js/index.js | 2 + templates/components/datepicker.html | 50 ------------------- templates/portfolios/task_orders/review.html | 33 +++++++++--- templates/task_orders/new/app_info.html | 11 ---- 9 files changed, 124 insertions(+), 82 deletions(-) create mode 100644 alembic/versions/1f690989e38e_add_pdf_to_task_order.py create mode 100644 js/components/upload.js delete mode 100644 templates/components/datepicker.html diff --git a/alembic/versions/1f690989e38e_add_pdf_to_task_order.py b/alembic/versions/1f690989e38e_add_pdf_to_task_order.py new file mode 100644 index 00000000..a7d30c9a --- /dev/null +++ b/alembic/versions/1f690989e38e_add_pdf_to_task_order.py @@ -0,0 +1,36 @@ +"""Add PDF to Task Order + +Revision ID: 1f690989e38e +Revises: 0ff4c31c4d28 +Create Date: 2019-02-04 15:56:57.642156 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = '1f690989e38e' +down_revision = '0ff4c31c4d28' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('task_orders', sa.Column('pdf_attachment_id', postgresql.UUID(as_uuid=True), nullable=True)) + op.drop_constraint('task_orders_attachments_attachment_id', 'task_orders', type_='foreignkey') + op.alter_column('task_orders', 'attachment_id', new_column_name='csp_attachment_id') + op.create_foreign_key(None, 'task_orders', 'attachments', ['pdf_attachment_id'], ['id']) + op.create_foreign_key(None, 'task_orders', 'attachments', ['csp_attachment_id'], ['id']) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'task_orders', type_='foreignkey') + op.drop_constraint(None, 'task_orders', type_='foreignkey') + op.alter_column('task_orders', 'csp_attachment_id', new_column_name='attachment_id') + op.create_foreign_key('task_orders_attachments_attachment_id', 'task_orders', 'attachments', ['attachment_id'], ['id']) + op.drop_column('task_orders', 'pdf_attachment_id') + # ### end Alembic commands ### diff --git a/atst/domain/authz.py b/atst/domain/authz.py index f0c44afe..de3c2156 100644 --- a/atst/domain/authz.py +++ b/atst/domain/authz.py @@ -36,15 +36,6 @@ class Authorization(object): def is_ccpo(cls, user): return user.atat_role.name == "ccpo" - @classmethod - def check_is_mo_or_cor(cls, user, task_order): - if ( - task_order.contracting_officer_representative != user - and task_order.creator != user - ): - message = "build Task Order {}".format(task_order.id) - raise UnauthorizedError(user, message) - @classmethod def check_is_ko(cls, user, task_order): if task_order.contracting_officer != user: diff --git a/atst/models/task_order.py b/atst/models/task_order.py index 6ea523e2..60cb2ac9 100644 --- a/atst/models/task_order.py +++ b/atst/models/task_order.py @@ -51,8 +51,8 @@ class TaskOrder(Base, mixins.TimestampsMixin): start_date = Column(Date) # Period of Performance end_date = Column(Date) performance_length = Column(Integer) - attachment_id = Column(ForeignKey("attachments.id")) - _csp_estimate = relationship("Attachment") + csp_attachment_id = Column(ForeignKey("attachments.id")) + _csp_estimate = relationship("Attachment", foreign_keys=[csp_attachment_id]) clin_01 = Column(Numeric(scale=2)) clin_02 = Column(Numeric(scale=2)) clin_03 = Column(Numeric(scale=2)) @@ -72,6 +72,8 @@ class TaskOrder(Base, mixins.TimestampsMixin): so_email = Column(String) # Email so_phone_number = Column(String) # Phone Number so_dod_id = Column(String) # DOD ID + pdf_attachment_id = Column(ForeignKey("attachments.id")) + _pdf = relationship("Attachment", foreign_keys=[pdf_attachment_id]) number = Column(String, unique=True) # Task Order Number loa = Column(String) # Line of Accounting (LOA) custom_clauses = Column(String) # Custom Clauses @@ -93,6 +95,21 @@ class TaskOrder(Base, mixins.TimestampsMixin): elif new_csp_estimate: raise TypeError("Could not set csp_estimate with invalid type") + @hybrid_property + def pdf(self): + return self._pdf + + @pdf.setter + def pdf(self, new_pdf): + if isinstance(new_pdf, Attachment): + self._pdf = new_pdf + elif isinstance(new_pdf, FileStorage): + self._pdf = Attachment.attach(new_pdf, "task_order", self.id) + elif not new_pdf and self._pdf: + self._pdf = None + elif new_pdf: + raise TypeError("Could not set pdf with invalid type") + @property def is_submitted(self): diff --git a/atst/routes/task_orders/new.py b/atst/routes/task_orders/new.py index 62647ce0..8892ba05 100644 --- a/atst/routes/task_orders/new.py +++ b/atst/routes/task_orders/new.py @@ -261,7 +261,6 @@ def get_started(): @task_orders_bp.route("/portfolios//task_orders/new/") def new(screen, task_order_id=None, portfolio_id=None): workflow = ShowTaskOrderWorkflow(g.current_user, screen, task_order_id) - Authorization.check_is_mo_or_cor(g.current_user, task_order) return render_template( workflow.template, current=screen, @@ -284,7 +283,6 @@ def update(screen, task_order_id=None, portfolio_id=None): workflow = UpdateTaskOrderWorkflow( g.current_user, form_data, screen, task_order_id, portfolio_id ) - Authorization.check_is_mo_or_cor(g.current_user, task_order) if workflow.validate(): workflow.update() return redirect( diff --git a/js/components/upload.js b/js/components/upload.js new file mode 100644 index 00000000..a53c1bb1 --- /dev/null +++ b/js/components/upload.js @@ -0,0 +1,42 @@ +import createNumberMask from 'text-mask-addons/dist/createNumberMask' +import { conformToMask } from 'vue-text-mask' + +import FormMixin from '../mixins/form' +import textinput from './text_input' +import optionsinput from './options_input' + +export default { + name: 'upload', + + mixins: [FormMixin], + + components: { + textinput, + optionsinput, + }, + + props: { + initialData: { + type: Object, + default: () => ({}), + }, + uploadErrors: { + type: Array, + default: () => [], + }, + }, + + data: function() { + const { pdf } = this.initialData + + return { + showUpload: !pdf || this.uploadErrors.length > 0, + } + }, + + methods: { + showUploadInput: function() { + this.showUpload = true + }, + }, +} diff --git a/js/index.js b/js/index.js index f9673744..8fb30039 100644 --- a/js/index.js +++ b/js/index.js @@ -20,6 +20,7 @@ import NewApplication from './components/forms/new_application' import EditEnvironmentRole from './components/forms/edit_environment_role' import EditApplicationRoles from './components/forms/edit_application_roles' import funding from './components/forms/funding' +import upload from './components/upload' import Modal from './mixins/modal' import selector from './components/selector' import BudgetChart from './components/charts/budget_chart' @@ -64,6 +65,7 @@ const app = new Vue({ RequestsList, ConfirmationPopover, funding, + upload, DateSelector, EditOfficerForm, }, diff --git a/templates/components/datepicker.html b/templates/components/datepicker.html deleted file mode 100644 index c597aeb5..00000000 --- a/templates/components/datepicker.html +++ /dev/null @@ -1,50 +0,0 @@ -{% from "components/icon.html" import Icon %} - -{% macro DatePicker(field) -%} - - -
- - - -
- - -
- -
- - -
- -
- - -
- -
- {{ Icon("ok", classes="icon--green") }} -
- -
-
- -{%- endmacro %} diff --git a/templates/portfolios/task_orders/review.html b/templates/portfolios/task_orders/review.html index 73e43186..2caa0167 100644 --- a/templates/portfolios/task_orders/review.html +++ b/templates/portfolios/task_orders/review.html @@ -17,11 +17,11 @@ {% include "fragments/flash.html" %} {% block form_action %} - {% if task_order_id %} - - {% endif %} + {% endblock %} + {{ form.csrf_token }} + {% block form %} {% set message = "task_orders.ko_review.submitted_by" | translate({"name": task_order.creator.full_name}) %} @@ -63,11 +63,28 @@
{{ "task_orders.ko_review.task_order_information"| translate }}
-
-
{{ form.pdf.label }}
- {{ form.pdf.description }} - {{ form.pdf }} -
+ + +
+ + +
+
+ {{ TextInput(form.number) }} {{ TextInput(form.loa) }} {{ TextInput(form.custom_clauses, paragraph=True) }} diff --git a/templates/task_orders/new/app_info.html b/templates/task_orders/new/app_info.html index fc819d93..ee8fe008 100644 --- a/templates/task_orders/new/app_info.html +++ b/templates/task_orders/new/app_info.html @@ -4,7 +4,6 @@ {% from "components/options_input.html" import OptionsInput %} {% from "components/date_input.html" import DateInput %} {% from "components/multi_checkbox_input.html" import MultiCheckboxInput %} -{% from "components/datepicker.html" import DatePicker %} {% block heading %} {{ "task_orders.new.app_info.section_title"| translate }} @@ -12,16 +11,6 @@ {% block form %} -
- - - For example: 04 28 1986 - - {{ DatePicker() }} -
-

{{ "task_orders.new.app_info.basic_info_title"| translate }}

From 9182b1078c2d37691d1dc5a5cebea8a0160091cd Mon Sep 17 00:00:00 2001 From: Montana Date: Wed, 6 Feb 2019 08:55:46 -0500 Subject: [PATCH 023/174] Refactor attachment setters and add tests --- atst/models/task_order.py | 33 ++++++++++----------- tests/models/test_task_order.py | 40 +++++++++++++++++++++++++- tests/routes/task_orders/test_index.py | 2 +- 3 files changed, 57 insertions(+), 18 deletions(-) diff --git a/atst/models/task_order.py b/atst/models/task_order.py index 60cb2ac9..119cf217 100644 --- a/atst/models/task_order.py +++ b/atst/models/task_order.py @@ -84,15 +84,7 @@ class TaskOrder(Base, mixins.TimestampsMixin): @csp_estimate.setter def csp_estimate(self, new_csp_estimate): - if isinstance(new_csp_estimate, Attachment): - self._csp_estimate = new_csp_estimate - elif isinstance(new_csp_estimate, FileStorage): - self._csp_estimate = Attachment.attach( - new_csp_estimate, "task_order", self.id - ) - elif not new_csp_estimate and self._csp_estimate: - self._csp_estimate = None - elif new_csp_estimate: + if not self._set_attachment_type(new_csp_estimate, "_csp_estimate"): raise TypeError("Could not set csp_estimate with invalid type") @hybrid_property @@ -101,15 +93,24 @@ class TaskOrder(Base, mixins.TimestampsMixin): @pdf.setter def pdf(self, new_pdf): - if isinstance(new_pdf, Attachment): - self._pdf = new_pdf - elif isinstance(new_pdf, FileStorage): - self._pdf = Attachment.attach(new_pdf, "task_order", self.id) - elif not new_pdf and self._pdf: - self._pdf = None - elif new_pdf: + if not self._set_attachment_type(new_pdf, "_pdf"): raise TypeError("Could not set pdf with invalid type") + def _set_attachment_type(self, new_attachment, property): + if isinstance(new_attachment, Attachment): + setattr(self, property, new_attachment) + return True + elif isinstance(new_attachment, FileStorage): + setattr( + self, property, Attachment.attach(new_attachment, "task_order", self.id) + ) + return True + elif not new_attachment and hasattr(self, property): + setattr(self, property, None) + return True + else: + return False + @property def is_submitted(self): diff --git a/tests/models/test_task_order.py b/tests/models/test_task_order.py index 560c6989..908daec2 100644 --- a/tests/models/test_task_order.py +++ b/tests/models/test_task_order.py @@ -47,7 +47,7 @@ class TestCSPEstimate: attachment = Attachment(filename="sample.pdf", object_name="sample") to.csp_estimate = attachment - assert to.attachment_id == attachment.id + assert to.csp_attachment_id == attachment.id def test_setting_estimate_with_file_storage(self): to = TaskOrder() @@ -77,3 +77,41 @@ class TestCSPEstimate: to.csp_estimate = "" assert to.csp_estimate is None + + +class TestPDF: + def test_setting_pdf_with_attachment(self): + to = TaskOrder() + attachment = Attachment(filename="sample.pdf", object_name="sample") + to.pdf = attachment + + assert to.pdf_attachment_id == attachment.id + + def test_setting_pdf_with_file_storage(self): + to = TaskOrder() + with open(PDF_FILENAME, "rb") as fp: + fs = FileStorage(fp, content_type="application/pdf") + to.pdf = fs + + assert to.pdf is not None + assert to.pdf.filename == PDF_FILENAME + + def test_setting_pdf_with_invalid_object(self): + to = TaskOrder() + with pytest.raises(TypeError): + to.pdf = "invalid" + + def test_setting_pdf_with_empty_value(self): + to = TaskOrder() + assert to.pdf is None + + to.pdf = "" + assert to.pdf is None + + def test_removing_pdf(self): + attachment = Attachment(filename="sample.pdf", object_name="sample") + to = TaskOrder(pdf=attachment) + assert to.pdf is not None + + to.pdf = "" + assert to.pdf is None diff --git a/tests/routes/task_orders/test_index.py b/tests/routes/task_orders/test_index.py index 45db3a82..784e4488 100644 --- a/tests/routes/task_orders/test_index.py +++ b/tests/routes/task_orders/test_index.py @@ -53,7 +53,7 @@ class TestDownloadCSPEstimate: assert expected_contents == response.data def test_download_without_attachment(self, client, user_session): - self.task_order.attachment_id = None + self.task_order.csp_attachment_id = None user_session(self.user) response = client.get( url_for( From fb95033dbe91218af7ebfd5a72945cf9e26f9bd1 Mon Sep 17 00:00:00 2001 From: Montana Date: Wed, 6 Feb 2019 11:07:43 -0500 Subject: [PATCH 024/174] Only set attrs in the setter --- atst/models/task_order.py | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/atst/models/task_order.py b/atst/models/task_order.py index 119cf217..e03cbdd9 100644 --- a/atst/models/task_order.py +++ b/atst/models/task_order.py @@ -84,8 +84,7 @@ class TaskOrder(Base, mixins.TimestampsMixin): @csp_estimate.setter def csp_estimate(self, new_csp_estimate): - if not self._set_attachment_type(new_csp_estimate, "_csp_estimate"): - raise TypeError("Could not set csp_estimate with invalid type") + self._csp_estimate = self._set_attachment(new_csp_estimate, "_csp_estimate") @hybrid_property def pdf(self): @@ -93,23 +92,17 @@ class TaskOrder(Base, mixins.TimestampsMixin): @pdf.setter def pdf(self, new_pdf): - if not self._set_attachment_type(new_pdf, "_pdf"): - raise TypeError("Could not set pdf with invalid type") + self._pdf = self._set_attachment(new_pdf, "_pdf") - def _set_attachment_type(self, new_attachment, property): + def _set_attachment(self, new_attachment, property): if isinstance(new_attachment, Attachment): - setattr(self, property, new_attachment) - return True + return new_attachment elif isinstance(new_attachment, FileStorage): - setattr( - self, property, Attachment.attach(new_attachment, "task_order", self.id) - ) - return True + return Attachment.attach(new_attachment, "task_order", self.id) elif not new_attachment and hasattr(self, property): - setattr(self, property, None) - return True + return None else: - return False + raise TypeError("Could not set attachment with invalid type") @property def is_submitted(self): From a6eab76ab79004d453c36f14eb1444c75afba376 Mon Sep 17 00:00:00 2001 From: Montana Date: Wed, 6 Feb 2019 13:38:27 -0500 Subject: [PATCH 025/174] Rename --- atst/models/task_order.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/atst/models/task_order.py b/atst/models/task_order.py index e03cbdd9..170e3be4 100644 --- a/atst/models/task_order.py +++ b/atst/models/task_order.py @@ -94,12 +94,12 @@ class TaskOrder(Base, mixins.TimestampsMixin): def pdf(self, new_pdf): self._pdf = self._set_attachment(new_pdf, "_pdf") - def _set_attachment(self, new_attachment, property): + def _set_attachment(self, new_attachment, attribute): if isinstance(new_attachment, Attachment): return new_attachment elif isinstance(new_attachment, FileStorage): return Attachment.attach(new_attachment, "task_order", self.id) - elif not new_attachment and hasattr(self, property): + elif not new_attachment and hasattr(self, attribute): return None else: raise TypeError("Could not set attachment with invalid type") From 193d128d7f004cfc8e44e040571ed48b56242a88 Mon Sep 17 00:00:00 2001 From: Montana Date: Thu, 7 Feb 2019 11:31:52 -0500 Subject: [PATCH 026/174] Update JSONEncoder to accept FileStorage objects --- atst/utils/json.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/atst/utils/json.py b/atst/utils/json.py index 4ce7bd8d..8e2a3217 100644 --- a/atst/utils/json.py +++ b/atst/utils/json.py @@ -1,4 +1,5 @@ from flask.json import JSONEncoder +from werkzeug.datastructures import FileStorage from datetime import date from atst.models.attachment import Attachment @@ -7,6 +8,8 @@ class CustomJSONEncoder(JSONEncoder): def default(self, obj): if isinstance(obj, Attachment): return obj.filename - if isinstance(obj, date): + elif isinstance(obj, date): return obj.strftime("%Y-%m-%d") + elif isinstance(obj, FileStorage): + return obj.filename return JSONEncoder.default(self, obj) From eaa5c939220669a15aadee7bb68233058913b036 Mon Sep 17 00:00:00 2001 From: Montana Date: Thu, 7 Feb 2019 13:12:30 -0500 Subject: [PATCH 027/174] Create upload macro --- js/components/forms/funding.js | 7 ++---- js/components/{upload.js => upload_input.js} | 7 +++--- js/index.js | 4 ++-- templates/components/upload_input.html | 24 ++++++++++++++++++++ templates/portfolios/task_orders/review.html | 24 ++------------------ templates/task_orders/new/funding.html | 18 ++------------- 6 files changed, 35 insertions(+), 49 deletions(-) rename js/components/{upload.js => upload_input.js} (86%) create mode 100644 templates/components/upload_input.html diff --git a/js/components/forms/funding.js b/js/components/forms/funding.js index 20b25a2c..8e4497a6 100644 --- a/js/components/forms/funding.js +++ b/js/components/forms/funding.js @@ -4,6 +4,7 @@ import { conformToMask } from 'vue-text-mask' import FormMixin from '../../mixins/form' import textinput from '../text_input' import optionsinput from '../options_input' +import uploadinput from '../upload_input' export default { name: 'funding', @@ -13,6 +14,7 @@ export default { components: { textinput, optionsinput, + uploadinput, }, props: { @@ -32,7 +34,6 @@ export default { clin_02 = 0, clin_03 = 0, clin_04 = 0, - csp_estimate, } = this.initialData return { @@ -40,7 +41,6 @@ export default { clin_02, clin_03, clin_04, - showUpload: !csp_estimate || this.uploadErrors.length > 0, } }, @@ -63,9 +63,6 @@ export default { }, methods: { - showUploadInput: function() { - this.showUpload = true - }, updateBudget: function() { document.querySelector('#to-target').innerText = this.totalBudgetStr }, diff --git a/js/components/upload.js b/js/components/upload_input.js similarity index 86% rename from js/components/upload.js rename to js/components/upload_input.js index a53c1bb1..a9b31460 100644 --- a/js/components/upload.js +++ b/js/components/upload_input.js @@ -6,7 +6,7 @@ import textinput from './text_input' import optionsinput from './options_input' export default { - name: 'upload', + name: 'uploadinput', mixins: [FormMixin], @@ -17,8 +17,7 @@ export default { props: { initialData: { - type: Object, - default: () => ({}), + type: String, }, uploadErrors: { type: Array, @@ -27,7 +26,7 @@ export default { }, data: function() { - const { pdf } = this.initialData + const pdf = this.initialData return { showUpload: !pdf || this.uploadErrors.length > 0, diff --git a/js/index.js b/js/index.js index 8fb30039..f2f82bc9 100644 --- a/js/index.js +++ b/js/index.js @@ -20,7 +20,7 @@ import NewApplication from './components/forms/new_application' import EditEnvironmentRole from './components/forms/edit_environment_role' import EditApplicationRoles from './components/forms/edit_application_roles' import funding from './components/forms/funding' -import upload from './components/upload' +import uploadinput from './components/upload_input' import Modal from './mixins/modal' import selector from './components/selector' import BudgetChart from './components/charts/budget_chart' @@ -65,7 +65,7 @@ const app = new Vue({ RequestsList, ConfirmationPopover, funding, - upload, + uploadinput, DateSelector, EditOfficerForm, }, diff --git a/templates/components/upload_input.html b/templates/components/upload_input.html new file mode 100644 index 00000000..b6835484 --- /dev/null +++ b/templates/components/upload_input.html @@ -0,0 +1,24 @@ +{% macro UploadInput(field, show_label=False) -%} + +
+ + +
+
+{%- endmacro %} diff --git a/templates/portfolios/task_orders/review.html b/templates/portfolios/task_orders/review.html index 2caa0167..e19054d4 100644 --- a/templates/portfolios/task_orders/review.html +++ b/templates/portfolios/task_orders/review.html @@ -9,6 +9,7 @@ {% from "components/text_input.html" import TextInput %} {% from "components/alert.html" import Alert %} {% from "components/review_field.html" import ReviewField %} +{% from "components/upload_input.html" import UploadInput %} {% block content %} @@ -63,28 +64,7 @@
{{ "task_orders.ko_review.task_order_information"| translate }}
- - -
- - -
-
- + {{ UploadInput(form.pdf) }} {{ TextInput(form.number) }} {{ TextInput(form.loa) }} {{ TextInput(form.custom_clauses, paragraph=True) }} diff --git a/templates/task_orders/new/funding.html b/templates/task_orders/new/funding.html index cc012474..cced51f7 100644 --- a/templates/task_orders/new/funding.html +++ b/templates/task_orders/new/funding.html @@ -3,6 +3,7 @@ {% from "components/text_input.html" import TextInput %} {% from "components/options_input.html" import OptionsInput %} {% from "components/date_input.html" import DateInput %} +{% from "components/upload_input.html" import UploadInput %} {% from "components/icon.html" import Icon %} @@ -32,22 +33,7 @@ {{ Icon("link")}} Go to Cloud Service Provider’s estimate calculator

{{ "task_orders.new.funding.estimate_usage_paragraph" | translate }}

- - + {{ UploadInput(form.csp_estimate, show_label=True) }}
From b5e6e67212d1d4496892b36d2d976dea2a47df9c Mon Sep 17 00:00:00 2001 From: Montana Date: Thu, 7 Feb 2019 13:24:02 -0500 Subject: [PATCH 028/174] Add names to foreign keys on latest migration --- alembic/versions/1f690989e38e_add_pdf_to_task_order.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/alembic/versions/1f690989e38e_add_pdf_to_task_order.py b/alembic/versions/1f690989e38e_add_pdf_to_task_order.py index a7d30c9a..c22926da 100644 --- a/alembic/versions/1f690989e38e_add_pdf_to_task_order.py +++ b/alembic/versions/1f690989e38e_add_pdf_to_task_order.py @@ -21,15 +21,15 @@ def upgrade(): op.add_column('task_orders', sa.Column('pdf_attachment_id', postgresql.UUID(as_uuid=True), nullable=True)) op.drop_constraint('task_orders_attachments_attachment_id', 'task_orders', type_='foreignkey') op.alter_column('task_orders', 'attachment_id', new_column_name='csp_attachment_id') - op.create_foreign_key(None, 'task_orders', 'attachments', ['pdf_attachment_id'], ['id']) - op.create_foreign_key(None, 'task_orders', 'attachments', ['csp_attachment_id'], ['id']) + op.create_foreign_key('task_orders_attachments_pdf_attachment_id', 'task_orders', 'attachments', ['pdf_attachment_id'], ['id']) + op.create_foreign_key('task_orders_attachments_csp_attachment_id', 'task_orders', 'attachments', ['csp_attachment_id'], ['id']) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_constraint(None, 'task_orders', type_='foreignkey') - op.drop_constraint(None, 'task_orders', type_='foreignkey') + op.drop_constraint('task_orders_attachments_csp_attachment_id', 'task_orders', type_='foreignkey') + op.drop_constraint('task_orders_attachments_pdf_attachment_id', 'task_orders', type_='foreignkey') op.alter_column('task_orders', 'csp_attachment_id', new_column_name='attachment_id') op.create_foreign_key('task_orders_attachments_attachment_id', 'task_orders', 'attachments', ['attachment_id'], ['id']) op.drop_column('task_orders', 'pdf_attachment_id') From d45f8e871b1166db02cd5452ffdf682b34700368 Mon Sep 17 00:00:00 2001 From: Montana Date: Thu, 7 Feb 2019 13:58:20 -0500 Subject: [PATCH 029/174] Test for KO review submission --- tests/routes/portfolios/test_task_orders.py | 38 +++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/routes/portfolios/test_task_orders.py b/tests/routes/portfolios/test_task_orders.py index 6bb44b5e..5a66abd9 100644 --- a/tests/routes/portfolios/test_task_orders.py +++ b/tests/routes/portfolios/test_task_orders.py @@ -251,3 +251,41 @@ def test_cor_redirected_to_build_page(client, user_session): url_for("task_orders.new", screen=1, task_order_id=task_order.id) ) assert response.status_code == 200 + + +def test_submit_completed_ko_review_page(client, user_session, pdf_upload): + portfolio = PortfolioFactory.create() + ko = UserFactory.create() + PortfolioRoleFactory.create( + role=Roles.get("officer"), + portfolio=portfolio, + user=ko, + status=PortfolioStatus.ACTIVE, + ) + task_order = TaskOrderFactory.create(portfolio=portfolio, contracting_officer=ko) + user_session(ko) + form_data = { + "start_date": "02/10/2019", + "end_date": "03/10/2019", + "number": "1938745981", + "loa": "0813458013405", + "custom_clauses": "hi im a custom clause", + "pdf": pdf_upload, + } + + response = client.post( + url_for( + "portfolios.ko_review", + portfolio_id=portfolio.id, + task_order_id=task_order.id, + ), + data=form_data, + ) + + assert task_order.pdf + assert response.headers["Location"] == url_for( + "portfolios.view_task_order", + portfolio_id=portfolio.id, + task_order_id=task_order.id, + _external=True, + ) From 53ce5323196352a78df782acd629e7e17aed7379 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Thu, 7 Feb 2019 14:09:22 -0500 Subject: [PATCH 030/174] Add "Applications" header on applications page --- styles/components/_portfolio_layout.scss | 24 +++++ templates/portfolios/applications/index.html | 100 +++++++++++-------- 2 files changed, 81 insertions(+), 43 deletions(-) diff --git a/styles/components/_portfolio_layout.scss b/styles/components/_portfolio_layout.scss index d95db7fa..05416cc3 100644 --- a/styles/components/_portfolio_layout.scss +++ b/styles/components/_portfolio_layout.scss @@ -89,6 +89,30 @@ margin-top: 6 * $gap; } +.portfolio-applications { + .portfolio-applications__header { + margin-bottom: 4 * $gap; + + .portfolio-applications__header--title { + color: $color-gray-dark; + padding: $gap 0; + text-transform: uppercase; + opacity: 0.54; + font-size: $small-font-size; + font-weight: bold; + } + + .portfolio-applications__header--actions { + color: $color-blue; + font-size: $small-font-size; + .icon { + @include icon-color($color-blue); + @include icon-size(14); + } + } + } +} + .portfolio-funding { .portfolio-funding__header { padding: 0; diff --git a/templates/portfolios/applications/index.html b/templates/portfolios/applications/index.html index 06eac461..c8d7ce65 100644 --- a/templates/portfolios/applications/index.html +++ b/templates/portfolios/applications/index.html @@ -3,52 +3,66 @@ {% extends "portfolios/base.html" %} +{% set can_create_applications = user_can(permissions.ADD_APPLICATION_IN_PORTFOLIO) %} + {% block portfolio_content %} -{% if not portfolio.applications %} - - {% set can_create_applications = user_can(permissions.ADD_APPLICATION_IN_PORTFOLIO) %} - - {{ EmptyState( - 'This portfolio doesn’t have any applications yet.', - action_label='Add a New Application' if can_create_applications else None, - action_href=url_for('portfolios.new_application', portfolio_id=portfolio.id) if can_create_applications else None, - icon='cloud', - sub_message=None if can_create_applications else 'Please contact your JEDI Cloud portfolio administrator to set up a new application.' - ) }} - -{% else %} - - {% for application in portfolio.applications %} -
-
-

{{ application.name }} ({{ application.environments|length }} environments)

- {% if user_can(permissions.RENAME_APPLICATION_IN_PORTFOLIO) %} - - {{ Icon('edit') }} - edit - - {% endif %} -
- +
+
+
Applications
+ - {% endfor %} +
-{% endif %} + {% if not portfolio.applications %} + + {{ EmptyState( + 'This portfolio doesn’t have any applications yet.', + action_label='Add a New Application' if can_create_applications else None, + action_href=url_for('portfolios.new_application', portfolio_id=portfolio.id) if can_create_applications else None, + icon='cloud', + sub_message=None if can_create_applications else 'Please contact your JEDI Cloud portfolio administrator to set up a new application.' + ) }} + + {% else %} + + {% for application in portfolio.applications %} +
+
+

{{ application.name }} ({{ application.environments|length }} environments)

+ {% if user_can(permissions.RENAME_APPLICATION_IN_PORTFOLIO) %} + + {{ Icon('edit') }} + edit + + {% endif %} +
+ +
+ {% endfor %} + + {% endif %} + +
{% endblock %} - From 85de20c1754b71897109ff08f11bceac23938a52 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Thu, 7 Feb 2019 14:53:58 -0500 Subject: [PATCH 031/174] Initial stab at accordian header on applications page --- styles/atat.scss | 1 + styles/components/_portfolio_layout.scss | 6 ++ styles/core/_variables.scss | 2 +- styles/elements/_accordians.scss | 102 +++++++++++++++++++ templates/portfolios/applications/index.html | 68 ++++++++----- 5 files changed, 151 insertions(+), 28 deletions(-) create mode 100644 styles/elements/_accordians.scss diff --git a/styles/atat.scss b/styles/atat.scss index f2e91d6e..6ef73f66 100644 --- a/styles/atat.scss +++ b/styles/atat.scss @@ -12,6 +12,7 @@ @import 'elements/buttons'; @import 'elements/panels'; @import 'elements/block_lists'; +@import 'elements/accordians'; @import 'elements/tables'; @import 'elements/sidenav'; @import 'elements/action_group'; diff --git a/styles/components/_portfolio_layout.scss b/styles/components/_portfolio_layout.scss index 05416cc3..27e058f6 100644 --- a/styles/components/_portfolio_layout.scss +++ b/styles/components/_portfolio_layout.scss @@ -111,6 +111,12 @@ } } } + + .application-list { + .application-list-item { + box-shadow: 0 4px 8px 1px rgba(230,230,230,0.5); + } + } } .portfolio-funding { diff --git a/styles/core/_variables.scss b/styles/core/_variables.scss index 95cfba10..28b89805 100644 --- a/styles/core/_variables.scss +++ b/styles/core/_variables.scss @@ -83,7 +83,7 @@ $color-green-lighter: #94bfa2; $color-green-lightest: #e7f4e4; $color-cool-blue: #205493; -$color-cool-blue-light: #4773aa; +$color-cool-blue-light: #4190e2; $color-cool-blue-lighter: #8ba6ca; $color-cool-blue-lightest: #dce4ef; diff --git a/styles/elements/_accordians.scss b/styles/elements/_accordians.scss new file mode 100644 index 00000000..6f11aedf --- /dev/null +++ b/styles/elements/_accordians.scss @@ -0,0 +1,102 @@ +.accordian { + @include block-list; + + .icon-link { + margin: -$gap 0; + } + + .icon-link, + .label { + &:first-child { + margin-left: -$gap; + } + + &:last-child { + margin-right: -$gap; + } + + } +} + +.accordian__header { + @include block-list-header; + border-top: 3px solid $color-blue; +} + +.accordian__title { + @include block-list__title; + color: $color-blue; + @include h3; +} + +.accordian__description { + @include block-list__description; + font-style: italic; + font-size: $small-font-size; + color: $color-gray; +} + +.accordian__actions { + margin-top: $gap; + display: flex; + flex-direction: row; + + .icon-link { + font-size: $small-font-size; + } + + .counter { + background-color: $color-cool-blue-light; + color: $color-white; + border-radius: 2px; + padding: $gap / 2 $gap; + margin-left: $gap; + } + + .separator { + border: 1px solid $color-gray-medium; + opacity: 0.75; + } +} + +.accordian__item { + @include block-list-item; + + &.accordian__item--selectable { + > div { + display: flex; + flex-direction: row-reverse; + + @include ie-only { + width: 100%; + } + + > label { + @include block-list-selectable-label; + } + } + + > label { + @include block-list-selectable-label; + } + + input:checked { + + label { + color: $color-primary; + } + } + + @include ie-only { + dl { + width: 100%; + padding-left: $gap*4; + } + } + + } +} + +.accordian__footer { + @include block-list__footer; + border-top: 0; +} diff --git a/templates/portfolios/applications/index.html b/templates/portfolios/applications/index.html index c8d7ce65..ec0f1530 100644 --- a/templates/portfolios/applications/index.html +++ b/templates/portfolios/applications/index.html @@ -32,34 +32,48 @@ {% else %} - {% for application in portfolio.applications %} -
-
-

{{ application.name }} ({{ application.environments|length }} environments)

- {% if user_can(permissions.RENAME_APPLICATION_IN_PORTFOLIO) %} - - {{ Icon('edit') }} - edit - - {% endif %} -
- -
- {% endfor %} +
+ + + +
+ {% endfor %} +
{% endif %} From c3157596be43f35e5c1758fa34c8ccdce9250f0a Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Thu, 7 Feb 2019 15:38:28 -0500 Subject: [PATCH 032/174] Add property to get number of users in an application --- atst/models/application.py | 8 +++++++ templates/portfolios/applications/index.html | 2 +- tests/models/test_application.py | 22 ++++++++++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 tests/models/test_application.py diff --git a/atst/models/application.py b/atst/models/application.py index 02c7185a..050b13d7 100644 --- a/atst/models/application.py +++ b/atst/models/application.py @@ -17,6 +17,14 @@ class Application(Base, mixins.TimestampsMixin, mixins.AuditableMixin): portfolio = relationship("Portfolio") environments = relationship("Environment", back_populates="application") + @property + def users(self): + return set([user for env in self.environments for user in env.users]) + + @property + def num_users(self): + return len(self.users) + @property def displayname(self): return self.name diff --git a/templates/portfolios/applications/index.html b/templates/portfolios/applications/index.html index ec0f1530..3ec59952 100644 --- a/templates/portfolios/applications/index.html +++ b/templates/portfolios/applications/index.html @@ -48,7 +48,7 @@ {% endif %} Team - 11 + {{ application.num_users }}
diff --git a/tests/models/test_application.py b/tests/models/test_application.py new file mode 100644 index 00000000..7701fbaf --- /dev/null +++ b/tests/models/test_application.py @@ -0,0 +1,22 @@ +from atst.domain.environments import Environments +from tests.factories import ApplicationFactory, UserFactory + + +def test_application_num_users(): + application = ApplicationFactory.create( + environments=[{"name": "dev"}, {"name": "staging"}, {"name": "prod"}] + ) + assert application.num_users == 0 + + first_env = application.environments[0] + user1 = UserFactory() + Environments.add_member(first_env, user1, "developer") + assert application.num_users == 1 + + second_env = application.environments[-1] + Environments.add_member(second_env, user1, "developer") + assert application.num_users == 1 + + user2 = UserFactory() + Environments.add_member(second_env, user2, "developer") + assert application.num_users == 2 From 0ea6f1d3eea146c77d062dba1041f9b7e5277b18 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Thu, 7 Feb 2019 15:43:34 -0500 Subject: [PATCH 033/174] Link to portfolio members page for "Team" button --- templates/portfolios/applications/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/portfolios/applications/index.html b/templates/portfolios/applications/index.html index 3ec59952..0ca8afe8 100644 --- a/templates/portfolios/applications/index.html +++ b/templates/portfolios/applications/index.html @@ -46,7 +46,7 @@
{% endif %} - + Team {{ application.num_users }} From 9afa449bdf1dcad1a7f1ce106ebc803c2f4adad1 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Thu, 7 Feb 2019 16:28:25 -0500 Subject: [PATCH 034/174] Touch up styles on environment rows in applications listing --- styles/components/_portfolio_layout.scss | 16 ++++++++++++++-- styles/core/_variables.scss | 1 + styles/elements/_accordians.scss | 8 ++++++++ templates/portfolios/applications/index.html | 11 +++++------ 4 files changed, 28 insertions(+), 8 deletions(-) diff --git a/styles/components/_portfolio_layout.scss b/styles/components/_portfolio_layout.scss index 27e058f6..b12cfece 100644 --- a/styles/components/_portfolio_layout.scss +++ b/styles/components/_portfolio_layout.scss @@ -86,7 +86,7 @@ } .portfolio-content { - margin-top: 6 * $gap; + margin: 6 * $gap $gap 0 $gap; } .portfolio-applications { @@ -114,7 +114,19 @@ .application-list { .application-list-item { - box-shadow: 0 4px 8px 1px rgba(230,230,230,0.5); + border-radius: 5px; + box-shadow: 0 4px 8px 1px rgba(230,230,230,0.5), -4px 4px 8px 1px rgba(230,230,230,0.5); + + .application-list-item__environment__name { + } + + .application-list-item__environment__csp_link { + font-size: $small-font-size; + font-weight: normal; + &:hover { + background-color: $color-aqua-light; + } + } } } } diff --git a/styles/core/_variables.scss b/styles/core/_variables.scss index 28b89805..3917fa5e 100644 --- a/styles/core/_variables.scss +++ b/styles/core/_variables.scss @@ -43,6 +43,7 @@ $font-bold: 700; $color-blue: #0071bc; $color-blue-darker: #205493; $color-blue-darkest: #112e51; +$color-blue-light: #e5f1ff; $color-aqua: #02bfe7; $color-aqua-dark: #00a6d2; diff --git a/styles/elements/_accordians.scss b/styles/elements/_accordians.scss index 6f11aedf..77727c2a 100644 --- a/styles/elements/_accordians.scss +++ b/styles/elements/_accordians.scss @@ -1,6 +1,8 @@ .accordian { @include block-list; + box-shadow: 0 4px 10px 0 rgba(193,193,193,0.5); + .icon-link { margin: -$gap 0; } @@ -21,6 +23,8 @@ .accordian__header { @include block-list-header; border-top: 3px solid $color-blue; + border-bottom: none; + box-shadow: 0 2px 4px 0 rgba(216,218,222,0.58); } .accordian__title { @@ -62,6 +66,10 @@ .accordian__item { @include block-list-item; + opacity: 0.75; + background-color: $color-blue-light; + border-bottom: 1px solid rgba($color-gray-light, 0.5); + &.accordian__item--selectable { > div { display: flex; diff --git a/templates/portfolios/applications/index.html b/templates/portfolios/applications/index.html index 0ca8afe8..453138e2 100644 --- a/templates/portfolios/applications/index.html +++ b/templates/portfolios/applications/index.html @@ -59,15 +59,14 @@ From 8c79eb39253a786bae5c4912fab77d2d5eee5aad Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Thu, 7 Feb 2019 18:45:25 -0500 Subject: [PATCH 035/174] Allow collapsing/expanding list of environments --- static/icons/minus.svg | 1 + styles/components/_portfolio_layout.scss | 7 ++ templates/portfolios/applications/index.html | 71 +++++++++++--------- 3 files changed, 48 insertions(+), 31 deletions(-) create mode 100644 static/icons/minus.svg diff --git a/static/icons/minus.svg b/static/icons/minus.svg new file mode 100644 index 00000000..ac83426d --- /dev/null +++ b/static/icons/minus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/styles/components/_portfolio_layout.scss b/styles/components/_portfolio_layout.scss index b12cfece..5ba22d65 100644 --- a/styles/components/_portfolio_layout.scss +++ b/styles/components/_portfolio_layout.scss @@ -113,6 +113,13 @@ } .application-list { + .toggle-link { + background-color: $color-blue-light; + .icon { + margin: $gap / 2; + } + } + .application-list-item { border-radius: 5px; box-shadow: 0 4px 8px 1px rgba(230,230,230,0.5), -4px 4px 8px 1px rgba(230,230,230,0.5); diff --git a/templates/portfolios/applications/index.html b/templates/portfolios/applications/index.html index 453138e2..1aa49604 100644 --- a/templates/portfolios/applications/index.html +++ b/templates/portfolios/applications/index.html @@ -34,42 +34,51 @@