Merge pull request #534 from dod-ccpo/view-task-order

View task order page
This commit is contained in:
patricksmithdds 2019-01-11 14:10:25 -05:00 committed by GitHub
commit 085ec0589a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 340 additions and 20 deletions

View File

@ -0,0 +1,31 @@
"""Add status enum to task_order
Revision ID: 3d346b5c8f19
Revises: 71cbe76c3b87
Create Date: 2019-01-10 13:55:58.934890
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '3d346b5c8f19'
down_revision = '71cbe76c3b87'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('task_orders', sa.Column('status', sa.Enum('PENDING', name='status', native_enum=False), nullable=True))
# ### end Alembic commands ###
conn = op.get_bind()
conn.execute("UPDATE task_orders set status = 'PENDING'")
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('task_orders', 'status')
# ### end Alembic commands ###

View File

@ -1,10 +1,16 @@
from sqlalchemy import Column, Numeric, String, ForeignKey, Date from enum import Enum
from sqlalchemy import Column, Enum as SQLAEnum, Numeric, String, ForeignKey, Date
from sqlalchemy.types import ARRAY from sqlalchemy.types import ARRAY
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from atst.models import Base, types, mixins from atst.models import Base, types, mixins
class Status(Enum):
PENDING = "Pending"
class TaskOrder(Base, mixins.TimestampsMixin): class TaskOrder(Base, mixins.TimestampsMixin):
__tablename__ = "task_orders" __tablename__ = "task_orders"
@ -27,6 +33,8 @@ class TaskOrder(Base, mixins.TimestampsMixin):
so_id = Column(ForeignKey("users.id")) so_id = Column(ForeignKey("users.id"))
security_officer = relationship("User", foreign_keys="TaskOrder.so_id") security_officer = relationship("User", foreign_keys="TaskOrder.so_id")
status = Column(SQLAEnum(Status, native_enum=False))
scope = Column(String) # Cloud Project Scope scope = Column(String) # Cloud Project Scope
defense_component = Column(String) # Department of Defense Component defense_component = Column(String) # Department of Defense Component
app_migration = Column(String) # App Migration app_migration = Column(String) # App Migration
@ -57,6 +65,11 @@ class TaskOrder(Base, mixins.TimestampsMixin):
number = Column(String, unique=True) # Task Order Number number = Column(String, unique=True) # Task Order Number
loa = Column(ARRAY(String)) # Line of Accounting (LOA) loa = Column(ARRAY(String)) # Line of Accounting (LOA)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if "status" not in kwargs:
self.status = Status.PENDING
@property @property
def budget(self): def budget(self):
return sum( return sum(
@ -67,6 +80,10 @@ class TaskOrder(Base, mixins.TimestampsMixin):
def portfolio_name(self): def portfolio_name(self):
return self.workspace.name return self.workspace.name
@property
def is_pending(self):
return self.status == Status.PENDING
def to_dictionary(self): def to_dictionary(self):
return { return {
"portfolio_name": self.portfolio_name, "portfolio_name": self.portfolio_name,

View File

@ -14,7 +14,7 @@ def workspace_task_orders(workspace_id):
@workspaces_bp.route("/workspaces/<workspace_id>/task_order/<task_order_id>") @workspaces_bp.route("/workspaces/<workspace_id>/task_order/<task_order_id>")
def view_task_order(workspace_id, task_order_id): def view_task_order(workspace_id, task_order_id):
workspace = Workspaces.get(g.current_user, workspace_id) workspace = Workspaces.get(g.current_user, workspace_id)
task_order = TaskOrders.get(task_order_id) task_order = TaskOrders.get(g.current_user, task_order_id)
return render_template( return render_template(
"workspaces/task_orders/show.html", workspace=workspace, task_order=task_order "workspaces/task_orders/show.html", workspace=workspace, task_order=task_order
) )

View File

@ -46,3 +46,4 @@
@import 'sections/project_edit'; @import 'sections/project_edit';
@import 'sections/member_edit'; @import 'sections/member_edit';
@import 'sections/reports'; @import 'sections/reports';
@import 'sections/task_order';

View File

@ -63,4 +63,9 @@
&.icon-link--default { &.icon-link--default {
@include icon-link-color($color-black-light, $color-gray-lightest); @include icon-link-color($color-black-light, $color-gray-lightest);
} }
&.icon-link--disabled {
opacity: 0.3;
pointer-events: none;
}
} }

View File

@ -0,0 +1,105 @@
.task-order-summary {
@include media($medium-screen) {
@include grid-row;
flex-wrap: wrap;
}
.panel {
width: 100%;
@include ie-only {
max-width: 100%;
}
}
.task-order-heading {
align-items: center;
justify-content: space-between;
.task-order-heading__name {
align-items: center;
label {
padding: $gap;
}
}
.task-order-heading__value {
margin: 0 ($gap * 2);
text-align: right;
dt {
font-weight: bold;
}
dd {
}
}
}
.task-order-details {
flex-grow: 1;
}
.task-order-next-steps {
flex-grow: 1;
padding-right: $gap;
.task-order-next-steps__step {
.task-order-next-steps__icon {
padding: $gap $gap 0 0;
justify-content: center;
.complete {
@include icon-color($color-green);
}
.incomplete {
@include icon-color($color-gray-light);
}
}
.task-order-next-steps__heading {
h4 {
margin: $gap $gap 0 0;
}
}
.task-order-next-steps__description {
font-style: italic;
}
}
}
.task-order-sidebar {
min-width: 35rem;
hr {
margin: 2rem 0;
}
}
.task-order-document-link {
.task-order-document-link__icon {
padding-top: 0.5rem;
}
.task-order-document-description {
padding-left: 3rem;
font-style: italic;
}
}
.task-order-invitation-status {
.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;
}
}
}

View File

@ -1,7 +1,159 @@
{% extends "workspaces/base.html" %} {% extends "workspaces/base.html" %}
{% from "components/icon.html" import Icon %}
{% block workspace_content %} {% block workspace_content %}
You're looking at TO {{ task_order.id }} {% macro Step(title="", description="", link_text=None, link_url=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>
<div class="task-order-next-steps__heading row">
<h4>{{ title }}</h4>
{% if link_url %}
<a href="{{ link_url }}" class="icon-link">
{{ Icon("edit") }}
<span>{{ link_text }}</span>
</a>
{% endif %}
</div>
<div class="task-order-next-steps__description">
{{ description }}
</div>
</div>
</div>
{% endmacro %}
{% macro DocumentLink(title="", link_url="", description="") %}
{% set disabled = not link_url %}
<div class="task-order-document-link">
<div class="row">
<a href="{{ link_url }}" class="icon-link {{ 'icon-link--disabled' if disabled }}" aria-disabled="{{ 'true' if disabled else 'false' }}">
<div class="task-order-document-link__icon col">
<span>{{ Icon("download") }}</span>
</div>
<div class="task-order-document-title col">
{{ title }}
</div>
</a>
</div>
{% if description %}
<div class="task-order-document-description">
{{ description }}
</div>
{% endif %}
</div>
{% endmacro %}
{% macro InvitationStatus(title, officer) %}
{% 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>
</div>
<div class="task-order-invitation-status__title col {{ class }}">
{{ title }}
</div>
</div>
{% endmacro %}
<div class="task-order-summary">
<div class="panel task-order-heading row">
<div class="panel__content task-order-heading__name row">
<h2>New Task Order</h2>
<span class="label label--{{ 'warning' if task_order.is_pending }}">{{ task_order.status.value }}</span>
</div>
<div class="task_order-heading__details row">
<div class="task-order-heading__value col">
<dt>Started</dt>
<dd>
<local-datetime
timestamp="{{ task_order.start_date }}"
format="M/D/YYYY">
</local-datetime>
</dd>
</div>
<div class="task-order-heading__value col">
<dt>Task Order Value</dt>
<dd>{{ task_order.budget | dollars }}</dd>
</div>
</div>
</div>
<div class="task-order-details row">
<div class="task-order-next-steps">
<div class="panel">
<h3 class="panel__content">What's next?</h3>
{{ Step(
title="Submit draft Task Order",
description="Complete initial task order request form.",
link_text="edit",
link_url=url_for("task_orders.new", screen=1, task_order_id=task_order.id),
complete=True) }}
{{ 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.",
complete=False) }}
{{ 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.",
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">
<div class="task-order-documents panel">
<div class="panel__content">
<h3>Download documents</h3>
{% set description -%}
last updated on <local-datetime
timestamp="{{ task_order.time_updated }}"
format="M/D/YYYY">
</local-datetime>
{%- endset %}
{{ DocumentLink(
title="Task Order Draft",
link_url=url_for('task_orders.download_summary', task_order_id=task_order.id),
description=description) }}
</div>
<hr />
<div class="panel__content">
{{ DocumentLink(
title="Cloud Services Estimate",
link_url="#") }}
{{ DocumentLink(
title="Market Research",
link_url="#") }}
{{ DocumentLink(
title="DD 254",
link_url="") }}
</div>
</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('task_orders.new', screen=3, task_order_id=task_order.id) }}" class="icon-link">
{{ Icon("edit") }}
<span>manage invitations</span>
</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,20 @@
from tests.factories import LegacyTaskOrderFactory
from tests.assert_util import dict_contains
def test_as_dictionary():
data = LegacyTaskOrderFactory.dictionary()
real_task_order = LegacyTaskOrderFactory.create(**data)
assert dict_contains(real_task_order.to_dictionary(), data)
def test_budget():
legacy_task_order = LegacyTaskOrderFactory.create(
clin_0001=500,
clin_0003=200,
clin_1001=None,
clin_1003=None,
clin_2001=None,
clin_2003=None,
)
assert legacy_task_order.budget == 700

View File

@ -1,20 +1,9 @@
from tests.factories import LegacyTaskOrderFactory from atst.models.task_order import TaskOrder, Status
from tests.assert_util import dict_contains
def test_as_dictionary(): def test_default_status():
data = LegacyTaskOrderFactory.dictionary() to = TaskOrder()
real_task_order = LegacyTaskOrderFactory.create(**data) assert to.status == Status.PENDING
assert dict_contains(real_task_order.to_dictionary(), data)
with_args = TaskOrder(number="42")
def test_budget(): assert to.status == Status.PENDING
legacy_task_order = LegacyTaskOrderFactory.create(
clin_0001=500,
clin_0003=200,
clin_1001=None,
clin_1003=None,
clin_2001=None,
clin_2003=None,
)
assert legacy_task_order.budget == 700