Merge pull request #640 from dod-ccpo/update-task-order-show
Update view task order page
This commit is contained in:
commit
baa0afe106
@ -30,6 +30,8 @@ def justCents(value):
|
||||
|
||||
|
||||
def usPhone(number):
|
||||
if not number:
|
||||
return ""
|
||||
phone = re.sub(r"\D", "", number)
|
||||
return "+1 ({}) {} - {}".format(phone[0:3], phone[3:6], phone[6:])
|
||||
|
||||
|
@ -21,6 +21,7 @@ from atst.models import Attachment, Base, types, mixins
|
||||
|
||||
|
||||
class Status(Enum):
|
||||
STARTED = "Started"
|
||||
PENDING = "Pending"
|
||||
ACTIVE = "Active"
|
||||
EXPIRED = "Expired"
|
||||
@ -142,7 +143,7 @@ class TaskOrder(Base, mixins.TimestampsMixin):
|
||||
return Status.EXPIRED
|
||||
return Status.ACTIVE
|
||||
else:
|
||||
return Status.PENDING
|
||||
return Status.STARTED
|
||||
|
||||
@property
|
||||
def display_status(self):
|
||||
|
@ -45,7 +45,10 @@ def portfolio_funding(portfolio_id):
|
||||
return render_template(
|
||||
"portfolios/task_orders/index.html",
|
||||
portfolio=portfolio,
|
||||
pending_task_orders=task_orders_by_status.get(TaskOrderStatus.PENDING, []),
|
||||
pending_task_orders=(
|
||||
task_orders_by_status.get(TaskOrderStatus.STARTED, [])
|
||||
+ task_orders_by_status.get(TaskOrderStatus.PENDING, [])
|
||||
),
|
||||
active_task_orders=active_task_orders,
|
||||
expired_task_orders=task_orders_by_status.get(TaskOrderStatus.EXPIRED, []),
|
||||
total_balance=total_balance,
|
||||
|
@ -20,20 +20,29 @@ def download_summary(task_order_id):
|
||||
)
|
||||
|
||||
|
||||
def send_file(attachment):
|
||||
generator = app.csp.files.download(attachment.object_name)
|
||||
return Response(
|
||||
generator,
|
||||
headers={
|
||||
"Content-Disposition": "attachment; filename={}".format(attachment.filename)
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@task_orders_bp.route("/task_orders/csp_estimate/<task_order_id>")
|
||||
def download_csp_estimate(task_order_id):
|
||||
task_order = TaskOrders.get(g.current_user, task_order_id)
|
||||
if task_order.csp_estimate:
|
||||
estimate = task_order.csp_estimate
|
||||
generator = app.csp.files.download(estimate.object_name)
|
||||
return Response(
|
||||
generator,
|
||||
headers={
|
||||
"Content-Disposition": "attachment; filename={}".format(
|
||||
estimate.filename
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
return send_file(task_order.csp_estimate)
|
||||
else:
|
||||
raise NotFoundError("task_order CSP estimate")
|
||||
|
||||
|
||||
@task_orders_bp.route("/task_orders/pdf/<task_order_id>")
|
||||
def download_task_order_pdf(task_order_id):
|
||||
task_order = TaskOrders.get(g.current_user, task_order_id)
|
||||
if task_order.pdf:
|
||||
return send_file(task_order.pdf)
|
||||
else:
|
||||
raise NotFoundError("task_order pdf")
|
||||
|
@ -63,6 +63,9 @@
|
||||
}
|
||||
|
||||
.task-order-summary {
|
||||
.panel {
|
||||
@include shadow-panel;
|
||||
}
|
||||
|
||||
.alert .alert__actions {
|
||||
margin-top: 2 * $gap;
|
||||
@ -72,7 +75,7 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.label--pending {
|
||||
.label--pending, .label--started {
|
||||
background-color: $color-gold;
|
||||
}
|
||||
|
||||
@ -117,6 +120,11 @@
|
||||
|
||||
.task-order-next-steps {
|
||||
flex-grow: 1;
|
||||
|
||||
.panel {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
@include media($xlarge-screen) {
|
||||
padding-right: $gap;
|
||||
}
|
||||
@ -140,8 +148,17 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.alert {
|
||||
margin-top: 3 * $gap;
|
||||
margin-bottom: 0;
|
||||
padding: 2 * $gap;
|
||||
|
||||
.alert__message {
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
.task-order-next-steps__icon {
|
||||
width: 8%;
|
||||
padding: $gap $gap 0 0;
|
||||
justify-content: center;
|
||||
.complete {
|
||||
@ -152,34 +169,29 @@
|
||||
}
|
||||
}
|
||||
|
||||
.task-order-next-steps__text {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
.task-order-next-steps__action {
|
||||
min-width: 10 * $gap;
|
||||
padding: $gap 0 0 $gap;
|
||||
width: 32%;
|
||||
a.usa-button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.task-order-next-steps__heading {
|
||||
|
||||
h4 {
|
||||
@include ie-only {
|
||||
width: 100%;
|
||||
.task-order-next-steps__text {
|
||||
display: flex;
|
||||
.task-order-next-steps__heading {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
margin: $gap $gap 0 0;
|
||||
}
|
||||
}
|
||||
.task-order-next-steps__description {
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.task-order-sidebar {
|
||||
@include media($xlarge-screen) {
|
||||
padding-left: 3 * $gap;
|
||||
}
|
||||
min-width: 35rem;
|
||||
|
||||
hr {
|
||||
@ -198,18 +210,33 @@
|
||||
}
|
||||
}
|
||||
|
||||
.task-order-invitation-status {
|
||||
.invited {
|
||||
color: $color-green;
|
||||
@include icon-color($color-green);
|
||||
}
|
||||
.uninvited {
|
||||
color: $color-red;
|
||||
@include icon-color($color-red);
|
||||
.task-order-invitations {
|
||||
.task-order-invitations__heading {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.task-order-invitation-status__icon {
|
||||
padding: 0 0.5rem;
|
||||
.task-order-invitation-status {
|
||||
margin-bottom: 3 * $gap;
|
||||
.task-order-invitation-status__title {
|
||||
font-weight: $font-bold;
|
||||
}
|
||||
|
||||
.invited {
|
||||
color: $color-green;
|
||||
@include icon-color($color-green);
|
||||
}
|
||||
.uninvited {
|
||||
color: $color-red;
|
||||
@include icon-color($color-red);
|
||||
}
|
||||
|
||||
.task-order-invitation-status__icon {
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.task-order-invitation-details {
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,19 +6,42 @@
|
||||
|
||||
{% block portfolio_content %}
|
||||
|
||||
{% macro Step(title="", description="", link_text=None, complete=True) %}
|
||||
<div class="task-order-next-steps__step panel__content row">
|
||||
<div class="task-order-next-steps__icon col">
|
||||
<span>{{ Icon("ok", classes="complete" if complete else "incomplete") }}</span>
|
||||
</div>
|
||||
<div class="task-order-next-steps__text col">
|
||||
<div class="task-order-next-steps__heading row">
|
||||
<h4>{{ title }}</h4>
|
||||
{% macro officer_name(officer) -%}
|
||||
{%- if not officer -%}
|
||||
Not specified
|
||||
{%- elif officer == g.current_user -%}
|
||||
You
|
||||
{%- else -%}
|
||||
{{ officer.full_name }}
|
||||
{%- endif -%}
|
||||
{%- endmacro -%}
|
||||
|
||||
{% macro Step(description="", complete=True, button_text=None, button_url=None) %}
|
||||
<div class="task-order-next-steps__step panel__content">
|
||||
<div class="row">
|
||||
<div class="task-order-next-steps__icon col">
|
||||
{% if complete %}
|
||||
<span class="label label--success">Completed</span>
|
||||
{% else %}
|
||||
<span class="label">Not Started</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="task-order-next-steps__description">
|
||||
{{ description }}
|
||||
<div class="task-order-next-steps__text col col--grow">
|
||||
<div class="task-order-next-steps__heading row">
|
||||
<span>{{ description }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="task-order-next-steps__action col">
|
||||
{% if not task_order.is_active and button_text and button_url %}
|
||||
<a
|
||||
href="{{ button_url }}"
|
||||
class="usa-button usa-button-primary">
|
||||
{{ button_text }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if caller %}
|
||||
{{ caller() }}
|
||||
{% endif %}
|
||||
@ -46,14 +69,27 @@
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro InvitationStatus(title, officer) %}
|
||||
{% macro InvitationStatus(title, officer, officer_info) %}
|
||||
{% set class = "invited" if officer else "uninvited" %}
|
||||
<div class="task-order-invitation-status row">
|
||||
<div class="task-order-invitation-status__icon col">
|
||||
<span>{{ Icon("ok" if officer else "alert", classes=class) }}</span>
|
||||
<span>{{ Icon("avatar" if officer else "alert", classes=class) }}</span>
|
||||
</div>
|
||||
<div class="task-order-invitation-status__title col {{ class }}">
|
||||
{{ title }}
|
||||
<div class="col">
|
||||
<div class="task-order-invitation-status__title {{ class }}">
|
||||
{{ title }}
|
||||
</div>
|
||||
<div class="task-order-invitation-details">
|
||||
{% if officer_info %}
|
||||
<div class="col">
|
||||
<div>{{ officer_info.first_name }} {{ officer_info.last_name }}</div>
|
||||
<div>{{ officer_info.email }}</div>
|
||||
<div>{{ officer_info.phone_number | usPhone }}</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<span>Not specified</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
@ -87,42 +123,42 @@
|
||||
<div class="task-order-details">
|
||||
<div id="next-steps" class="task-order-next-steps">
|
||||
<div class="panel">
|
||||
<h3 class="task-order-next-steps__panel-head panel__content">What's next?</h3>
|
||||
<h3 class="task-order-next-steps__panel-head panel__content">{{ "task_orders.view.whats_next" | translate }}</h3>
|
||||
{% call Step(
|
||||
title="Submit draft Task Order",
|
||||
description="Complete initial task order request form.",
|
||||
link_text="edit",
|
||||
description="task_orders.view.steps.draft" | translate({
|
||||
"contact": officer_name(task_order.creator)
|
||||
})| safe,
|
||||
button_url=url_for("task_orders.new", screen=1, task_order_id=task_order.id),
|
||||
button_text='Edit',
|
||||
complete=all_sections_complete) %}
|
||||
<div class="task-order-next-steps__action col">
|
||||
{% if user == task_order.contracting_officer %}
|
||||
{% set url=url_for("portfolios.ko_review", portfolio_id=portfolio.id, task_order_id=task_order.id) %}
|
||||
{% else %}
|
||||
{% set url = url_for("task_orders.new", screen=1, task_order_id=task_order.id) %}
|
||||
{% endif %}
|
||||
<a
|
||||
href="{{ url }}"
|
||||
class="usa-button usa-button-primary">
|
||||
Edit
|
||||
</a>
|
||||
</div>
|
||||
{% endcall %}
|
||||
{{ Step(
|
||||
title="Complete a Security Requirements Document",
|
||||
description="The IA Security Official you specified received an email invitation to complete and sign a DD-254: Security Requirements document that's been customized for the JEDI program here.",
|
||||
description="task_orders.view.steps.security" | translate({
|
||||
"security_officer": officer_name(task_order.security_officer)
|
||||
}) | safe,
|
||||
complete=False) }}
|
||||
{% call Step(
|
||||
description="task_orders.view.steps.record" | translate({
|
||||
"contracting_officer": officer_name(task_order.contracting_officer),
|
||||
"contracting_officer_representative": officer_name(task_order.contracting_officer_representative)
|
||||
}) | safe,
|
||||
complete=False) %}
|
||||
<div class='alert alert--warning'>
|
||||
<div class='alert__content'>
|
||||
<div class='alert__message'>
|
||||
{{ "task_orders.view.steps.record_description" | translate | safe }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endcall %}
|
||||
{% set is_ko = user == task_order.contracting_officer %}
|
||||
{{ Step(
|
||||
title="Prepare the Task Order Documents for your organization's contracting system",
|
||||
description="You'll file your task order in your organization's contracting system. Change the formatting based on your office prefers.",
|
||||
description="task_orders.view.steps.sign" | translate({
|
||||
"contracting_officer": officer_name(task_order.contracting_officer)
|
||||
}) | safe,
|
||||
button_url=is_ko and url_for("portfolios.ko_review", portfolio_id=portfolio.id, task_order_id=task_order.id),
|
||||
button_text=is_ko and 'Sign',
|
||||
complete=False) }}
|
||||
{{ Step(
|
||||
title="Get a funding document",
|
||||
description="User your organization's normal process to get a funding document, typically from your financial manager. Your Contracting Officer's Representative (COR) or Contracting Officer (KO) can help with this, too.",
|
||||
complete=False) }}
|
||||
{{ Step(
|
||||
title="Have your KO submit your final task order",
|
||||
description="Your KO will submit the final task order into your organization's contracting system and receive an official task order number. Your KO should enter your task order number in this system, along with a copy of the submitted task order.",
|
||||
complete=False) }}
|
||||
<h4 class="panel__content">Once your required information is submitted in this system, you're funded and ready to start using JEDI cloud services!</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div class="task-order-sidebar col">
|
||||
@ -135,13 +171,23 @@
|
||||
format="M/D/YYYY">
|
||||
</local-datetime>
|
||||
{%- endset %}
|
||||
{{ DocumentLink(
|
||||
title="Task Order Draft",
|
||||
link_url=all_sections_complete and url_for('task_orders.download_summary', task_order_id=task_order.id),
|
||||
description=description) }}
|
||||
{% if task_order.pdf %}
|
||||
{{ DocumentLink(
|
||||
title="Task Order",
|
||||
link_url=url_for('task_orders.download_task_order_pdf', task_order_id=task_order.id),
|
||||
description=description) }}
|
||||
{% else %}
|
||||
{{ DocumentLink(
|
||||
title="Task Order Draft",
|
||||
link_url=all_sections_complete and url_for('task_orders.download_summary', task_order_id=task_order.id),
|
||||
description=description) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<hr />
|
||||
<div class="panel__content">
|
||||
{{ DocumentLink(
|
||||
title="Instruction Sheet",
|
||||
link_url="#") }}
|
||||
{{ DocumentLink(
|
||||
title="Cloud Services Estimate",
|
||||
link_url=task_order.csp_estimate and url_for("task_orders.download_csp_estimate", task_order_id=task_order.id) ) }}
|
||||
@ -155,15 +201,16 @@
|
||||
</div>
|
||||
<div class="task-order-invitations panel">
|
||||
<div class="panel__content">
|
||||
<h3>Invitations</h3>
|
||||
{{ InvitationStatus('Contracting Officer', task_order.contracting_officer) }}
|
||||
{{ InvitationStatus('Contracting Officer Representative', task_order.contracting_officer_representative) }}
|
||||
{{ InvitationStatus('IA Security Officer', officer=task_order.security_officer) }}
|
||||
|
||||
<a href="{{ url_for('portfolios.task_order_invitations', portfolio_id=portfolio.id, task_order_id=task_order.id) }}" class="icon-link">
|
||||
{{ Icon("edit") }}
|
||||
<span>manage invitations</span>
|
||||
</a>
|
||||
<div class="task-order-invitations__heading row">
|
||||
<h3>Invitations</h3>
|
||||
<a href="{{ url_for('portfolios.task_order_invitations', portfolio_id=portfolio.id, task_order_id=task_order.id) }}" class="icon-link">
|
||||
<span>manage</span>
|
||||
{{ Icon("edit") }}
|
||||
</a>
|
||||
</div>
|
||||
{{ InvitationStatus('Contracting Officer', task_order.contracting_officer, officer_info=task_order.officer_dictionary('contracting_officer')) }}
|
||||
{{ InvitationStatus('Contracting Officer Representative', task_order.contracting_officer_representative, officer_info=task_order.officer_dictionary('contracting_officer_representative')) }}
|
||||
{{ InvitationStatus('IA Security Officer', officer=task_order.security_officer, officer_info=task_order.officer_dictionary('security_officer')) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -9,11 +9,14 @@ from tests.mocks import PDF_FILENAME
|
||||
|
||||
|
||||
class TestTaskOrderStatus:
|
||||
def test_pending_status(self):
|
||||
def test_started_status(self):
|
||||
to = TaskOrder()
|
||||
assert to.status == Status.PENDING
|
||||
assert to.status == Status.STARTED
|
||||
|
||||
to = TaskOrder(number="42", start_date=random_future_date())
|
||||
def test_pending_status(self):
|
||||
to = TaskOrder(
|
||||
number="42", start_date=random_future_date(), end_date=random_future_date()
|
||||
)
|
||||
assert to.status == Status.PENDING
|
||||
|
||||
def test_active_status(self):
|
||||
|
@ -1,6 +1,6 @@
|
||||
import pytest
|
||||
|
||||
from atst.filters import dollars, renderAuditEvent
|
||||
from atst.filters import dollars, renderAuditEvent, usPhone
|
||||
from atst.models import AuditEvent
|
||||
|
||||
|
||||
@ -28,3 +28,9 @@ def test_render_audit_event_with_unknown_resource_type():
|
||||
event = AuditEvent(resource_type="boat")
|
||||
result = renderAuditEvent(event)
|
||||
assert "<article" in result
|
||||
|
||||
|
||||
def test_usPhone():
|
||||
assert usPhone("1234567890") == "+1 (123) 456 - 7890"
|
||||
assert usPhone(number=None) == ""
|
||||
assert usPhone(number="") == ""
|
||||
|
@ -412,6 +412,14 @@ task_orders:
|
||||
title: Sign Task Order
|
||||
unlimited_level_of_warrant_description: Unlimited Level of Warrant funds
|
||||
verify_warrant_level_paragraph: Verify your level of warrant and provide your digital signature to authorize this Task Order.
|
||||
view:
|
||||
whats_next: Here are the remaining tasks to get your Task Order approved.
|
||||
steps:
|
||||
draft: '<strong>Primary Point of contact ({contact})</strong> completes initial Task Order form.'
|
||||
security: '<strong>IA Security Officer ({security_officer})</strong> completes a Security Requirements Document. <a href="#">Send a reminder</a>'
|
||||
record: '<strong>Contracting Officer ({contracting_officer}) or Contracting Officer Representative ({contracting_officer_representative})</strong> records Task Order information. <a href="#">Send a reminder</a>'
|
||||
record_description: Obtain a funding document and file a Task Order in the appropriate system of record. Once this is complete, come back here and record the task order information.
|
||||
sign: '<strong>Contracting Officer ({contracting_officer})</strong> verifies funding to unlock cloud services.'
|
||||
new:
|
||||
app_info:
|
||||
section_title: "What You're Making"
|
||||
|
Loading…
x
Reference in New Issue
Block a user