Merge pull request #534 from dod-ccpo/view-task-order
View task order page
This commit is contained in:
commit
085ec0589a
@ -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 ###
|
@ -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,
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
|
@ -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';
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
105
styles/sections/_task_order.scss
Normal file
105
styles/sections/_task_order.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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 %}
|
||||||
|
20
tests/models/test_legacy_task_order.py
Normal file
20
tests/models/test_legacy_task_order.py
Normal 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
|
@ -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
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user