Merge pull request #868 from dod-ccpo/funding-page

Funding page
This commit is contained in:
richard-dds 2019-06-06 11:38:30 -04:00 committed by GitHub
commit e327a0bada
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 114 additions and 160 deletions

View File

@ -1,6 +1,6 @@
from collections import defaultdict from collections import defaultdict
from flask import g, render_template, url_for from flask import g, render_template
from . import task_orders_bp from . import task_orders_bp
from atst.domain.authz.decorator import user_can_access_decorator as user_can from atst.domain.authz.decorator import user_can_access_decorator as user_can
@ -30,22 +30,6 @@ def review_task_order(task_order_id):
return render_template("portfolios/task_orders/review.html", task_order=task_order) return render_template("portfolios/task_orders/review.html", task_order=task_order)
def serialize_task_order(task_order):
return {
key: getattr(task_order, key)
for key in [
"id",
"budget",
"time_created",
"start_date",
"end_date",
"display_status",
"days_to_expiration",
"balance",
]
}
@task_orders_bp.route("/portfolios/<portfolio_id>/task_orders") @task_orders_bp.route("/portfolios/<portfolio_id>/task_orders")
@user_can(Permissions.VIEW_PORTFOLIO_FUNDING, message="view portfolio funding") @user_can(Permissions.VIEW_PORTFOLIO_FUNDING, message="view portfolio funding")
def portfolio_funding(portfolio_id): def portfolio_funding(portfolio_id):
@ -53,14 +37,9 @@ def portfolio_funding(portfolio_id):
task_orders_by_status = defaultdict(list) task_orders_by_status = defaultdict(list)
for task_order in portfolio.task_orders: for task_order in portfolio.task_orders:
serialized_task_order = serialize_task_order(task_order) task_orders_by_status[task_order.status].append(task_order)
serialized_task_order["url"] = url_for(
"task_orders.view_task_order", task_order_id=task_order.id
)
task_orders_by_status[task_order.status].append(serialized_task_order)
active_task_orders = task_orders_by_status.get(TaskOrderStatus.ACTIVE, []) active_task_orders = task_orders_by_status.get(TaskOrderStatus.ACTIVE, [])
total_balance = sum([task_order["balance"] for task_order in active_task_orders])
return render_template( return render_template(
"portfolios/task_orders/index.html", "portfolios/task_orders/index.html",
@ -70,5 +49,4 @@ def portfolio_funding(portfolio_id):
), ),
active_task_orders=active_task_orders, active_task_orders=active_task_orders,
expired_task_orders=task_orders_by_status.get(TaskOrderStatus.EXPIRED, []), expired_task_orders=task_orders_by_status.get(TaskOrderStatus.EXPIRED, []),
total_balance=total_balance,
) )

View File

@ -22,6 +22,7 @@
@import "elements/kpi"; @import "elements/kpi";
@import "elements/graphs"; @import "elements/graphs";
@import "elements/menu"; @import "elements/menu";
@import "elements/card";
@import "components/accordion_table"; @import "components/accordion_table";
@import "components/topbar"; @import "components/topbar";

View File

