diff --git a/.secrets.baseline b/.secrets.baseline index 385c0b04..05921baa 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -3,7 +3,7 @@ "files": "^.secrets.baseline$|^.*pgsslrootcert.yml$", "lines": null }, - "generated_at": "2019-12-05T17:54:05Z", + "generated_at": "2019-12-06T21:22:07Z", "plugins_used": [ { "base64_limit": 4.5, @@ -161,7 +161,7 @@ "hashed_secret": "e4f14805dfd1e6af030359090c535e149e6b4207", "is_secret": false, "is_verified": false, - "line_number": 31, + "line_number": 41, "type": "Hex High Entropy String" } ], diff --git a/atst/domain/task_orders.py b/atst/domain/task_orders.py index 0c67e1d4..24acdee7 100644 --- a/atst/domain/task_orders.py +++ b/atst/domain/task_orders.py @@ -64,10 +64,12 @@ class TaskOrders(BaseDomainClass): db.session.commit() @classmethod - def sort(cls, task_orders: [TaskOrder]) -> [TaskOrder]: - # Sorts a list of task orders on two keys: status (primary) and time_created (secondary) - by_time_created = sorted(task_orders, key=lambda to: to.time_created) - by_status = sorted(by_time_created, key=lambda to: SORT_ORDERING.get(to.status)) + def sort_by_status(cls, task_orders): + by_status = {status.value: [] for status in SORT_ORDERING} + + for task_order in task_orders: + by_status[task_order.display_status].append(task_order) + return by_status @classmethod diff --git a/atst/models/task_order.py b/atst/models/task_order.py index 85bf363a..64693831 100644 --- a/atst/models/task_order.py +++ b/atst/models/task_order.py @@ -1,4 +1,3 @@ -from datetime import timedelta from enum import Enum from sqlalchemy import Column, DateTime, ForeignKey, String @@ -20,12 +19,13 @@ class Status(Enum): UNSIGNED = "Not signed" -SORT_ORDERING = { - status: order - for (order, status) in enumerate( - [Status.DRAFT, Status.ACTIVE, Status.UPCOMING, Status.EXPIRED, Status.UNSIGNED] - ) -} +SORT_ORDERING = [ + Status.ACTIVE, + Status.DRAFT, + Status.UPCOMING, + Status.EXPIRED, + Status.UNSIGNED, +] class TaskOrder(Base, mixins.TimestampsMixin): @@ -131,12 +131,11 @@ class TaskOrder(Base, mixins.TimestampsMixin): @property def start_date(self): - return min((c.start_date for c in self.clins), default=self.time_created.date()) + return min((c.start_date for c in self.clins), default=None) @property def end_date(self): - default_end_date = self.start_date + timedelta(days=1) - return max((c.end_date for c in self.clins), default=default_end_date) + return max((c.end_date for c in self.clins), default=None) @property def days_to_expiration(self): @@ -170,6 +169,11 @@ class TaskOrder(Base, mixins.TimestampsMixin): # Faked for display purposes return 50 + @property + def invoiced_funds(self): + # TODO: implement this using reporting data from the CSP + return self.total_obligated_funds * 75 / 100 + @property def display_status(self): return self.status.value diff --git a/atst/routes/task_orders/index.py b/atst/routes/task_orders/index.py index a58ea8da..a3d72cf7 100644 --- a/atst/routes/task_orders/index.py +++ b/atst/routes/task_orders/index.py @@ -6,7 +6,6 @@ from atst.domain.portfolios import Portfolios from atst.domain.task_orders import TaskOrders from atst.forms.task_order import SignatureForm from atst.models import Permissions -from atst.models.task_order import Status as TaskOrderStatus @task_orders_bp.route("/task_orders//review") @@ -28,14 +27,6 @@ def review_task_order(task_order_id): @user_can(Permissions.VIEW_PORTFOLIO_FUNDING, message="view portfolio funding") def portfolio_funding(portfolio_id): portfolio = Portfolios.get(g.current_user, portfolio_id) - task_orders = TaskOrders.sort(portfolio.task_orders) - label_colors = { - TaskOrderStatus.DRAFT: "warning", - TaskOrderStatus.ACTIVE: "success", - TaskOrderStatus.UPCOMING: "info", - TaskOrderStatus.EXPIRED: "error", - TaskOrderStatus.UNSIGNED: "purple", - } - return render_template( - "task_orders/index.html", task_orders=task_orders, label_colors=label_colors - ) + task_orders = TaskOrders.sort_by_status(portfolio.task_orders) + # TODO: Get expended amount from the CSP + return render_template("task_orders/index.html", task_orders=task_orders) diff --git a/js/components/accordion.js b/js/components/accordion.js index d281a9e7..b96e1b80 100644 --- a/js/components/accordion.js +++ b/js/components/accordion.js @@ -11,4 +11,10 @@ export default { default: false, }, }, + + methods: { + collapse: function() { + this.isVisible = false + }, + }, } diff --git a/js/components/accordion_list.js b/js/components/accordion_list.js new file mode 100644 index 00000000..41e11d33 --- /dev/null +++ b/js/components/accordion_list.js @@ -0,0 +1,16 @@ +import Accordion from './accordion' + +export default { + name: 'accordion-list', + + components: { + Accordion, + }, + + methods: { + handleClick: function(e) { + e.preventDefault() + this.$children.forEach(el => el.collapse()) + }, + }, +} diff --git a/js/index.js b/js/index.js index a28c4868..fb5cdd6e 100644 --- a/js/index.js +++ b/js/index.js @@ -7,6 +7,8 @@ import Vue from 'vue/dist/vue' import VTooltip from 'v-tooltip' import stickybits from 'stickybits' +import Accordion from './components/accordion' +import AccordionList from './components/accordion_list' import dodlogin from './components/dodlogin' import optionsinput from './components/options_input' import multicheckboxinput from './components/multi_checkbox_input' @@ -29,7 +31,6 @@ import SemiCollapsibleText from './components/semi_collapsible_text' import ToForm from './components/forms/to_form' import ClinFields from './components/clin_fields' import PopDateRange from './components/pop_date_range' -import Accordion from './components/accordion' import ToggleMenu from './components/toggle_menu' Vue.config.productionTip = false @@ -42,6 +43,7 @@ const app = new Vue({ el: '#app-root', components: { Accordion, + AccordionList, dodlogin, toggler, optionsinput, diff --git a/js/mixins/toggle.js b/js/mixins/toggle.js index d891eb02..3e155dd3 100644 --- a/js/mixins/toggle.js +++ b/js/mixins/toggle.js @@ -17,6 +17,7 @@ export default { methods: { toggle: function(e) { e.preventDefault() + e.stopPropagation() this.isVisible = !this.isVisible }, }, diff --git a/styles/elements/_accordions.scss b/styles/elements/_accordions.scss index a6e61692..4f56e9bb 100644 --- a/styles/elements/_accordions.scss +++ b/styles/elements/_accordions.scss @@ -47,4 +47,12 @@ } } } + + &-list { + max-width: $max-panel-width; + + &__collapse { + cursor: pointer; + } + } } diff --git a/styles/sections/_task_order.scss b/styles/sections/_task_order.scss index 4307333a..228bf126 100644 --- a/styles/sections/_task_order.scss +++ b/styles/sections/_task_order.scss @@ -127,21 +127,6 @@ width: 100%; } - .label { - &--pending, - &--started { - background-color: $color-gold; - } - - &--active { - background-color: $color-green; - } - - &--expired { - background-color: $color-red; - } - } - .task-order-document-link { &__icon { padding-top: 0.5rem; diff --git a/templates/applications/index.html b/templates/applications/index.html index 81509be5..00a9b0e8 100644 --- a/templates/applications/index.html +++ b/templates/applications/index.html @@ -1,4 +1,5 @@ {% from "components/accordion.html" import Accordion %} +{% from "components/accordion_list.html" import AccordionList %} {% from "components/empty_state.html" import EmptyState %} {% from "components/sticky_cta.html" import StickyCTA %} {% from "components/icon.html" import Icon %} @@ -32,7 +33,7 @@ ) }} {% else %} -
+ {% call AccordionList() %} {% for application in portfolio.applications|sort(attribute='name') %} {% set section_name = "application-{}".format(application.id) %} {% set title = "Environments ({})".format(application.environments|length) %} @@ -76,7 +77,7 @@ {% endcall %}
{% endfor %} - + {% endcall %} {% endif %} diff --git a/templates/components/accordion_list.html b/templates/components/accordion_list.html new file mode 100644 index 00000000..216e51b1 --- /dev/null +++ b/templates/components/accordion_list.html @@ -0,0 +1,11 @@ +{% macro AccordionList() %} + +
+ + + {{ caller() }} +
+
+{% endmacro %} diff --git a/templates/task_orders/index.html b/templates/task_orders/index.html index 0c319403..dfabbd02 100644 --- a/templates/task_orders/index.html +++ b/templates/task_orders/index.html @@ -1,4 +1,5 @@ {% from "components/accordion.html" import Accordion %} +{% from "components/accordion_list.html" import AccordionList %} {% from "components/empty_state.html" import EmptyState %} {% from "components/icon.html" import Icon %} {% from "components/sticky_cta.html" import StickyCTA %} @@ -13,36 +14,44 @@ {% macro TaskOrderList(task_orders, status) %} - {% set status = "All Task Orders" %} -
+
{% call Accordion(title=status, id=status, heading_tag="h4") %} {% for task_order in task_orders %} + {% set to_number %} + {% if task_order.number != "" %} + Task Order #{{ task_order.number }} + {% else %} + New Task Order + {% endif %} + {% endset %}
-

