diff --git a/alembic/versions/3d346b5c8f19_add_status_enum_to_task_order.py b/alembic/versions/3d346b5c8f19_add_status_enum_to_task_order.py new file mode 100644 index 00000000..61ffe84b --- /dev/null +++ b/alembic/versions/3d346b5c8f19_add_status_enum_to_task_order.py @@ -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 ### diff --git a/atst/models/task_order.py b/atst/models/task_order.py index 27b6cc06..2a1c5daa 100644 --- a/atst/models/task_order.py +++ b/atst/models/task_order.py @@ -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.orm import relationship from atst.models import Base, types, mixins +class Status(Enum): + PENDING = "Pending" + + class TaskOrder(Base, mixins.TimestampsMixin): __tablename__ = "task_orders" @@ -27,6 +33,8 @@ class TaskOrder(Base, mixins.TimestampsMixin): so_id = Column(ForeignKey("users.id")) security_officer = relationship("User", foreign_keys="TaskOrder.so_id") + status = Column(SQLAEnum(Status, native_enum=False)) + scope = Column(String) # Cloud Project Scope defense_component = Column(String) # Department of Defense Component app_migration = Column(String) # App Migration @@ -57,6 +65,11 @@ class TaskOrder(Base, mixins.TimestampsMixin): number = Column(String, unique=True) # Task Order Number 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 def budget(self): return sum( @@ -67,6 +80,10 @@ class TaskOrder(Base, mixins.TimestampsMixin): def portfolio_name(self): return self.workspace.name + @property + def is_pending(self): + return self.status == Status.PENDING + def to_dictionary(self): return { "portfolio_name": self.portfolio_name, diff --git a/atst/routes/workspaces/task_orders.py b/atst/routes/workspaces/task_orders.py index 7fdcbc9a..d99d6c3b 100644 --- a/atst/routes/workspaces/task_orders.py +++ b/atst/routes/workspaces/task_orders.py @@ -14,7 +14,7 @@ def workspace_task_orders(workspace_id): @workspaces_bp.route("/workspaces//task_order/") def view_task_order(workspace_id, task_order_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( "workspaces/task_orders/show.html", workspace=workspace, task_order=task_order ) diff --git a/styles/atat.scss b/styles/atat.scss index c128610e..37389c5e 100644 --- a/styles/atat.scss +++ b/styles/atat.scss @@ -46,3 +46,4 @@ @import 'sections/project_edit'; @import 'sections/member_edit'; @import 'sections/reports'; +@import 'sections/task_order'; diff --git a/styles/elements/_icon_link.scss b/styles/elements/_icon_link.scss index c4543b38..88e340ff 100644 --- a/styles/elements/_icon_link.scss +++ b/styles/elements/_icon_link.scss @@ -63,4 +63,9 @@ &.icon-link--default { @include icon-link-color($color-black-light, $color-gray-lightest); } + + &.icon-link--disabled { + opacity: 0.3; + pointer-events: none; + } } diff --git a/styles/sections/_task_order.scss b/styles/sections/_task_order.scss new file mode 100644 index 00000000..3845edac --- /dev/null +++ b/styles/sections/_task_order.scss @@ -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; + } + } + +} diff --git a/templates/workspaces/task_orders/show.html b/templates/workspaces/task_orders/show.html index d65f9ec0..1ea2d3a7 100644 --- a/templates/workspaces/task_orders/show.html +++ b/templates/workspaces/task_orders/show.html @@ -1,7 +1,159 @@ {% extends "workspaces/base.html" %} +{% from "components/icon.html" import Icon %} + {% block workspace_content %} -You're looking at TO {{ task_order.id }} +{% macro Step(title="", description="", link_text=None, link_url=None, complete=True) %} +
+
+ {{ Icon("ok", classes="complete" if complete else "incomplete") }} +
+
+
+

{{ title }}

+ {% if link_url %} + + {{ Icon("edit") }} + {{ link_text }} + + {% endif %} +
+
+ {{ description }} +
+
+
+{% endmacro %} + +{% macro DocumentLink(title="", link_url="", description="") %} + {% set disabled = not link_url %} + +{% endmacro %} + +{% macro InvitationStatus(title, officer) %} + {% set class = "invited" if officer else "uninvited" %} +
+
+ {{ Icon("ok" if officer else "alert", classes=class) }} +
+
+ {{ title }} +
+
+{% endmacro %} + +
+
+
+

New Task Order

+ {{ task_order.status.value }} +
+
+
+
Started
+
+ + +
+
+
+
Task Order Value
+
{{ task_order.budget | dollars }}
+
+
+
+ +
+
+
+

What's next?

+ {{ 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) }} +

Once your required information is submitted in this system, you're funded and ready to start using JEDI cloud services!

+
+
+
+
+
+

Download documents

+ {% set description -%} + last updated on + + {%- endset %} + {{ DocumentLink( + title="Task Order Draft", + link_url=url_for('task_orders.download_summary', task_order_id=task_order.id), + description=description) }} +
+
+
+ {{ DocumentLink( + title="Cloud Services Estimate", + link_url="#") }} + {{ DocumentLink( + title="Market Research", + link_url="#") }} + {{ DocumentLink( + title="DD 254", + link_url="") }} +
+
+
+
+

Invitations

+ {{ 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) }} + + + {{ Icon("edit") }} + manage invitations + +
+
+
+
+
{% endblock %} diff --git a/tests/models/test_legacy_task_order.py b/tests/models/test_legacy_task_order.py new file mode 100644 index 00000000..80e02b05 --- /dev/null +++ b/tests/models/test_legacy_task_order.py @@ -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 diff --git a/tests/models/test_task_order.py b/tests/models/test_task_order.py index 80e02b05..afd9611d 100644 --- a/tests/models/test_task_order.py +++ b/tests/models/test_task_order.py @@ -1,20 +1,9 @@ -from tests.factories import LegacyTaskOrderFactory -from tests.assert_util import dict_contains +from atst.models.task_order import TaskOrder, Status -def test_as_dictionary(): - data = LegacyTaskOrderFactory.dictionary() - real_task_order = LegacyTaskOrderFactory.create(**data) - assert dict_contains(real_task_order.to_dictionary(), data) +def test_default_status(): + to = TaskOrder() + assert to.status == Status.PENDING - -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 + with_args = TaskOrder(number="42") + assert to.status == Status.PENDING