@ -20,7 +20,7 @@
flex-direction: row; flex-direction: row;
} }
margin-bottom: $gap * 4; margin-bottom: $gap * 1;
.col--grow { .col--grow {
overflow: inherit; overflow: inherit;
@ -127,7 +127,7 @@
} }
.portfolio-content { .portfolio-content {
margin: 6 * $gap $gap 0 $gap; margin: 1 * $gap $gap 0 $gap;
.panel { .panel {
@include shadow-panel; @include shadow-panel;
@ -373,7 +373,7 @@
} }
.portfolio-funding { .portfolio-funding {
padding: 2 * $gap; padding: (2 * $gap) 0;
.panel { .panel {
@include shadow-panel; @include shadow-panel;

View File

@ -4,12 +4,12 @@
} }
@include media($medium-screen) { @include media($medium-screen) {
margin-left: -$gap * 7; margin-left: -$gap * 5;
} }
} }
.sticky-cta.js-is-sticky { .sticky-cta.js-is-sticky {
width: 78.5%; width: 80.8%;
} }
.sticky-cta-container { .sticky-cta-container {

View File

@ -165,6 +165,7 @@ $checkbox-border-radius: 2px;
$border-radius: 3px; $border-radius: 3px;
$button-border-radius: 5px; $button-border-radius: 5px;
$box-shadow: 0px 2px 5px 0px $color-shadow; $box-shadow: 0px 2px 5px 0px $color-shadow;
$box-shadow-big: 0 4px 10px 0 rgba(193, 193, 193, 0.5);
$focus-outline: 2px dotted $color-gray-light; $focus-outline: 2px dotted $color-gray-light;
$focus-spacing: 3px; $focus-spacing: 3px;
$nav-width: 300px; $nav-width: 300px;

View File

@ -0,0 +1,34 @@
.card {
width: 100%;
box-shadow: $box-shadow-big;
padding: ($gap * 2) ($gap * 2.5) ($gap * 4) ($gap * 3);
margin-bottom: 20px;
.card__status {
display: flex;
align-items: baseline;
justify-content: space-around;
.card__status-spacer {
flex-grow: 10;
}
}
.card__header h3 {
margin-top: 0;
}
.card__body {
font-size: $small-font-size;
}
}
.card {
.label {
margin-left: 0;
}
.datetime {
font-size: $small-font-size;
}
}

View File

@ -66,6 +66,19 @@
} }
} }
.task-order-list {
margin-top: 6 * $gap;
}
.task-order-card .label {
font-size: $small-font-size;
margin-right: 2 * $gap;
}
.task-order-card__buttons .usa-button {
min-width: 10rem;
}
.task-order-summary { .task-order-summary {
margin: $gap * 4; margin: $gap * 4;

View File

@ -7,7 +7,6 @@
{% block portfolio_header %} {% block portfolio_header %}
{% include "portfolios/header.html" %} {% include "portfolios/header.html" %}
{% endblock %} {% endblock %}
<div class='line'></div>
<div class='portfolio-content'> <div class='portfolio-content'>
{% block portfolio_content %}{% endblock %} {% block portfolio_content %}{% endblock %}
</div> </div>

View File

@ -6,121 +6,71 @@
{% block portfolio_content %} {% block portfolio_content %}
{% macro ViewLink(task_order) %} {% macro ViewLink(task_order, text="Edit") %}
<a href="{{ url_for('task_orders.view_task_order', task_order_id=task_order.id) }}" class="icon-link view-task-order-link"> <a href="{{ url_for('task_orders.view_task_order', task_order_id=task_order.id) }}" class="usa-button">
<span>View</span> {{ text }}
{{ Icon("caret_right", classes="icon--tiny") }}
</a> </a>
{% endmacro %} {% endmacro %}
{% macro TaskOrderList(task_orders, label='success', expired=False, funded=False) %} {% macro TaskOrderDateTime(dt, className="") %}
<task-order-list <local-datetime timestamp="{{ dt }}" format="MMMM D, YYYY" class="{{ className }}"></local-datetime>
inline-template {% endmacro %}
v-bind:data='{{ task_orders | tojson }}'
v-bind:expired='{{ 'true' if expired else 'false' }}'
v-bind:funded='{{'true' if funded else 'false' }}'
v-cloak
>
<div class='responsive-table-wrapper'>
<table v-cloak class="atat-table">
<thead>
<tr>
<th v-for="col in getColumns()" @click="updateSort(col.displayName)" :width="col.width" :class="col.class" scope="col">
!{ col.displayName }
<template v-if="col.sortFunc">
<span class="sorting-direction" v-if="col.displayName === sortInfo.columnName && sortInfo.isAscending">
{{ Icon("caret_down", classes="icon--tiny") }}
</span>
<span class="sorting-direction" v-if="col.displayName === sortInfo.columnName && !sortInfo.isAscending">
{{ Icon("caret_up", classes="icon--tiny") }}
</span>
</template>
</th>
</tr>
</thead>
<tbody> {% macro TaskOrderDate(task_order) %}
<tr v-for='taskOrder in taskOrders' :key="taskOrder.id"> <span class="datetime">
<td> {% if task_order.is_active %}
<span class='label label--{{ label }}'>!{ taskOrder.display_status }</span> Began {{ TaskOrderDateTime(task_order.start_date) }} &nbsp;&nbsp;|&nbsp;&nbsp; Ends {{ TaskOrderDateTime(task_order.end_date) }}
</td> {% elif task_order.is_expired %}
<td> Started {{ TaskOrderDateTime(task_order.start_date) }} &nbsp;&nbsp;|&nbsp;&nbsp; Ended {{ TaskOrderDateTime(task_order.end_date) }}
<span :class="{ 'to-performance-period': true, 'to-expiring-soon': (taskOrder.days_to_expiration > 0 && taskOrder.days_to_expiration <= days_to_exp_alert_limit), 'funded': funded && taskOrder.display_status === 'Active', 'unfunded': !funded && taskOrder.display_status === 'Active' }"> {% else %}
<local-datetime Started {{ TaskOrderDateTime(task_order.start_date) }}
v-bind:timestamp="taskOrder.start_date" {% endif %}
format="M/D/YYYY"> </span>
</local-datetime> {% endmacro %}
-
<local-datetime {% macro TaskOrderActions(task_order) %}
v-bind:timestamp="taskOrder.end_date" <div class="task-order-card__buttons">
format="M/D/YYYY" {% if task_order.is_pending %}
class="to-end-date" {{ ViewLink(task_order, text="Edit") }}
> {% elif task_order.is_active %}
</local-datetime> {{ ViewLink(task_order, text="Modify") }}
<span {% else %}
v-if="taskOrder.days_to_expiration > 0 && taskOrder.days_to_expiration <= days_to_exp_alert_limit && funded" {{ ViewLink(task_order, text="View") }}
class="to-expiration-alert"> {% endif %}
{{ Icon('ok') }} Period ending in !{ taskOrder.days_to_expiration } days, but new period funded </div>
</span> {% endmacro %}
<span
v-if="taskOrder.days_to_expiration > 0 && taskOrder.days_to_expiration <= days_to_exp_alert_limit && !funded" {% macro TaskOrderList(task_orders, label='success') %}
class="to-expiration-alert"> <div class="task-order-list">
{{ Icon('alert') }} Period ends in !{ taskOrder.days_to_expiration } days, submit a new task order {% for task_order in task_orders %}
</span> <div class="card task-order-card">
</span> <div class="card__status">
</td> <span class='label label--{{ label }}'>{{ task_order.display_status }}</span>
<td class="table-cell--align-right"> {{ TaskOrderDate(task_order) }}
<span v-html='formatDollars(taskOrder.budget)'></span> <span class="card__status-spacer"></span>
</td> <span class="card__button">
<td v-bind:class="{ 'table-cell--align-right': true, 'unused-balance': expired && taskOrder.balance > 0 }"> {{ TaskOrderActions(task_order) }}
<span v-html='formatDollars(taskOrder.balance)'></span> </span>
</td> </div>
<td> <div class="card__header">
<a v-bind:href="taskOrder.url" class="icon-link view-task-order-link"> <h3>Task Order #{{ task_order.number }}</h3>
<span>View</span> </div>
{{ Icon("caret_right", classes="icon--tiny") }} <div class="card__body">
</a> <b>Obligated amount: </b>${{ task_order.total_obligated_funds }}
</td> </div>
</tr> </div>
{{ caller and caller() }} {% endfor %}
</tbody> </div>
</table>
</div>
</task-order-list>
{% endmacro %} {% endmacro %}
<div class="portfolio-funding"> <div class="portfolio-funding">
{% call StickyCTA(text="Funding") %} {% call StickyCTA(text="Funding") %}
<div class='portfolio-funding__header row'> <div class='portfolio-funding__header row'>
<a href="{{ url_for("task_orders.edit", portfolio_id=portfolio.id) }}" class="usa-button">Start a new task order</a> <a href="{{ url_for("task_orders.edit", portfolio_id=portfolio.id) }}" class="usa-button">Add a new task order</a>
</div> </div>
{% endcall %} {% endcall %}
{% for task_order in pending_task_orders %}
<div class='subheading'>
Pending
</div>
<div class='panel pending-task-order row'>
<span class='label label--warning'>Pending</span>
<div class="pending-task-order__started col">
<dt>Started</dt>
<dd>
<local-datetime
timestamp="{{ task_order.time_created }}"
format="M/D/YYYY">
</local-datetime>
</dd>
</div>
<div class="pending-task-order__value col">
<dt>Value</dt>
<dd>{{ task_order.budget | dollars }}</dd>
</div>
{{ ViewLink(task_order) }}
</div>
{% endfor %}
{% if not active_task_orders and not pending_task_orders %} {% if not active_task_orders and not pending_task_orders %}
{{ EmptyState( {{ EmptyState(
'This portfolio doesnt have any active or pending task orders.', 'This portfolio doesnt have any active or pending task orders.',
@ -130,22 +80,16 @@
) }} ) }}
{% endif %} {% endif %}
{% if pending_task_orders %}
{{ TaskOrderList(pending_task_orders, label='warning') }}
{% endif %}
{% if active_task_orders %} {% if active_task_orders %}
<div class='subheading'>Active</div> {{ TaskOrderList(active_task_orders, label='success') }}
{% call TaskOrderList(active_task_orders, label='success', funded=funded) %}
<tr class='total-balance'>
<td colspan='4'>
<span class='label label--success'>Total Active Balance</span>
<span>{{ total_balance | dollars }}</span>
</td>
<td>&nbsp;</td>
</tr>
{% endcall %}
{% endif %} {% endif %}
{% if expired_task_orders %} {% if expired_task_orders %}
<div class='subheading'>Expired</div> {{ TaskOrderList(expired_task_orders, label='error') }}
{{ TaskOrderList(expired_task_orders, label='expired', expired=True) }}
{% endif %} {% endif %}
</div> </div>

View File

@ -29,22 +29,6 @@ def user():
class TestPortfolioFunding: class TestPortfolioFunding:
def test_portfolio_with_no_task_orders(self, app, user_session, portfolio):
user_session(portfolio.owner)
with captured_templates(app) as templates:
response = app.test_client().get(
url_for("task_orders.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"] == []
@pytest.mark.skip(reason="Update later when CLINs are implemented") @pytest.mark.skip(reason="Update later when CLINs are implemented")
def test_funded_portfolio(self, app, user_session, portfolio): def test_funded_portfolio(self, app, user_session, portfolio):
user_session(portfolio.owner) user_session(portfolio.owner)