Task Order #{{ task_order.number }} {{ Icon("caret_right", classes="icon--tiny icon--primary" ) }}

-
-
-
- Current Period of Performance -
-

- {{ task_order.start_date | formattedDate(formatter="%b %d, %Y") }} - - - {{ task_order.end_date | formattedDate(formatter="%b %d, %Y") }} -

+

{{ to_number }} {{ Icon("caret_right", classes="icon--tiny icon--primary" ) }}

+ {% if status != 'Expired' -%} +
+
+
+ Current Period of Performance +
+

+ {{ task_order.start_date | formattedDate(formatter="%b %d, %Y") }} + - + {{ task_order.end_date | formattedDate(formatter="%b %d, %Y") }} +

+
+
+
Total Value
+

{{ task_order.total_contract_amount | dollars }}

+
+
+
Total Obligated
+

{{ task_order.total_obligated_funds | dollars }}

+
+
+
Total Expended
+

{{ task_order.invoiced_funds | dollars }}

+
-
-
Total Value
-

{{ task_order.total_contract_amount | dollars }}

-
-
-
Total Obligated
-

{{ task_order.total_obligated_funds | dollars }}

-
-
-
Total Expended
-

$0

-
-
+ {%- endif %}
{% endfor %} {% endcall %} @@ -63,7 +72,11 @@
{% if task_orders %} - {{ TaskOrderList(task_orders) }} + {% call AccordionList() %} + {% for status, to_list in task_orders.items() %} + {{ TaskOrderList(to_list, status) }} + {% endfor %} + {% endcall %} {% else %} {{ EmptyState( header="task_orders.empty_state.header"|translate, diff --git a/tests/domain/test_task_orders.py b/tests/domain/test_task_orders.py index 8b1eb724..acc2d158 100644 --- a/tests/domain/test_task_orders.py +++ b/tests/domain/test_task_orders.py @@ -3,78 +3,11 @@ from datetime import date, timedelta from decimal import Decimal from atst.domain.task_orders import TaskOrders -from atst.models import Attachment, TaskOrder +from atst.models import Attachment +from atst.models.task_order import TaskOrder, SORT_ORDERING, Status from tests.factories import TaskOrderFactory, CLINFactory, PortfolioFactory -def test_task_order_sorting(): - """ - Task orders should be listed first by status, and then by time_created. - """ - - today = date.today() - yesterday = today - timedelta(days=1) - future = today + timedelta(days=100) - - task_orders = [ - # Draft - TaskOrderFactory.create(pdf=None), - TaskOrderFactory.create(pdf=None), - TaskOrderFactory.create(pdf=None), - # Active - TaskOrderFactory.create( - signed_at=yesterday, - clins=[CLINFactory.create(start_date=yesterday, end_date=future)], - ), - TaskOrderFactory.create( - signed_at=yesterday, - clins=[CLINFactory.create(start_date=yesterday, end_date=future)], - ), - TaskOrderFactory.create( - signed_at=yesterday, - clins=[CLINFactory.create(start_date=yesterday, end_date=future)], - ), - # Upcoming - TaskOrderFactory.create( - signed_at=yesterday, - clins=[CLINFactory.create(start_date=future, end_date=future)], - ), - TaskOrderFactory.create( - signed_at=yesterday, - clins=[CLINFactory.create(start_date=future, end_date=future)], - ), - TaskOrderFactory.create( - signed_at=yesterday, - clins=[CLINFactory.create(start_date=future, end_date=future)], - ), - # Expired - TaskOrderFactory.create( - signed_at=yesterday, - clins=[CLINFactory.create(start_date=yesterday, end_date=yesterday)], - ), - TaskOrderFactory.create( - signed_at=yesterday, - clins=[CLINFactory.create(start_date=yesterday, end_date=yesterday)], - ), - TaskOrderFactory.create( - signed_at=yesterday, - clins=[CLINFactory.create(start_date=yesterday, end_date=yesterday)], - ), - # Unsigned - TaskOrderFactory.create( - clins=[CLINFactory.create(start_date=today, end_date=today)] - ), - TaskOrderFactory.create( - clins=[CLINFactory.create(start_date=today, end_date=today)] - ), - TaskOrderFactory.create( - clins=[CLINFactory.create(start_date=today, end_date=today)] - ), - ] - - assert TaskOrders.sort(task_orders) == task_orders - - def test_create_adds_clins(): portfolio = PortfolioFactory.create() clins = [ @@ -177,3 +110,47 @@ def test_delete_task_order_with_clins(session): assert not session.query( session.query(TaskOrder).filter_by(id=task_order.id).exists() ).scalar() + + +def test_task_order_sort_by_status(): + today = date.today() + yesterday = today - timedelta(days=1) + future = today + timedelta(days=100) + + initial_to_list = [ + # Draft + TaskOrderFactory.create(pdf=None), + TaskOrderFactory.create(pdf=None), + TaskOrderFactory.create(pdf=None), + # Active + TaskOrderFactory.create( + signed_at=yesterday, + clins=[CLINFactory.create(start_date=yesterday, end_date=future)], + ), + # Upcoming + TaskOrderFactory.create( + signed_at=yesterday, + clins=[CLINFactory.create(start_date=future, end_date=future)], + ), + # Expired + TaskOrderFactory.create( + signed_at=yesterday, + clins=[CLINFactory.create(start_date=yesterday, end_date=yesterday)], + ), + TaskOrderFactory.create( + signed_at=yesterday, + clins=[CLINFactory.create(start_date=yesterday, end_date=yesterday)], + ), + # Unsigned + TaskOrderFactory.create( + clins=[CLINFactory.create(start_date=today, end_date=today)] + ), + ] + + sorted_by_status = TaskOrders.sort_by_status(initial_to_list) + assert len(sorted_by_status["Draft"]) == 3 + assert len(sorted_by_status["Active"]) == 1 + assert len(sorted_by_status["Upcoming"]) == 1 + assert len(sorted_by_status["Expired"]) == 2 + assert len(sorted_by_status["Not signed"]) == 1 + assert list(sorted_by_status.keys()) == [status.value for status in SORT_ORDERING] diff --git a/tests/routes/task_orders/test_index.py b/tests/routes/task_orders/test_index.py index 708b2bdc..48d8bd6b 100644 --- a/tests/routes/task_orders/test_index.py +++ b/tests/routes/task_orders/test_index.py @@ -29,8 +29,10 @@ def task_order(): user = UserFactory.create() portfolio = PortfolioFactory.create(owner=user) attachment = Attachment(filename="sample_attachment", object_name="sample") + task_order = TaskOrderFactory.create(portfolio=portfolio) + CLINFactory.create(task_order=task_order) - return TaskOrderFactory.create(portfolio=portfolio) + return task_order def test_review_task_order_not_draft(client, user_session, task_order): diff --git a/tests/routes/task_orders/test_new.py b/tests/routes/task_orders/test_new.py index 4d2f5fdb..1e8e16eb 100644 --- a/tests/routes/task_orders/test_new.py +++ b/tests/routes/task_orders/test_new.py @@ -19,6 +19,16 @@ def build_pdf_form_data(filename="sample.pdf", object_name=None): def task_order(): user = UserFactory.create() portfolio = PortfolioFactory.create(owner=user) + task_order = TaskOrderFactory.create(portfolio=portfolio) + CLINFactory.create(task_order=task_order) + + return task_order + + +@pytest.fixture +def incomplete_to(): + user = UserFactory.create() + portfolio = PortfolioFactory.create(owner=user) return TaskOrderFactory.create(portfolio=portfolio) @@ -234,7 +244,7 @@ def test_task_orders_submit_form_step_three_add_clins_existing_to( }, ] TaskOrders.create_clins(task_order.id, clin_list) - assert len(task_order.clins) == 2 + assert len(task_order.clins) == 3 user_session(task_order.portfolio.owner) form_data = { @@ -267,11 +277,11 @@ def test_task_orders_form_step_four_review(client, user_session, completed_task_ def test_task_orders_form_step_four_review_incomplete_to( - client, user_session, task_order + client, user_session, incomplete_to ): - user_session(task_order.portfolio.owner) + user_session(incomplete_to.portfolio.owner) response = client.get( - url_for("task_orders.form_step_four_review", task_order_id=task_order.id) + url_for("task_orders.form_step_four_review", task_order_id=incomplete_to.id) ) assert response.status_code == 404 @@ -290,12 +300,13 @@ def test_task_orders_form_step_five_confirm_signature( def test_task_orders_form_step_five_confirm_signature_incomplete_to( - client, user_session, task_order + client, user_session, incomplete_to ): - user_session(task_order.portfolio.owner) + user_session(incomplete_to.portfolio.owner) response = client.get( url_for( - "task_orders.form_step_five_confirm_signature", task_order_id=task_order.id + "task_orders.form_step_five_confirm_signature", + task_order_id=incomplete_to.id, ) ) assert response.status_code == 404