From 176a87faae0011b2f0eb11211a583b3856220a07 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Tue, 15 Jan 2019 13:20:54 -0500 Subject: [PATCH 01/13] Fix started timestamp on TO view page --- templates/portfolios/task_orders/show.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/portfolios/task_orders/show.html b/templates/portfolios/task_orders/show.html index 28212db0..1b8ee5a0 100644 --- a/templates/portfolios/task_orders/show.html +++ b/templates/portfolios/task_orders/show.html @@ -70,7 +70,7 @@
Started
From f6037aa8af013e34a9bda405234695ff4b2e4adf Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Tue, 15 Jan 2019 13:32:05 -0500 Subject: [PATCH 02/13] Show pending task orders at top of funding page --- atst/routes/portfolios/task_orders.py | 13 +++- styles/components/_portfolio_layout.scss | 34 ++++++++++ templates/portfolios/task_orders/index.html | 70 +++++++++++++++------ 3 files changed, 98 insertions(+), 19 deletions(-) diff --git a/atst/routes/portfolios/task_orders.py b/atst/routes/portfolios/task_orders.py index 7b5b7999..afadd3ae 100644 --- a/atst/routes/portfolios/task_orders.py +++ b/atst/routes/portfolios/task_orders.py @@ -1,14 +1,25 @@ +from collections import defaultdict + from flask import g, render_template from . import portfolios_bp from atst.domain.task_orders import TaskOrders from atst.domain.portfolios import Portfolios +from atst.models.task_order import Status as TaskOrderStatus @portfolios_bp.route("/portfolios//task_orders") def portfolio_task_orders(portfolio_id): portfolio = Portfolios.get(g.current_user, portfolio_id) - return render_template("portfolios/task_orders/index.html", portfolio=portfolio) + task_orders_by_status = defaultdict(list) + for task_order in portfolio.task_orders: + task_orders_by_status[task_order.status].append(task_order) + + return render_template( + "portfolios/task_orders/index.html", + portfolio=portfolio, + pending_task_orders=task_orders_by_status.get(TaskOrderStatus.PENDING, []), + ) @portfolios_bp.route("/portfolios//task_order/") diff --git a/styles/components/_portfolio_layout.scss b/styles/components/_portfolio_layout.scss index 62938d35..6a6db378 100644 --- a/styles/components/_portfolio_layout.scss +++ b/styles/components/_portfolio_layout.scss @@ -29,3 +29,37 @@ } } } + +.portfolio-funding { + .pending-task-order { + background-color: $color-gold-lightest; + + align-items: center; + margin: 0; + padding: 2 * $gap; + + dt { + font-weight: bold; + } + + dd { + margin-left: 0; + } + + .label { + margin-right: 2 * $gap; + } + + .pending-task-order__started { + flex-grow: 1; + } + + .pending-task-order__value { + text-align: right; + } + } + + .view-task-order-link { + margin-left: $gap * 2; + } +} diff --git a/templates/portfolios/task_orders/index.html b/templates/portfolios/task_orders/index.html index 91326fb1..36620b88 100644 --- a/templates/portfolios/task_orders/index.html +++ b/templates/portfolios/task_orders/index.html @@ -1,30 +1,64 @@ {% from "components/empty_state.html" import EmptyState %} +{% from "components/icon.html" import Icon %} {% extends "portfolios/base.html" %} {% block portfolio_content %} -{% if not portfolio.task_orders %} +{% macro ViewLink(task_order) %} - {{ EmptyState( - 'This portfolio doesn’t have any task orders yet.', - action_label='Add a New Task Order', - action_href=url_for('task_orders.new', screen=1, portfolio_id=portfolio.id), - icon='cloud', - ) }} + + View + {{ Icon("caret_right") }} + +{% endmacro %} -{% else %} +
+ {% for task_order in pending_task_orders %} +
+
+ Pending +
+
Started
+
+ + +
+
+
+
Value
+
{{ task_order.budget | dollars }}
+
+ {{ ViewLink(task_order) }} +
+
+ {% endfor %} - -{% endif %} + {% if not portfolio.task_orders %} + + {{ EmptyState( + 'This portfolio doesn’t have any task orders yet.', + action_label='Add a New Task Order', + action_href=url_for('task_orders.new', screen=1, portfolio_id=portfolio.id), + icon='cloud', + ) }} + + {% else %} + + + + {% endif %} +
{% endblock %} From 8cc46c9e05d850ea11605fb2b6e954ddce1a31c2 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Tue, 15 Jan 2019 13:41:45 -0500 Subject: [PATCH 03/13] Update condition for empty state on portfolio funding screen --- templates/portfolios/task_orders/index.html | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/templates/portfolios/task_orders/index.html b/templates/portfolios/task_orders/index.html index 36620b88..e01ae94d 100644 --- a/templates/portfolios/task_orders/index.html +++ b/templates/portfolios/task_orders/index.html @@ -37,16 +37,14 @@ {% endfor %} - {% if not portfolio.task_orders %} - + {% if not active_task_orders and not pending_task_orders %} {{ EmptyState( - 'This portfolio doesn’t have any task orders yet.', + 'This portfolio doesn’t have any active or pending task orders.', action_label='Add a New Task Order', action_href=url_for('task_orders.new', screen=1, portfolio_id=portfolio.id), icon='cloud', ) }} - - {% else %} + {% endif %}
    {% for task_order in portfolio.task_orders %} From a6a53525f8e04389fabbec1602430b1b8bb35978 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Tue, 15 Jan 2019 17:29:13 -0500 Subject: [PATCH 04/13] Partition and serialize task orders need on funding page --- atst/models/task_order.py | 9 +++++++++ atst/routes/portfolios/task_orders.py | 25 +++++++++++++++++++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/atst/models/task_order.py b/atst/models/task_order.py index 340be9af..04260b7b 100644 --- a/atst/models/task_order.py +++ b/atst/models/task_order.py @@ -88,12 +88,21 @@ class TaskOrder(Base, mixins.TimestampsMixin): else: return Status.PENDING + @property + def display_status(self): + return self.status.value + @property def budget(self): return sum( filter(None, [self.clin_01, self.clin_02, self.clin_03, self.clin_04]) ) + @property + def balance(self): + # TODO: somehow calculate the remaining balance. For now, assume $0 spent + return self.budget + @property def portfolio_name(self): return self.portfolio.name diff --git a/atst/routes/portfolios/task_orders.py b/atst/routes/portfolios/task_orders.py index afadd3ae..8c326775 100644 --- a/atst/routes/portfolios/task_orders.py +++ b/atst/routes/portfolios/task_orders.py @@ -1,6 +1,6 @@ from collections import defaultdict -from flask import g, render_template +from flask import g, render_template, url_for from . import portfolios_bp from atst.domain.task_orders import TaskOrders @@ -12,13 +12,34 @@ from atst.models.task_order import Status as TaskOrderStatus def portfolio_task_orders(portfolio_id): portfolio = Portfolios.get(g.current_user, portfolio_id) task_orders_by_status = defaultdict(list) + serialize_task_order = lambda task_order: { + key: getattr(task_order, key) + for key in [ + "id", + "budget", + "time_created", + "start_date", + "end_date", + "display_status", + "balance", + ] + } + for task_order in portfolio.task_orders: - task_orders_by_status[task_order.status].append(task_order) + serialized_task_order = serialize_task_order(task_order) + serialized_task_order["url"] = url_for( + "portfolios.view_task_order", + portfolio_id=portfolio.id, + task_order_id=task_order.id, + ) + task_orders_by_status[task_order.status].append(serialized_task_order) return render_template( "portfolios/task_orders/index.html", portfolio=portfolio, pending_task_orders=task_orders_by_status.get(TaskOrderStatus.PENDING, []), + active_task_orders=task_orders_by_status.get(TaskOrderStatus.ACTIVE, []), + expired_task_orders=task_orders_by_status.get(TaskOrderStatus.EXPIRED, []), ) From e92ee12f204d843ca7d119c3bd36568138803fdd Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Tue, 15 Jan 2019 17:30:33 -0500 Subject: [PATCH 05/13] Sortable listing of task orders on funding page --- js/components/tables/task_order_list.js | 102 ++++++++++++++++++++ js/index.js | 2 + templates/portfolios/task_orders/index.html | 75 ++++++++++++-- 3 files changed, 169 insertions(+), 10 deletions(-) create mode 100644 js/components/tables/task_order_list.js diff --git a/js/components/tables/task_order_list.js b/js/components/tables/task_order_list.js new file mode 100644 index 00000000..b8015139 --- /dev/null +++ b/js/components/tables/task_order_list.js @@ -0,0 +1,102 @@ +import { set } from 'vue/dist/vue' +import { compose, sortBy, reverse, indexBy, prop, toLower } from 'ramda' + +import { formatDollars } from '../../lib/dollars' +import localDatetime from '../../components/local_datetime' + +const sort = (sortInfo, members) => { + if (sortInfo.columnName === '') { + return members + } else { + const sortColumn = sortInfo.columns[sortInfo.columnName] + const sortedMembers = sortColumn.sortFunc(sortColumn.attr, members) + + return sortInfo.isAscending ? + sortedMembers : + reverse(sortedMembers) + } +} + +export default { + name: 'task-order-list', + + props: { + data: Array + }, + + components: { + localDatetime + }, + + data: function () { + const alphabeticalSort = (attr, members) => { + const lowercaseProp = compose(toLower, prop(attr)) + return sortBy(lowercaseProp, members) + } + + const numericSort = (attr, members) => sortBy(prop(attr), members) + const columns = [ + { + displayName: 'Status', + attr: 'display_status', + }, + { + displayName: 'Period of Performance', + attr: 'start_date', + sortFunc: numericSort, + width: "50%" + }, + { + displayName: 'Initial Value', + attr: 'budget', + class: "table-cell--align-right", + sortFunc: numericSort + }, + { + displayName: 'Balance', + attr: 'budget', + class: "table-cell--align-right", + sortFunc: numericSort + }, + { + displayName: '' + } + ] + + const defaultSortColumn = 'Period of Performance' + return { + sortInfo: { + columnName: defaultSortColumn, + isAscending: false, + columns: indexBy(prop('displayName'), columns) + } + } + }, + + computed: { + taskOrders: function () { + return sort(this.sortInfo, this.data) + } + }, + + methods: { + updateSort: function(columnName) { + // clicking a column twice toggles ascending / descending + if (columnName === this.sortInfo.columnName) { + this.sortInfo.isAscending = !this.sortInfo.isAscending + } + + this.sortInfo.columnName = columnName + }, + + getColumns: function() { + return Object.values(this.sortInfo.columns) + }, + + formatDollars: function (value) { + return formatDollars(value, false) + } + }, + + template: '
    ' +} diff --git a/js/index.js b/js/index.js index 97fe3acc..a3c7a390 100644 --- a/js/index.js +++ b/js/index.js @@ -21,6 +21,7 @@ import Modal from './mixins/modal' import selector from './components/selector' import BudgetChart from './components/charts/budget_chart' import SpendTable from './components/tables/spend_table' +import TaskOrderList from './components/tables/task_order_list.js' import CcpoApproval from './components/forms/ccpo_approval' import MembersList from './components/members_list' import LocalDatetime from './components/local_datetime' @@ -48,6 +49,7 @@ const app = new Vue({ selector, BudgetChart, SpendTable, + TaskOrderList, CcpoApproval, MembersList, LocalDatetime, diff --git a/templates/portfolios/task_orders/index.html b/templates/portfolios/task_orders/index.html index e01ae94d..9d878413 100644 --- a/templates/portfolios/task_orders/index.html +++ b/templates/portfolios/task_orders/index.html @@ -6,13 +6,72 @@ {% block portfolio_content %} {% macro ViewLink(task_order) %} - View {{ Icon("caret_right") }} {% endmacro %} +{% macro TaskOrderList(task_orders, label='success') %} + +
    + + + + + + + + + + + + + + + + +
    + !{ col.displayName } + +
    + !{ taskOrder.display_status } + + + + + - + + + + + + + + + View + {{ Icon("caret_right") }} + +
    +
    +
    +{% endmacro %} +
    {% for task_order in pending_task_orders %}
    @@ -46,16 +105,12 @@ ) }} {% endif %} - + {% if active_task_orders %} + {{ TaskOrderList(active_task_orders, label='success') }} + {% endif %} + {% if expired_task_orders %} + {{ TaskOrderList(expired_task_orders, label='') }} {% endif %}
    From 8bd1d131e94c7930af01b29bc33230bf1c8da873 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Tue, 15 Jan 2019 17:31:38 -0500 Subject: [PATCH 06/13] Update nav name from "Task Orders" to "Funding" --- translations.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/translations.yaml b/translations.yaml index 987addb9..bff5d596 100644 --- a/translations.yaml +++ b/translations.yaml @@ -254,7 +254,7 @@ navigation: activity_log: Activity Log members: Members applications: Applications - task_orders: Task Orders + task_orders: Funding portfolio_settings: Portfolio Settings requests: _new: From b2782c0b66ade97f33b92d242fb9cbf4c89e817d Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Tue, 15 Jan 2019 17:49:48 -0500 Subject: [PATCH 07/13] Add portfolio funding header --- atst/routes/portfolios/task_orders.py | 11 ++++++++++- styles/components/_portfolio_layout.scss | 21 +++++++++++++++++++++ templates/portfolios/task_orders/index.html | 18 ++++++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/atst/routes/portfolios/task_orders.py b/atst/routes/portfolios/task_orders.py index 8c326775..d5c6b78f 100644 --- a/atst/routes/portfolios/task_orders.py +++ b/atst/routes/portfolios/task_orders.py @@ -1,4 +1,5 @@ from collections import defaultdict +from operator import itemgetter from flask import g, render_template, url_for @@ -34,12 +35,20 @@ def portfolio_task_orders(portfolio_id): ) task_orders_by_status[task_order.status].append(serialized_task_order) + active_task_orders = task_orders_by_status.get(TaskOrderStatus.ACTIVE, []) + funding_end_date = ( + sorted(active_task_orders, key=itemgetter("end_date"))[-1]["end_date"] + if active_task_orders + else None + ) + return render_template( "portfolios/task_orders/index.html", portfolio=portfolio, pending_task_orders=task_orders_by_status.get(TaskOrderStatus.PENDING, []), - active_task_orders=task_orders_by_status.get(TaskOrderStatus.ACTIVE, []), + active_task_orders=active_task_orders, expired_task_orders=task_orders_by_status.get(TaskOrderStatus.EXPIRED, []), + funding_end_date=funding_end_date, ) diff --git a/styles/components/_portfolio_layout.scss b/styles/components/_portfolio_layout.scss index 6a6db378..ca546914 100644 --- a/styles/components/_portfolio_layout.scss +++ b/styles/components/_portfolio_layout.scss @@ -31,6 +31,27 @@ } .portfolio-funding { + .portfolio-funding__header { + padding: 0; + margin: 0 $gap; + + align-items: center; + + .portfolio-funding__header--funded-through { + padding: 2 * $gap; + flex-grow: 1; + text-align: left; + font-weight: bold; + } + + .funded { + color: $color-green; + .icon { + @include icon-color($color-green); + } + } + } + .pending-task-order { background-color: $color-gold-lightest; diff --git a/templates/portfolios/task_orders/index.html b/templates/portfolios/task_orders/index.html index 9d878413..ea5ad818 100644 --- a/templates/portfolios/task_orders/index.html +++ b/templates/portfolios/task_orders/index.html @@ -73,6 +73,24 @@ {% endmacro %}
    + +
    +
    +

    Portfolio Funding

    +
    + {% if funding_end_date %} + {{ Icon('ok') }} + Funded through + + + {% endif %} +
    + Start a New Task Order +
    +
    + {% for task_order in pending_task_orders %}
    From 26c898da0de295b92e9dc33be82bbbe06dd7c95c Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Tue, 15 Jan 2019 17:57:26 -0500 Subject: [PATCH 08/13] Add row for total balance --- atst/routes/portfolios/task_orders.py | 2 ++ styles/components/_portfolio_layout.scss | 10 ++++++++++ templates/portfolios/task_orders/index.html | 6 ++++++ 3 files changed, 18 insertions(+) diff --git a/atst/routes/portfolios/task_orders.py b/atst/routes/portfolios/task_orders.py index d5c6b78f..9155c4f0 100644 --- a/atst/routes/portfolios/task_orders.py +++ b/atst/routes/portfolios/task_orders.py @@ -41,6 +41,7 @@ def portfolio_task_orders(portfolio_id): if active_task_orders else None ) + total_balance = sum([task_order["balance"] for task_order in active_task_orders]) return render_template( "portfolios/task_orders/index.html", @@ -49,6 +50,7 @@ def portfolio_task_orders(portfolio_id): active_task_orders=active_task_orders, expired_task_orders=task_orders_by_status.get(TaskOrderStatus.EXPIRED, []), funding_end_date=funding_end_date, + total_balance=total_balance, ) diff --git a/styles/components/_portfolio_layout.scss b/styles/components/_portfolio_layout.scss index ca546914..853efc4c 100644 --- a/styles/components/_portfolio_layout.scss +++ b/styles/components/_portfolio_layout.scss @@ -83,4 +83,14 @@ .view-task-order-link { margin-left: $gap * 2; } + + .portfolio-total-balance { + flex-direction: row-reverse; + margin: 2 * $gap 0; + padding-right: 14rem; + + .label { + margin: 0 2 * $gap; + } + } } diff --git a/templates/portfolios/task_orders/index.html b/templates/portfolios/task_orders/index.html index ea5ad818..52172327 100644 --- a/templates/portfolios/task_orders/index.html +++ b/templates/portfolios/task_orders/index.html @@ -125,6 +125,12 @@ {% if active_task_orders %} {{ TaskOrderList(active_task_orders, label='success') }} +
    +
    + {{ total_balance | dollars }} + Total Active Balance +
    +
    {% endif %} {% if expired_task_orders %} From c1584087e33556e4e90d71c260277bb2aae9a73a Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Tue, 15 Jan 2019 18:23:44 -0500 Subject: [PATCH 09/13] Bring balance closer to active TO table --- styles/components/_portfolio_layout.scss | 13 ++++++++----- templates/portfolios/task_orders/index.html | 4 ++-- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/styles/components/_portfolio_layout.scss b/styles/components/_portfolio_layout.scss index 853efc4c..23175add 100644 --- a/styles/components/_portfolio_layout.scss +++ b/styles/components/_portfolio_layout.scss @@ -85,12 +85,15 @@ } .portfolio-total-balance { - flex-direction: row-reverse; - margin: 2 * $gap 0; - padding-right: 14rem; + margin-top: -$gap; + .row { + flex-direction: row-reverse; + margin: 2 * $gap 0; + padding-right: 14rem; - .label { - margin: 0 2 * $gap; + .label { + margin: 0 2 * $gap; + } } } } diff --git a/templates/portfolios/task_orders/index.html b/templates/portfolios/task_orders/index.html index 52172327..0f1df834 100644 --- a/templates/portfolios/task_orders/index.html +++ b/templates/portfolios/task_orders/index.html @@ -125,8 +125,8 @@ {% if active_task_orders %} {{ TaskOrderList(active_task_orders, label='success') }} -
    -
    +
    +
    {{ total_balance | dollars }} Total Active Balance
    From b2605e3a852cb3c176da11593a6acdb7b8427249 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Tue, 15 Jan 2019 18:33:00 -0500 Subject: [PATCH 10/13] Add even more task orders for sample portfolios --- script/seed_sample.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/script/seed_sample.py b/script/seed_sample.py index 1395d7b0..8ba4692b 100644 --- a/script/seed_sample.py +++ b/script/seed_sample.py @@ -110,16 +110,33 @@ def seed_db(): ) db.session.add(invitation) - [expired_start, expired_end] = sorted( + [old_expired_start, expired_start, expired_end] = 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), ] ) - active_start = expired_end - active_end = random_future_date(year_min=1, year_max=1) + [ + first_active_start, + second_active_start, + first_active_end, + second_active_end, + ] = sorted( + [ + expired_end, + random_past_date(year_max=1, year_min=1), + random_future_date(year_min=0, year_max=1), + random_future_date(year_min=1, year_max=1), + ] + ) - date_ranges = [(expired_start, expired_end), (active_start, active_end)] + date_ranges = [ + (old_expired_start, expired_start), + (expired_start, expired_end), + (first_active_start, first_active_end), + (second_active_start, second_active_end), + ] for (start_date, end_date) in date_ranges: task_order = TaskOrderFactory.build( start_date=start_date, From b1348b52e51bc5053cf4b14801fd46eb1f2ec7b6 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Tue, 15 Jan 2019 20:43:48 -0500 Subject: [PATCH 11/13] Rename route from to be more general --- atst/routes/portfolios/task_orders.py | 2 +- templates/navigation/portfolio_navigation.html | 4 ++-- translations.yaml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/atst/routes/portfolios/task_orders.py b/atst/routes/portfolios/task_orders.py index 9155c4f0..b8e76ef3 100644 --- a/atst/routes/portfolios/task_orders.py +++ b/atst/routes/portfolios/task_orders.py @@ -10,7 +10,7 @@ from atst.models.task_order import Status as TaskOrderStatus @portfolios_bp.route("/portfolios//task_orders") -def portfolio_task_orders(portfolio_id): +def portfolio_funding(portfolio_id): portfolio = Portfolios.get(g.current_user, portfolio_id) task_orders_by_status = defaultdict(list) serialize_task_order = lambda task_order: { diff --git a/templates/navigation/portfolio_navigation.html b/templates/navigation/portfolio_navigation.html index ee8a2870..5bfca5c6 100644 --- a/templates/navigation/portfolio_navigation.html +++ b/templates/navigation/portfolio_navigation.html @@ -41,8 +41,8 @@ {% endif %} {{ SidenavItem( - ("navigation.portfolio_navigation.task_orders" | translate), - href=url_for("portfolios.portfolio_task_orders", portfolio_id=portfolio.id), + ("navigation.portfolio_navigation.portfolio_funding" | translate), + href=url_for("portfolios.portfolio_funding", portfolio_id=portfolio.id), active=request.url_rule.rule.startswith('/portfolios//task_order'), subnav=None ) }} diff --git a/translations.yaml b/translations.yaml index bff5d596..64645809 100644 --- a/translations.yaml +++ b/translations.yaml @@ -254,7 +254,7 @@ navigation: activity_log: Activity Log members: Members applications: Applications - task_orders: Funding + portfolio_funding: Funding portfolio_settings: Portfolio Settings requests: _new: From ba97117a74b3ea6285ee0ef06031e6948c48b2f7 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Tue, 15 Jan 2019 21:10:15 -0500 Subject: [PATCH 12/13] Add tests for portfolio funding route This adds a helper to grab a template's context. Using a helper from the Flask documentation: http://flask.pocoo.org/docs/1.0/signals/?highlight=template_rendered#subscribing-to-signals --- Pipfile | 1 + Pipfile.lock | 73 +++++++++++---------- tests/routes/portfolios/test_task_orders.py | 62 +++++++++++++++++ tests/utils.py | 16 +++++ 4 files changed, 119 insertions(+), 33 deletions(-) create mode 100644 tests/routes/portfolios/test_task_orders.py create mode 100644 tests/utils.py diff --git a/Pipfile b/Pipfile index b0c8024d..cc2d7314 100644 --- a/Pipfile +++ b/Pipfile @@ -38,6 +38,7 @@ pytest-env = "*" pytest-cov = "*" selenium = "*" honcho = "*" +blinker = "*" [requires] python_version = "3.6.6" diff --git a/Pipfile.lock b/Pipfile.lock index 7aefaeb6..3944528d 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "339ade228d14f03a31061a114103a5f61686b6efe812a1e0295268e08c18f149" + "sha256": "9f750c048a57b4494e2f4e53711a52a8e9f6ae3f6bbdb5d49491510ca105f40f" }, "pipfile-spec": 6, "requires": { @@ -18,10 +18,10 @@ "default": { "alembic": { "hashes": [ - "sha256:e9ffdece0eece55f4108b14b6b0f29ffc730d58e28446a434fe41a1cc5c5f266" + "sha256:35660f7e6159288e2be111126be148ef04cbf7306da73c8b8bd4400837bb08e3" ], "index": "pypi", - "version": "==1.0.5" + "version": "==1.0.6" }, "apache-libcloud": { "hashes": [ @@ -499,6 +499,13 @@ "index": "pypi", "version": "==18.9b0" }, + "blinker": { + "hashes": [ + "sha256:471aee25f3992bd325afa3772f1063dbdbbca947a041b8b89466dc00d606f8b6" + ], + "index": "pypi", + "version": "==1.4" + }, "click": { "hashes": [ "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", @@ -775,10 +782,10 @@ }, "pluggy": { "hashes": [ - "sha256:447ba94990e8014ee25ec853339faf7b0fc8050cdc3289d4d71f7f410fb90095", - "sha256:bde19360a8ec4dfd8a20dcb811780a30998101f078fc7ded6162f0076f50508f" + "sha256:8ddc32f03971bfdf900a81961a48ccf2fb677cf7715108f85295c67405798616", + "sha256:980710797ff6a041e9a73a5787804f848996ecaa6f8a1b1e08224a5894f2074a" ], - "version": "==0.8.0" + "version": "==0.8.1" }, "prompt-toolkit": { "hashes": [ @@ -827,11 +834,11 @@ }, "pytest-cov": { "hashes": [ - "sha256:513c425e931a0344944f84ea47f3956be0e416d95acbd897a44970c8d926d5d7", - "sha256:e360f048b7dae3f2f2a9a4d067b2dd6b6a015d384d1577c994a43f3f7cbad762" + "sha256:0ab664b25c6aa9716cbf203b17ddb301932383046082c081b9848a0edf5add33", + "sha256:230ef817450ab0699c6cc3c9c8f7a829c34674456f2ed8df1fe1d39780f7c87f" ], "index": "pypi", - "version": "==2.6.0" + "version": "==2.6.1" }, "pytest-env": { "hashes": [ @@ -924,30 +931,30 @@ }, "typed-ast": { "hashes": [ - "sha256:0555eca1671ebe09eb5f2176723826f6f44cca5060502fea259de9b0e893ab53", - "sha256:0ca96128ea66163aea13911c9b4b661cb345eb729a20be15c034271360fc7474", - "sha256:16ccd06d614cf81b96de42a37679af12526ea25a208bce3da2d9226f44563868", - "sha256:1e21ae7b49a3f744958ffad1737dfbdb43e1137503ccc59f4e32c4ac33b0bd1c", - "sha256:37670c6fd857b5eb68aa5d193e14098354783b5138de482afa401cc2644f5a7f", - "sha256:46d84c8e3806619ece595aaf4f37743083f9454c9ea68a517f1daa05126daf1d", - "sha256:5b972bbb3819ece283a67358103cc6671da3646397b06e7acea558444daf54b2", - "sha256:6306ffa64922a7b58ee2e8d6f207813460ca5a90213b4a400c2e730375049246", - "sha256:6cb25dc95078931ecbd6cbcc4178d1b8ae8f2b513ae9c3bd0b7f81c2191db4c6", - "sha256:7e19d439fee23620dea6468d85bfe529b873dace39b7e5b0c82c7099681f8a22", - "sha256:7f5cd83af6b3ca9757e1127d852f497d11c7b09b4716c355acfbebf783d028da", - "sha256:81e885a713e06faeef37223a5b1167615db87f947ecc73f815b9d1bbd6b585be", - "sha256:94af325c9fe354019a29f9016277c547ad5d8a2d98a02806f27a7436b2da6735", - "sha256:b1e5445c6075f509d5764b84ce641a1535748801253b97f3b7ea9d948a22853a", - "sha256:cb061a959fec9a514d243831c514b51ccb940b58a5ce572a4e209810f2507dcf", - "sha256:cc8d0b703d573cbabe0d51c9d68ab68df42a81409e4ed6af45a04a95484b96a5", - "sha256:da0afa955865920edb146926455ec49da20965389982f91e926389666f5cf86a", - "sha256:dc76738331d61818ce0b90647aedde17bbba3d3f9e969d83c1d9087b4f978862", - "sha256:e7ec9a1445d27dbd0446568035f7106fa899a36f55e52ade28020f7b3845180d", - "sha256:f741ba03feb480061ab91a465d1a3ed2d40b52822ada5b4017770dfcb88f839f", - "sha256:fe800a58547dd424cd286b7270b967b5b3316b993d86453ede184a17b5a6b17d" + "sha256:023625bfa9359e29bd6e24cac2a4503495b49761d48a5f1e38333fc4ac4d93fe", + "sha256:07591f7a5fdff50e2e566c4c1e9df545c75d21e27d98d18cb405727ed0ef329c", + "sha256:153e526b0f4ffbfada72d0bb5ffe8574ba02803d2f3a9c605c8cf99dfedd72a2", + "sha256:3ad2bdcd46a4a1518d7376e9f5016d17718a9ed3c6a3f09203d832f6c165de4a", + "sha256:3ea98c84df53ada97ee1c5159bb3bc784bd734231235a1ede14c8ae0775049f7", + "sha256:51a7141ccd076fa561af107cfb7a8b6d06a008d92451a1ac7e73149d18e9a827", + "sha256:52c93cd10e6c24e7ac97e8615da9f224fd75c61770515cb323316c30830ddb33", + "sha256:6344c84baeda3d7b33e157f0b292e4dd53d05ddb57a63f738178c01cac4635c9", + "sha256:64699ca1b3bd5070bdeb043e6d43bc1d0cebe08008548f4a6bee782b0ecce032", + "sha256:74903f2e56bbffe29282ef8a5487d207d10be0f8513b41aff787d954a4cf91c9", + "sha256:7891710dba83c29ee2bd51ecaa82f60f6bede40271af781110c08be134207bf2", + "sha256:91976c56224e26c256a0de0f76d2004ab885a29423737684b4f7ebdd2f46dde2", + "sha256:9bad678a576ecc71f25eba9f1e3fd8d01c28c12a2834850b458428b3e855f062", + "sha256:b4726339a4c180a8b6ad9d8b50d2b6dc247e1b79b38fe2290549c98e82e4fd15", + "sha256:ba36f6aa3f8933edf94ea35826daf92cbb3ec248b89eccdc053d4a815d285357", + "sha256:bbc96bde544fd19e9ef168e4dfa5c3dfe704bfa78128fa76f361d64d6b0f731a", + "sha256:c0c927f1e44469056f7f2dada266c79b577da378bbde3f6d2ada726d131e4824", + "sha256:c0f9a3708008aa59f560fa1bd22385e05b79b8e38e0721a15a8402b089243442", + "sha256:f0bf6f36ff9c5643004171f11d2fdc745aa3953c5aacf2536a0685db9ceb3fb1", + "sha256:f5be39a0146be663cbf210a4d95c3c58b2d7df7b043c9047c5448e358f0550a2", + "sha256:fcd198bf19d9213e5cbf2cde2b9ef20a9856e716f76f9476157f90ae6de06cc6" ], "markers": "python_version < '3.7' and implementation_name == 'cpython'", - "version": "==1.1.1" + "version": "==1.2.0" }, "urllib3": { "hashes": [ @@ -978,9 +985,9 @@ }, "wrapt": { "hashes": [ - "sha256:d4d560d479f2c21e1b5443bbd15fe7ec4b37fe7e53d335d3b9b0a7b1226fe3c6" + "sha256:e03f19f64d81d0a3099518ca26b04550026f131eced2e76ced7b85c6b8d32128" ], - "version": "==1.10.11" + "version": "==1.11.0" } } } diff --git a/tests/routes/portfolios/test_task_orders.py b/tests/routes/portfolios/test_task_orders.py new file mode 100644 index 00000000..1b2c6aba --- /dev/null +++ b/tests/routes/portfolios/test_task_orders.py @@ -0,0 +1,62 @@ +from flask import url_for +import pytest + +from tests.factories import ( + PortfolioFactory, + TaskOrderFactory, + random_future_date, + random_past_date, +) +from tests.utils import captured_templates + + +class TestPortfolioFunding: + def test_unfunded_portfolio(self, app, user_session): + portfolio = PortfolioFactory.create() + user_session(portfolio.owner) + + 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 None + assert context["total_balance"] == 0 + assert context["pending_task_orders"] == [] + assert context["active_task_orders"] == [] + assert context["expired_task_orders"] == [] + + def test_funded_portfolio(self, app, user_session): + portfolio = PortfolioFactory.create() + user_session(portfolio.owner) + + pending_to = TaskOrderFactory.create(portfolio=portfolio) + active_to1 = TaskOrderFactory.create( + portfolio=portfolio, + start_date=random_past_date(), + end_date=random_future_date(), + number="42", + ) + active_to2 = TaskOrderFactory.create( + portfolio=portfolio, + start_date=random_past_date(), + end_date=random_future_date(), + number="43", + ) + end_date = ( + active_to1.end_date + if active_to1.end_date > active_to2.end_date + else active_to2.end_date + ) + + 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 end_date + assert context["total_balance"] == active_to1.budget + active_to2.budget diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 00000000..c590136e --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,16 @@ +from flask import template_rendered +from contextlib import contextmanager + + +@contextmanager +def captured_templates(app): + recorded = [] + + def record(sender, template, context, **extra): + recorded.append((template, context)) + + template_rendered.connect(record, app) + try: + yield recorded + finally: + template_rendered.disconnect(record, app) From c2101b007da4e84b7563c08726917ef42c7ea1a1 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Tue, 15 Jan 2019 21:22:22 -0500 Subject: [PATCH 13/13] Update display of expired balances on portfolio funding --- js/components/tables/task_order_list.js | 5 +++-- styles/components/_portfolio_layout.scss | 6 ++++++ templates/portfolios/task_orders/index.html | 9 +++++---- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/js/components/tables/task_order_list.js b/js/components/tables/task_order_list.js index b8015139..d8b6cfbd 100644 --- a/js/components/tables/task_order_list.js +++ b/js/components/tables/task_order_list.js @@ -21,7 +21,8 @@ export default { name: 'task-order-list', props: { - data: Array + data: Array, + expired: Boolean }, components: { @@ -53,7 +54,7 @@ export default { sortFunc: numericSort }, { - displayName: 'Balance', + displayName: this.expired ? 'Expired Balance' : 'Balance', attr: 'budget', class: "table-cell--align-right", sortFunc: numericSort diff --git a/styles/components/_portfolio_layout.scss b/styles/components/_portfolio_layout.scss index 23175add..6702d67d 100644 --- a/styles/components/_portfolio_layout.scss +++ b/styles/components/_portfolio_layout.scss @@ -96,4 +96,10 @@ } } } + + table { + td.unused-balance { + color: $color-red; + } + } } diff --git a/templates/portfolios/task_orders/index.html b/templates/portfolios/task_orders/index.html index 0f1df834..4951c63d 100644 --- a/templates/portfolios/task_orders/index.html +++ b/templates/portfolios/task_orders/index.html @@ -12,10 +12,11 @@ {% endmacro %} -{% macro TaskOrderList(task_orders, label='success') %} +{% macro TaskOrderList(task_orders, label='success', expired=False) %}