From b22d20218687d815b42b13c53b4dc07595f09fe0 Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Thu, 24 Jan 2019 19:58:59 -0500 Subject: [PATCH 1/9] Fix styling on header and icons --- js/components/tables/task_order_list.js | 3 ++- styles/components/_portfolio_layout.scss | 25 +++++++++++++++++++++ templates/portfolios/task_orders/index.html | 10 ++++----- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/js/components/tables/task_order_list.js b/js/components/tables/task_order_list.js index d9cc3a2f..04d32389 100644 --- a/js/components/tables/task_order_list.js +++ b/js/components/tables/task_order_list.js @@ -46,7 +46,8 @@ export default { displayName: 'Period of Performance', attr: 'start_date', sortFunc: numericSort, - width: '50%', + width: "50%", + class: "period-of-performance" }, { displayName: 'Initial Value', diff --git a/styles/components/_portfolio_layout.scss b/styles/components/_portfolio_layout.scss index 6702d67d..16aa41c5 100644 --- a/styles/components/_portfolio_layout.scss +++ b/styles/components/_portfolio_layout.scss @@ -82,10 +82,17 @@ .view-task-order-link { margin-left: $gap * 2; + + .icon--tiny { + @include icon-size(10); + margin-left: 1rem; + } } .portfolio-total-balance { margin-top: -$gap; + margin-bottom: 3rem; + .row { flex-direction: row-reverse; margin: 2 * $gap 0; @@ -98,8 +105,26 @@ } table { + th{ + .icon { + margin-left: 1rem; + } + + &.period-of-performance { + color: $color-blue; + + .icon { + @include icon-color($color-primary) + } + } + } + td.unused-balance { color: $color-red; } + + .label--expired { + background-color: $color-gray-light; + } } } diff --git a/templates/portfolios/task_orders/index.html b/templates/portfolios/task_orders/index.html index 783c676d..4b004e62 100644 --- a/templates/portfolios/task_orders/index.html +++ b/templates/portfolios/task_orders/index.html @@ -8,7 +8,7 @@ {% macro ViewLink(task_order) %} View - {{ Icon("caret_right") }} + {{ Icon("caret_right", classes="icon--tiny") }} {% endmacro %} @@ -27,10 +27,10 @@ !{ col.displayName } @@ -63,7 +63,7 @@ View - {{ Icon("caret_right") }} + {{ Icon("caret_right", classes="icon--tiny") }} @@ -135,7 +135,7 @@ {% endif %} {% if expired_task_orders %} - {{ TaskOrderList(expired_task_orders, label='', expired=True) }} + {{ TaskOrderList(expired_task_orders, label='expired', expired=True) }} {% endif %} From db1c712c8bce50d6de66e819dfdf2bb9ea67b57d Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Fri, 25 Jan 2019 11:17:41 -0500 Subject: [PATCH 2/9] Update seed_sample with porfolios with different funding statuses --- script/seed_sample.py | 90 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/script/seed_sample.py b/script/seed_sample.py index d7361663..2e116caa 100644 --- a/script/seed_sample.py +++ b/script/seed_sample.py @@ -1,6 +1,7 @@ # Add root application dir to the python path import os import sys +from datetime import datetime, timedelta, date parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) sys.path.append(parent_dir) @@ -94,6 +95,9 @@ def seed_db(): users.append(user) + amanda = Users.get_by_dod_id("2345678901") + + # create Portfolios for all users that have funding and are not expiring soon for user in users: portfolio = Portfolios.create( user, name="{}'s portfolio".format(user.first_name) @@ -161,6 +165,92 @@ def seed_db(): environment_names=["dev", "staging", "prod"], ) + # Create Portfolio for Amanda with TO that is expiring soon and does not have another TO + unfunded_portfolio = Portfolios.create( + amanda, name="{}'s unfunded portfolio".format(amanda.first_name) + ) + + [past_date_1, past_date_2, past_date_3, future_date] = sorted( + [ + random_past_date(year_max=3, year_min=2), + random_past_date(year_max=2, year_min=1), + random_past_date(year_max=1, year_min=1), + (date.today() + timedelta(days=20)), + ] + ) + + date_ranges = [ + (past_date_1, past_date_2), + (past_date_2, past_date_3), + (past_date_3, future_date), + ] + for (start_date, end_date) in date_ranges: + task_order = TaskOrderFactory.build( + start_date=start_date, + end_date=end_date, + number=random_task_order_number(), + portfolio=unfunded_portfolio, + ) + db.session.add(task_order) + + db.session.commit() + + Applications.create( + amanda, + portfolio=unfunded_portfolio, + name="First Application", + description="This is our first application.", + environment_names=["dev", "staging", "prod"], + ) + + # Create Portfolio for Amanda with TO that is expiring soon and has another TO + funded_portfolio = Portfolios.create( + amanda, name="{}'s funded portfolio".format(amanda.first_name) + ) + + [ + past_date_1, + past_date_2, + past_date_3, + past_date_4, + future_date_1, + future_date_2, + ] = sorted( + [ + random_past_date(year_max=3, year_min=2), + random_past_date(year_max=2, year_min=1), + random_past_date(year_max=1, year_min=1), + random_past_date(year_max=1, year_min=1), + (date.today() + timedelta(days=20)), + random_future_date(year_min=0, year_max=1), + ] + ) + + date_ranges = [ + (past_date_1, past_date_2), + (past_date_2, past_date_3), + (past_date_3, future_date_1), + (past_date_4, future_date_2), + ] + for (start_date, end_date) in date_ranges: + task_order = TaskOrderFactory.build( + start_date=start_date, + end_date=end_date, + number=random_task_order_number(), + portfolio=funded_portfolio, + ) + db.session.add(task_order) + + db.session.commit() + + Applications.create( + amanda, + portfolio=funded_portfolio, + name="First Application", + description="This is our first application.", + environment_names=["dev", "staging", "prod"], + ) + if __name__ == "__main__": config = make_config({"DISABLE_CRL_CHECK": True}) From 2cbfa59b9248ca09591baed0a3a29e9d8f1a1c4a Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Mon, 28 Jan 2019 17:12:09 -0500 Subject: [PATCH 3/9] Add funding status alerts --- atst/models/task_order.py | 6 +++++ atst/routes/portfolios/task_orders.py | 1 + js/components/tables/task_order_list.js | 4 +-- styles/components/_portfolio_layout.scss | 22 +++++++++++++++++ templates/portfolios/task_orders/index.html | 27 +++++++++++++++------ 5 files changed, 50 insertions(+), 10 deletions(-) diff --git a/atst/models/task_order.py b/atst/models/task_order.py index 846f1fbc..fdb59934 100644 --- a/atst/models/task_order.py +++ b/atst/models/task_order.py @@ -1,4 +1,5 @@ from enum import Enum +from datetime import date import pendulum from sqlalchemy import Column, Numeric, String, ForeignKey, Date, Integer @@ -111,6 +112,11 @@ class TaskOrder(Base, mixins.TimestampsMixin): def display_status(self): return self.status.value + @property + def days_to_expiration(self): + if self.end_date: + return (self.end_date - date.today()).days + @property def budget(self): return sum( diff --git a/atst/routes/portfolios/task_orders.py b/atst/routes/portfolios/task_orders.py index 8bcb4715..120108e5 100644 --- a/atst/routes/portfolios/task_orders.py +++ b/atst/routes/portfolios/task_orders.py @@ -22,6 +22,7 @@ def portfolio_funding(portfolio_id): "start_date", "end_date", "display_status", + "days_to_expiration", "balance", ] } diff --git a/js/components/tables/task_order_list.js b/js/components/tables/task_order_list.js index 04d32389..b511508f 100644 --- a/js/components/tables/task_order_list.js +++ b/js/components/tables/task_order_list.js @@ -46,8 +46,8 @@ export default { displayName: 'Period of Performance', attr: 'start_date', sortFunc: numericSort, - width: "50%", - class: "period-of-performance" + width: '50%', + class: 'period-of-performance', }, { displayName: 'Initial Value', diff --git a/styles/components/_portfolio_layout.scss b/styles/components/_portfolio_layout.scss index 16aa41c5..6b546805 100644 --- a/styles/components/_portfolio_layout.scss +++ b/styles/components/_portfolio_layout.scss @@ -126,5 +126,27 @@ .label--expired { background-color: $color-gray-light; } + + .to-expiring-soon { + font-weight: $font-bold; + font-size: 1.5rem; + margin-left: $gap; + + &.funded { + color: $color-blue; + + .icon { + @include icon-color($color-blue); + } + } + + &.unfunded { + color: $color-red; + + .icon { + @include icon-color($color-red); + } + } + } } } diff --git a/templates/portfolios/task_orders/index.html b/templates/portfolios/task_orders/index.html index 4b004e62..f3b2f765 100644 --- a/templates/portfolios/task_orders/index.html +++ b/templates/portfolios/task_orders/index.html @@ -44,15 +44,26 @@ - - - - - + + + - + + + {{ Icon('ok') }} Period ending in !{ taskOrder.days_to_expiration } days, but new period funded + + + {{ Icon('alert') }} Period ends in !{ taskOrder.days_to_expiration } days, submit a new task order + + From b4cd657d62a5498576eb38dbca3381ff84c37223 Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Wed, 30 Jan 2019 10:26:57 -0500 Subject: [PATCH 4/9] Add styling to funding alerts --- atst/routes/portfolios/task_orders.py | 2 + js/components/tables/task_order_list.js | 1 + styles/components/_portfolio_layout.scss | 47 +++++++++++++++------ templates/portfolios/task_orders/index.html | 37 ++++++++++------ 4 files changed, 59 insertions(+), 28 deletions(-) diff --git a/atst/routes/portfolios/task_orders.py b/atst/routes/portfolios/task_orders.py index 120108e5..8cde94c9 100644 --- a/atst/routes/portfolios/task_orders.py +++ b/atst/routes/portfolios/task_orders.py @@ -42,6 +42,7 @@ def portfolio_funding(portfolio_id): if active_task_orders else None ) + funded = len(active_task_orders) > 1 total_balance = sum([task_order["balance"] for task_order in active_task_orders]) return render_template( @@ -51,6 +52,7 @@ def portfolio_funding(portfolio_id): active_task_orders=active_task_orders, expired_task_orders=task_orders_by_status.get(TaskOrderStatus.EXPIRED, []), funding_end_date=funding_end_date, + funded=funded, total_balance=total_balance, ) diff --git a/js/components/tables/task_order_list.js b/js/components/tables/task_order_list.js index b511508f..effe3567 100644 --- a/js/components/tables/task_order_list.js +++ b/js/components/tables/task_order_list.js @@ -21,6 +21,7 @@ export default { props: { data: Array, expired: Boolean, + funded: Boolean, }, components: { diff --git a/styles/components/_portfolio_layout.scss b/styles/components/_portfolio_layout.scss index 6b546805..e74481a3 100644 --- a/styles/components/_portfolio_layout.scss +++ b/styles/components/_portfolio_layout.scss @@ -50,6 +50,13 @@ @include icon-color($color-green); } } + + .unfunded { + color: $color-red; + .icon { + @include icon-color($color-red); + } + } } .pending-task-order { @@ -127,24 +134,36 @@ background-color: $color-gray-light; } - .to-expiring-soon { - font-weight: $font-bold; - font-size: 1.5rem; - margin-left: $gap; + .to-performance-period { + &.to-expiring-soon { - &.funded { - color: $color-blue; - - .icon { - @include icon-color($color-blue); + .to-expiration-alert { + font-weight: $font-bold; + font-size: 1.5rem; + margin-left: $gap; } - } - &.unfunded { - color: $color-red; + &.funded .to-expiration-alert { + color: $color-blue; - .icon { - @include icon-color($color-red); + + .icon { + @include icon-color($color-blue); + } + } + + &.unfunded { + .to-expiration-alert { + color: $color-red; + } + + .icon { + @include icon-color($color-red); + } + + .to-end-date { + color: $color-red; + } } } } diff --git a/templates/portfolios/task_orders/index.html b/templates/portfolios/task_orders/index.html index f3b2f765..58b5704f 100644 --- a/templates/portfolios/task_orders/index.html +++ b/templates/portfolios/task_orders/index.html @@ -12,7 +12,7 @@ {% endmacro %} -{% macro TaskOrderList(task_orders, label='success', expired=False) %} +{% macro TaskOrderList(task_orders, label='success', expired=False, funded=False) %} !{ taskOrder.display_status } - + @@ -51,17 +51,19 @@ - + format="M/D/YYYY" + class="to-end-date" + > + v-if="taskOrder.days_to_expiration > 0 && taskOrder.days_to_expiration <= 30 && funded" + class="to-expiration-alert"> {{ Icon('ok') }} Period ending in !{ taskOrder.days_to_expiration } days, but new period funded - {{ Icon('alert') }} Period ends in !{ taskOrder.days_to_expiration } days, submit a new task order + v-if="taskOrder.days_to_expiration > 0 && taskOrder.days_to_expiration <= 30 && !funded" + class="to-expiration-alert"> + {{ Icon('alert') }} Period ends in !{ taskOrder.days_to_expiration } days, submit a new task order
@@ -89,15 +91,22 @@

Portfolio Funding

-
- {% if funding_end_date %} +
+ {% if funding_end_date and funded %} {{ Icon('ok') }} Funded through - - {% endif %} + + {% elif funding_end_date and not funded %} + {{ Icon('alert') }} + Funded period ends + + + {% endif %}
Start a New Task Order
@@ -136,7 +145,7 @@ {% endif %} {% if active_task_orders %} - {{ TaskOrderList(active_task_orders, label='success') }} + {{ TaskOrderList(active_task_orders, label='success', funded=funded) }}
{{ total_balance | dollars }} From 4c775517b92b32ba9dbbaf3d4d6e7720341caafe Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Fri, 1 Feb 2019 11:12:54 -0500 Subject: [PATCH 5/9] Prevent python datetime from converting to Javascript datetime before json encoding. Previously the conversion from python to Javascript datetime objects was adding timezone information, which was causing the date to render incorrectly because the users timezone was different than the encoded time zone. --- atst/utils/json.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/atst/utils/json.py b/atst/utils/json.py index a489fcaa..4ce7bd8d 100644 --- a/atst/utils/json.py +++ b/atst/utils/json.py @@ -1,4 +1,5 @@ from flask.json import JSONEncoder +from datetime import date from atst.models.attachment import Attachment @@ -6,4 +7,6 @@ class CustomJSONEncoder(JSONEncoder): def default(self, obj): if isinstance(obj, Attachment): return obj.filename + if isinstance(obj, date): + return obj.strftime("%Y-%m-%d") return JSONEncoder.default(self, obj) From 277e440f0b04a062ea16f1c70ad334dedf85debc Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Fri, 1 Feb 2019 14:32:28 -0500 Subject: [PATCH 6/9] Pass funded as prop to vue component --- templates/portfolios/task_orders/index.html | 1 + 1 file changed, 1 insertion(+) diff --git a/templates/portfolios/task_orders/index.html b/templates/portfolios/task_orders/index.html index 58b5704f..9a7b8981 100644 --- a/templates/portfolios/task_orders/index.html +++ b/templates/portfolios/task_orders/index.html @@ -17,6 +17,7 @@ inline-template v-bind:data='{{ task_orders | tojson }}' v-bind:expired='{{ 'true' if expired else 'false' }}' + v-bind:funded='{{'true' if funded else 'false' }}' v-cloak >
From e39f8d6feef7864eeade3a98475f28f9c7b6af3c Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Mon, 4 Feb 2019 13:56:49 -0500 Subject: [PATCH 7/9] Update Portfolio names in seed file to reflect funding status --- script/seed_sample.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/script/seed_sample.py b/script/seed_sample.py index 2e116caa..801056e9 100644 --- a/script/seed_sample.py +++ b/script/seed_sample.py @@ -100,7 +100,7 @@ def seed_db(): # create Portfolios for all users that have funding and are not expiring soon for user in users: portfolio = Portfolios.create( - user, name="{}'s portfolio".format(user.first_name) + user, name="{}'s portfolio (not expiring)".format(user.first_name) ) for portfolio_role in PORTFOLIO_USERS: ws_role = Portfolios.create_member(user, portfolio, portfolio_role) @@ -167,7 +167,7 @@ def seed_db(): # Create Portfolio for Amanda with TO that is expiring soon and does not have another TO unfunded_portfolio = Portfolios.create( - amanda, name="{}'s unfunded portfolio".format(amanda.first_name) + amanda, name="{}'s portfolio (expiring and unfunded)".format(amanda.first_name) ) [past_date_1, past_date_2, past_date_3, future_date] = sorted( @@ -205,7 +205,7 @@ def seed_db(): # Create Portfolio for Amanda with TO that is expiring soon and has another TO funded_portfolio = Portfolios.create( - amanda, name="{}'s funded portfolio".format(amanda.first_name) + amanda, name="{}'s portfolio (expiring and funded)".format(amanda.first_name) ) [ From 93e593e50d5b0a5714d1dfdc32263293b90fc29d Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Mon, 4 Feb 2019 21:49:02 -0500 Subject: [PATCH 8/9] Add tests --- tests/routes/portfolios/test_task_orders.py | 51 ++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/tests/routes/portfolios/test_task_orders.py b/tests/routes/portfolios/test_task_orders.py index 6bebd3aa..0d3f66df 100644 --- a/tests/routes/portfolios/test_task_orders.py +++ b/tests/routes/portfolios/test_task_orders.py @@ -1,5 +1,6 @@ from flask import url_for import pytest +from datetime import timedelta, date from atst.domain.roles import Roles from atst.models.portfolio_role import Status as PortfolioStatus @@ -16,7 +17,7 @@ from tests.utils import captured_templates class TestPortfolioFunding: - def test_unfunded_portfolio(self, app, user_session): + def test_portfolio_with_no_task_orders(self, app, user_session): portfolio = PortfolioFactory.create() user_session(portfolio.owner) @@ -66,6 +67,54 @@ class TestPortfolioFunding: assert context["funding_end_date"] is end_date assert context["total_balance"] == active_to1.budget + active_to2.budget + def test_expiring_and_funded_portfolio(self, app, user_session): + portfolio = PortfolioFactory.create() + user_session(portfolio.owner) + + expiring_to = TaskOrderFactory.create( + portfolio=portfolio, + start_date=random_past_date(), + end_date=(date.today() + timedelta(days=10)), + number="42", + ) + active_to = TaskOrderFactory.create( + portfolio=portfolio, + start_date=random_past_date(), + end_date=random_future_date(year_min=1, year_max=2), + number="43", + ) + + with captured_templates(app) as templates: + response = app.test_client().get( + url_for("portfolios.portfolio_funding", portfolio_id=portfolio.id) + ) + + assert response.status_code == 200 + _, context = templates[0] + assert context["funding_end_date"] is active_to.end_date + assert context["funded"] == True + + def test_expiring_and_unfunded_portfolio(self, app, user_session): + portfolio = PortfolioFactory.create() + user_session(portfolio.owner) + + expiring_to = TaskOrderFactory.create( + portfolio=portfolio, + start_date=random_past_date(), + end_date=(date.today() + timedelta(days=10)), + number="42", + ) + + with captured_templates(app) as templates: + response = app.test_client().get( + url_for("portfolios.portfolio_funding", portfolio_id=portfolio.id) + ) + + assert response.status_code == 200 + _, context = templates[0] + assert context["funding_end_date"] is expiring_to.end_date + assert context["funded"] == False + def test_ko_can_view_task_order(client, user_session): portfolio = PortfolioFactory.create() From 051e9d794f186356c770eb32559b7532656e3aac Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Tue, 5 Feb 2019 09:51:44 -0500 Subject: [PATCH 9/9] Move date limit for expiring alert into a variable in Vue component --- js/components/tables/task_order_list.js | 1 + templates/portfolios/task_orders/index.html | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/js/components/tables/task_order_list.js b/js/components/tables/task_order_list.js index effe3567..d7353307 100644 --- a/js/components/tables/task_order_list.js +++ b/js/components/tables/task_order_list.js @@ -74,6 +74,7 @@ export default { isAscending: false, columns: indexBy(prop('displayName'), columns), }, + days_to_exp_alert_limit: 30, } }, diff --git a/templates/portfolios/task_orders/index.html b/templates/portfolios/task_orders/index.html index 9a7b8981..18653ab1 100644 --- a/templates/portfolios/task_orders/index.html +++ b/templates/portfolios/task_orders/index.html @@ -44,7 +44,7 @@ !{ taskOrder.display_status } - + @@ -57,12 +57,12 @@ > {{ Icon('ok') }} Period ending in !{ taskOrder.days_to_expiration } days, but new period funded {{ Icon('alert') }} Period ends in !{ taskOrder.days_to_expiration } days, submit a new task order