diff --git a/atst/filters.py b/atst/filters.py index 2b54572e..011d930b 100644 --- a/atst/filters.py +++ b/atst/filters.py @@ -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:]) diff --git a/atst/models/task_order.py b/atst/models/task_order.py index 4e338735..0e58ed60 100644 --- a/atst/models/task_order.py +++ b/atst/models/task_order.py @@ -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): diff --git a/atst/routes/portfolios/task_orders.py b/atst/routes/portfolios/task_orders.py index 44edd888..15dbdd03 100644 --- a/atst/routes/portfolios/task_orders.py +++ b/atst/routes/portfolios/task_orders.py @@ -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, diff --git a/atst/routes/task_orders/index.py b/atst/routes/task_orders/index.py index 6abf34fd..86f25f91 100644 --- a/atst/routes/task_orders/index.py +++ b/atst/routes/task_orders/index.py @@ -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/") 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/") +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") diff --git a/styles/sections/_task_order.scss b/styles/sections/_task_order.scss index 47eadcfb..25f7e780 100644 --- a/styles/sections/_task_order.scss +++ b/styles/sections/_task_order.scss @@ -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; } } } diff --git a/templates/portfolios/task_orders/show.html b/templates/portfolios/task_orders/show.html index fb7d98eb..1acbcf30 100644 --- a/templates/portfolios/task_orders/show.html +++ b/templates/portfolios/task_orders/show.html @@ -6,19 +6,42 @@ {% block portfolio_content %} -{% macro Step(title="", description="", link_text=None, complete=True) %} -
-
- {{ Icon("ok", classes="complete" if complete else "incomplete") }} -
-
-
-

{{ title }}

+{% 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) %} +
+
+
+ {% if complete %} + Completed + {% else %} + Not Started + {% endif %}
-
- {{ description }} +
+
+ {{ description }} +
+
+
+ {% if not task_order.is_active and button_text and button_url %} + + {{ button_text }} + + {% endif %}
+ {% if caller %} {{ caller() }} {% endif %} @@ -46,14 +69,27 @@
{% endmacro %} -{% macro InvitationStatus(title, officer) %} +{% macro InvitationStatus(title, officer, officer_info) %} {% set class = "invited" if officer else "uninvited" %}
- {{ Icon("ok" if officer else "alert", classes=class) }} + {{ Icon("avatar" if officer else "alert", classes=class) }}
-
- {{ title }} +
+
+ {{ title }} +
+
+ {% if officer_info %} +
+
{{ officer_info.first_name }} {{ officer_info.last_name }}
+
{{ officer_info.email }}
+
{{ officer_info.phone_number | usPhone }}
+
+ {% else %} + Not specified + {% endif %} +
{% endmacro %} @@ -87,42 +123,42 @@
-

What's next?

+

{{ "task_orders.view.whats_next" | translate }}

{% 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) %} -
- {% 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 %} - - Edit - -
{% 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) %} +
+
+
+ {{ "task_orders.view.steps.record_description" | translate | safe }} +
+
+
+ {% 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) }} -

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

@@ -135,13 +171,23 @@ format="M/D/YYYY"> {%- 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 %}

+ {{ 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 @@
-

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 - + + {{ 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')) }}
diff --git a/tests/models/test_task_order.py b/tests/models/test_task_order.py index 908daec2..fdb7e327 100644 --- a/tests/models/test_task_order.py +++ b/tests/models/test_task_order.py @@ -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): diff --git a/tests/test_filters.py b/tests/test_filters.py index 5c091faa..f35bb659 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -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 "Primary Point of contact ({contact}) completes initial Task Order form.' + security: 'IA Security Officer ({security_officer}) completes a Security Requirements Document. Send a reminder' + record: 'Contracting Officer ({contracting_officer}) or Contracting Officer Representative ({contracting_officer_representative}) records Task Order information. Send a reminder' + 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: 'Contracting Officer ({contracting_officer}) verifies funding to unlock cloud services.' new: app_info: section_title: "What You're Making"