From 898f63a2f5bedfca6aff60eb49af0f5a3706b3c8 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Thu, 6 Jun 2019 11:37:29 -0400 Subject: [PATCH 01/16] Fix CLINFactory --- tests/factories.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/factories.py b/tests/factories.py index 05b8606f..c7c64dc5 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -293,7 +293,7 @@ class CLINFactory(Base): start_date = datetime.date.today() end_date = factory.LazyFunction(random_future_date) obligated_amount = random.randint(100, 999999) - jedi_clin_type = random.choice([e.value for e in clin.JEDICLINType]) + jedi_clin_type = random.choice([e for e in clin.JEDICLINType]) class NotificationRecipientFactory(Base): From 7b8ccbf1458d3fdba03a334e25af4b681d7aca73 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Thu, 6 Jun 2019 11:53:44 -0400 Subject: [PATCH 02/16] Implement TO start_date and end_date --- atst/models/task_order.py | 8 ++------ tests/factories.py | 2 +- tests/models/test_task_order.py | 35 ++++++++++++++++++++++++++++++++- 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/atst/models/task_order.py b/atst/models/task_order.py index 0e2d5ba2..ed0b394d 100644 --- a/atst/models/task_order.py +++ b/atst/models/task_order.py @@ -70,15 +70,11 @@ class TaskOrder(Base, mixins.TimestampsMixin): @property def start_date(self): - # TODO: fix task order -- reimplement using CLINs - # Faked for display purposes - return date.today() + return min(c.start_date for c in self.clins) @property def end_date(self): - # TODO: fix task order -- reimplement using CLINs - # Faked for display purposes - return date.today() + return max(c.end_date for c in self.clins) @property def days_to_expiration(self): diff --git a/tests/factories.py b/tests/factories.py index c7c64dc5..60bd76a9 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -293,7 +293,7 @@ class CLINFactory(Base): start_date = datetime.date.today() end_date = factory.LazyFunction(random_future_date) obligated_amount = random.randint(100, 999999) - jedi_clin_type = random.choice([e for e in clin.JEDICLINType]) + jedi_clin_type = random.choice(list(clin.JEDICLINType)) class NotificationRecipientFactory(Base): diff --git a/tests/models/test_task_order.py b/tests/models/test_task_order.py index d6ae9e1c..59d3beba 100644 --- a/tests/models/test_task_order.py +++ b/tests/models/test_task_order.py @@ -1,5 +1,6 @@ from werkzeug.datastructures import FileStorage -import pytest, datetime +import pytest +from datetime import date from atst.models import * from atst.models.clin import JEDICLINType @@ -14,6 +15,38 @@ from tests.factories import ( from tests.mocks import PDF_FILENAME +class TestPeriodOfPerformance: + def test_period_of_performance_is_first_to_last_clin(self): + start_date = date(2019, 6, 6) + end_date = date(2020, 6, 6) + + intermediate_start_date = date(2019, 7, 1) + intermediate_end_date = date(2020, 3, 1) + + task_order = TaskOrderFactory.create( + clins=[ + CLINFactory.create( + start_date=intermediate_start_date, end_date=intermediate_end_date + ), + CLINFactory.create( + start_date=start_date, end_date=intermediate_end_date + ), + CLINFactory.create( + start_date=intermediate_start_date, end_date=intermediate_end_date + ), + CLINFactory.create( + start_date=intermediate_start_date, end_date=end_date + ), + CLINFactory.create( + start_date=intermediate_start_date, end_date=intermediate_end_date + ), + ] + ) + + assert task_order.start_date == start_date + assert task_order.end_date == end_date + + class TestTaskOrderStatus: @pytest.mark.skip(reason="Reimplement after adding CLINs") def test_started_status(self): From 8ecf112c4833a3d82d356363ed80fd7589a5aa9b Mon Sep 17 00:00:00 2001 From: richard-dds Date: Thu, 6 Jun 2019 15:40:41 -0400 Subject: [PATCH 03/16] Implement new CLIN-based TO statuses --- atst/models/task_order.py | 32 +++++++++++--- tests/models/test_task_order.py | 78 +++++++++++++++++++++++++++------ 2 files changed, 90 insertions(+), 20 deletions(-) diff --git a/atst/models/task_order.py b/atst/models/task_order.py index ed0b394d..d34632cb 100644 --- a/atst/models/task_order.py +++ b/atst/models/task_order.py @@ -1,5 +1,5 @@ from enum import Enum -from datetime import date +from datetime import date, datetime from sqlalchemy import Column, DateTime, ForeignKey, String from sqlalchemy.ext.hybrid import hybrid_property @@ -15,6 +15,9 @@ class Status(Enum): PENDING = "Pending" ACTIVE = "Active" EXPIRED = "Expired" + DRAFT = "Draft" + UPCOMING = "Upcoming" + UNSIGNED = "Unsigned" class TaskOrder(Base, mixins.TimestampsMixin): @@ -62,19 +65,36 @@ class TaskOrder(Base, mixins.TimestampsMixin): def is_expired(self): return self.status == Status.EXPIRED + @property + def is_completed(self): + return True + + @property + def is_signed(self): + return self.signed_at is not None + @property def status(self): - # TODO: fix task order -- implement correctly using CLINs - # Faked for display purposes - return Status.ACTIVE + today = date.today() + + if not self.is_completed and not self.is_signed: + return Status.DRAFT + elif self.is_completed and not self.is_signed: + return Status.UNSIGNED + elif today < self.start_date: + return Status.UPCOMING + elif today >= self.end_date: + return Status.EXPIRED + elif self.start_date <= today < self.end_date: + return Status.ACTIVE @property def start_date(self): - return min(c.start_date for c in self.clins) + return min((c.start_date for c in self.clins), default=None) @property def end_date(self): - return max(c.end_date for c in self.clins) + return max((c.end_date for c in self.clins), default=None) @property def days_to_expiration(self): diff --git a/tests/models/test_task_order.py b/tests/models/test_task_order.py index 59d3beba..a9593281 100644 --- a/tests/models/test_task_order.py +++ b/tests/models/test_task_order.py @@ -1,6 +1,8 @@ from werkzeug.datastructures import FileStorage import pytest -from datetime import date +from datetime import date, datetime +from unittest.mock import Mock, patch, PropertyMock +import pendulum from atst.models import * from atst.models.clin import JEDICLINType @@ -48,26 +50,74 @@ class TestPeriodOfPerformance: class TestTaskOrderStatus: - @pytest.mark.skip(reason="Reimplement after adding CLINs") - def test_started_status(self): + @patch("atst.models.TaskOrder.is_completed", new_callable=PropertyMock) + @patch("atst.models.TaskOrder.is_signed", new_callable=PropertyMock) + def test_draft_status(self, is_signed, is_completed): + # Given that I have a TO that is neither completed nor signed to = TaskOrder() - assert to.status == Status.STARTED + is_signed.return_value = False + is_completed.return_value = False - @pytest.mark.skip(reason="See if still needed after implementing CLINs") - def test_pending_status(self): - to = TaskOrder(number="42") - assert to.status == Status.PENDING + assert to.status == Status.DRAFT - @pytest.mark.skip(reason="See if still needed after implementing CLINs") - def test_active_status(self): - to = TaskOrder(number="42") + @patch("atst.models.TaskOrder.end_date", new_callable=PropertyMock) + @patch("atst.models.TaskOrder.start_date", new_callable=PropertyMock) + @patch("atst.models.TaskOrder.is_completed", new_callable=PropertyMock) + @patch("atst.models.TaskOrder.is_signed", new_callable=PropertyMock) + def test_active_status(self, is_signed, is_completed, start_date, end_date): + # Given that I have a signed TO and today is within its start_date and end_date + today = pendulum.today().date() + to = TaskOrder() + + start_date.return_value = today.subtract(days=1) + end_date.return_value = today.add(days=1) + is_signed.return_value = True + is_completed.return_value = True + + # Its status should be active assert to.status == Status.ACTIVE - @pytest.mark.skip(reason="See if still needed after implementing CLINs") - def test_expired_status(self): - to = TaskOrder(number="42") + @patch("atst.models.TaskOrder.end_date", new_callable=PropertyMock) + @patch("atst.models.TaskOrder.start_date", new_callable=PropertyMock) + @patch("atst.models.TaskOrder.is_completed", new_callable=PropertyMock) + @patch("atst.models.TaskOrder.is_signed", new_callable=PropertyMock) + def test_upcoming_status(self, is_signed, is_completed, start_date, end_date): + # Given that I have a signed TO and today is before its start_date + to = TaskOrder() + start_date.return_value = pendulum.today().add(days=1).date() + end_date.return_value = pendulum.today().add(days=2).date() + is_signed.return_value = True + is_completed.return_value = True + + # Its status should be upcoming + assert to.status == Status.UPCOMING + + @patch("atst.models.TaskOrder.start_date", new_callable=PropertyMock) + @patch("atst.models.TaskOrder.end_date", new_callable=PropertyMock) + @patch("atst.models.TaskOrder.is_completed", new_callable=PropertyMock) + @patch("atst.models.TaskOrder.is_signed", new_callable=PropertyMock) + def test_expired_status(self, is_signed, is_completed, end_date, start_date): + # Given that I have a signed TO and today is after its expiration date + to = TaskOrder() + end_date.return_value = pendulum.today().subtract(days=1).date() + start_date.return_value = pendulum.today().subtract(days=2).date() + is_signed.return_value = True + is_completed.return_value = True + + # Its status should be expired assert to.status == Status.EXPIRED + @patch("atst.models.TaskOrder.is_completed", new_callable=PropertyMock) + @patch("atst.models.TaskOrder.is_signed", new_callable=PropertyMock) + def test_unsigned_status(self, is_signed, is_completed): + # Given that I have a TO that is completed but not signed + to = TaskOrder(signed_at=pendulum.now().subtract(days=1)) + is_completed.return_value = True + is_signed.return_value = False + + # Its status should be unsigned + assert to.status == Status.UNSIGNED + class TestBudget: def test_total_contract_amount(self): From 0f4d17a94ab408a3488e2228ee6917d03f7e1e43 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Thu, 6 Jun 2019 16:21:02 -0400 Subject: [PATCH 04/16] Implemen TaskOrder.is_completed --- atst/models/task_order.py | 2 +- tests/factories.py | 1 + tests/models/test_task_order.py | 60 ++++++++++++++++++--------------- 3 files changed, 35 insertions(+), 28 deletions(-) diff --git a/atst/models/task_order.py b/atst/models/task_order.py index d34632cb..0033c5aa 100644 --- a/atst/models/task_order.py +++ b/atst/models/task_order.py @@ -67,7 +67,7 @@ class TaskOrder(Base, mixins.TimestampsMixin): @property def is_completed(self): - return True + return all([self.pdf, self.number, len(self.clins)]) @property def is_signed(self): diff --git a/tests/factories.py b/tests/factories.py index 60bd76a9..18bf7e66 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -272,6 +272,7 @@ class TaskOrderFactory(Base): portfolio = factory.SubFactory(PortfolioFactory) number = factory.LazyFunction(random_task_order_number) creator = factory.SubFactory(UserFactory) + _pdf = factory.SubFactory(AttachmentFactory) @classmethod def _create(cls, model_class, *args, **kwargs): diff --git a/tests/models/test_task_order.py b/tests/models/test_task_order.py index a9593281..39893d08 100644 --- a/tests/models/test_task_order.py +++ b/tests/models/test_task_order.py @@ -17,36 +17,42 @@ from tests.factories import ( from tests.mocks import PDF_FILENAME -class TestPeriodOfPerformance: - def test_period_of_performance_is_first_to_last_clin(self): - start_date = date(2019, 6, 6) - end_date = date(2020, 6, 6) +def test_period_of_performance_is_first_to_last_clin(): + start_date = date(2019, 6, 6) + end_date = date(2020, 6, 6) - intermediate_start_date = date(2019, 7, 1) - intermediate_end_date = date(2020, 3, 1) + intermediate_start_date = date(2019, 7, 1) + intermediate_end_date = date(2020, 3, 1) - task_order = TaskOrderFactory.create( - clins=[ - CLINFactory.create( - start_date=intermediate_start_date, end_date=intermediate_end_date - ), - CLINFactory.create( - start_date=start_date, end_date=intermediate_end_date - ), - CLINFactory.create( - start_date=intermediate_start_date, end_date=intermediate_end_date - ), - CLINFactory.create( - start_date=intermediate_start_date, end_date=end_date - ), - CLINFactory.create( - start_date=intermediate_start_date, end_date=intermediate_end_date - ), - ] - ) + task_order = TaskOrderFactory.create( + clins=[ + CLINFactory.create( + start_date=intermediate_start_date, end_date=intermediate_end_date + ), + CLINFactory.create( + start_date=start_date, end_date=intermediate_end_date + ), + CLINFactory.create( + start_date=intermediate_start_date, end_date=intermediate_end_date + ), + CLINFactory.create( + start_date=intermediate_start_date, end_date=end_date + ), + CLINFactory.create( + start_date=intermediate_start_date, end_date=intermediate_end_date + ), + ] + ) - assert task_order.start_date == start_date - assert task_order.end_date == end_date + assert task_order.start_date == start_date + assert task_order.end_date == end_date + + +def test_task_order_completed(): + assert TaskOrderFactory.create(clins=[CLINFactory.create()]).is_completed + assert not TaskOrderFactory.create().is_completed + assert not TaskOrderFactory.create(clins=[]).is_completed + assert not TaskOrderFactory.create(number=None).is_completed class TestTaskOrderStatus: From fd159a2d806387cf22e09e74251047201be0e581 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Thu, 6 Jun 2019 16:25:50 -0400 Subject: [PATCH 05/16] Remove start_time assertion --- tests/routes/portfolios/test_index.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/routes/portfolios/test_index.py b/tests/routes/portfolios/test_index.py index 368aa02e..948c4301 100644 --- a/tests/routes/portfolios/test_index.py +++ b/tests/routes/portfolios/test_index.py @@ -100,8 +100,6 @@ def test_portfolio_reports(client, user_session): response = client.get(url_for("portfolios.reports", portfolio_id=portfolio.id)) assert response.status_code == 200 assert portfolio.name in response.data.decode() - expiration_date = task_order.end_date.strftime("%Y-%m-%d") - assert expiration_date in response.data.decode() def test_portfolio_reports_with_mock_portfolio(client, user_session): From e84e61bbad289265b3a8808d4b562d722110a9c4 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Thu, 6 Jun 2019 17:39:41 -0400 Subject: [PATCH 06/16] Update seed script with TOs of various statuses --- atst/models/task_order.py | 10 +++---- atst/routes/task_orders/index.py | 14 +--------- script/seed_sample.py | 29 ++++++++++++++------- templates/portfolios/task_orders/index.html | 16 +++--------- tests/models/test_task_order.py | 8 ++---- 5 files changed, 30 insertions(+), 47 deletions(-) diff --git a/atst/models/task_order.py b/atst/models/task_order.py index 0033c5aa..fce8fdfb 100644 --- a/atst/models/task_order.py +++ b/atst/models/task_order.py @@ -1,5 +1,5 @@ from enum import Enum -from datetime import date, datetime +from datetime import date from sqlalchemy import Column, DateTime, ForeignKey, String from sqlalchemy.ext.hybrid import hybrid_property @@ -11,12 +11,10 @@ from atst.models.clin import JEDICLINType class Status(Enum): - STARTED = "Started" - PENDING = "Pending" - ACTIVE = "Active" - EXPIRED = "Expired" DRAFT = "Draft" + ACTIVE = "Active" UPCOMING = "Upcoming" + EXPIRED = "Expired" UNSIGNED = "Unsigned" @@ -90,7 +88,7 @@ class TaskOrder(Base, mixins.TimestampsMixin): @property def start_date(self): - return min((c.start_date for c in self.clins), default=None) + return min((c.start_date for c in self.clins), default=self.time_created.date()) @property def end_date(self): diff --git a/atst/routes/task_orders/index.py b/atst/routes/task_orders/index.py index d91db98f..544db001 100644 --- a/atst/routes/task_orders/index.py +++ b/atst/routes/task_orders/index.py @@ -34,19 +34,7 @@ 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_by_status = defaultdict(list) - - for task_order in portfolio.task_orders: - task_orders_by_status[task_order.status].append(task_order) - - active_task_orders = task_orders_by_status.get(TaskOrderStatus.ACTIVE, []) return render_template( - "portfolios/task_orders/index.html", - 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, []), + "portfolios/task_orders/index.html", task_orders=portfolio.task_orders ) diff --git a/script/seed_sample.py b/script/seed_sample.py index 6c9d3f34..d499de69 100644 --- a/script/seed_sample.py +++ b/script/seed_sample.py @@ -1,7 +1,7 @@ # Add root application dir to the python path import os import sys -from datetime import timedelta, date +from datetime import timedelta, date, timedelta import random from faker import Faker @@ -31,6 +31,7 @@ from tests.factories import ( random_service_branch, random_task_order_number, TaskOrderFactory, + CLINFactory, ) fake = Faker() @@ -160,16 +161,26 @@ def add_members_to_portfolio(portfolio): def add_task_orders_to_portfolio(portfolio): - # TODO: after CLINs are implemented, vary the start/end dates of TOs - create_task_order(portfolio) - create_task_order(portfolio) - create_task_order(portfolio) + today = date.today() + future = today + timedelta(days=100) + yesterday = today - timedelta(days=1) + draft_to = TaskOrderFactory.build(portfolio=portfolio, pdf=None) + unsigned_to = TaskOrderFactory.build(portfolio=portfolio) + upcoming_to = TaskOrderFactory.build(portfolio=portfolio, signed_at=yesterday) + expired_to = TaskOrderFactory.build(portfolio=portfolio, signed_at=yesterday) + active_to = TaskOrderFactory.build(portfolio=portfolio, signed_at=yesterday) -def create_task_order(portfolio): - # TODO: after CLINs are implemented add them to TO - task_order = TaskOrderFactory.build(portfolio=portfolio) - db.session.add(task_order) + clins = [ + CLINFactory.build(task_order=unsigned_to, start_date=today, end_date=today), + CLINFactory.build(task_order=upcoming_to, start_date=future, end_date=future), + CLINFactory.build(task_order=expired_to, start_date=yesterday, end_date=yesterday), + CLINFactory.build(task_order=active_to, start_date=yesterday, end_date=future), + ] + + task_orders = [draft_to, unsigned_to, upcoming_to, expired_to, active_to] + + db.session.add_all(task_orders + clins) db.session.commit() diff --git a/templates/portfolios/task_orders/index.html b/templates/portfolios/task_orders/index.html index 2df23b45..598bbc5f 100644 --- a/templates/portfolios/task_orders/index.html +++ b/templates/portfolios/task_orders/index.html @@ -70,7 +70,9 @@
- {% if not active_task_orders and not pending_task_orders %} + {% if task_orders %} + {{ TaskOrderList(task_orders) }} + {% else %} {{ EmptyState( 'This portfolio doesn’t have any active or pending task orders.', action_label='Add a New Task Order', @@ -78,18 +80,6 @@ icon='cloud', ) }} {% endif %} - - {% if pending_task_orders %} - {{ TaskOrderList(pending_task_orders, label='warning') }} - {% endif %} - - {% if active_task_orders %} - {{ TaskOrderList(active_task_orders, label='success') }} - {% endif %} - - {% if expired_task_orders %} - {{ TaskOrderList(expired_task_orders, label='error') }} - {% endif %}
{% endblock %} diff --git a/tests/models/test_task_order.py b/tests/models/test_task_order.py index 39893d08..183bd4e8 100644 --- a/tests/models/test_task_order.py +++ b/tests/models/test_task_order.py @@ -29,15 +29,11 @@ def test_period_of_performance_is_first_to_last_clin(): CLINFactory.create( start_date=intermediate_start_date, end_date=intermediate_end_date ), - CLINFactory.create( - start_date=start_date, end_date=intermediate_end_date - ), + CLINFactory.create(start_date=start_date, end_date=intermediate_end_date), CLINFactory.create( start_date=intermediate_start_date, end_date=intermediate_end_date ), - CLINFactory.create( - start_date=intermediate_start_date, end_date=end_date - ), + CLINFactory.create(start_date=intermediate_start_date, end_date=end_date), CLINFactory.create( start_date=intermediate_start_date, end_date=intermediate_end_date ), From 7f4f857424963f1d34a755091e9ba5565e72468e Mon Sep 17 00:00:00 2001 From: richard-dds Date: Mon, 10 Jun 2019 11:32:53 -0400 Subject: [PATCH 07/16] Sort task orders by status and time_created --- atst/domain/task_orders.py | 9 ++++++++- atst/models/task_order.py | 8 ++++++++ atst/routes/task_orders/index.py | 9 ++------- script/seed_sample.py | 4 +++- 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/atst/domain/task_orders.py b/atst/domain/task_orders.py index b3206b35..a433965e 100644 --- a/atst/domain/task_orders.py +++ b/atst/domain/task_orders.py @@ -2,7 +2,7 @@ from flask import current_app as app from atst.database import db from atst.models.clin import CLIN -from atst.models.task_order import TaskOrder +from atst.models.task_order import TaskOrder, SORT_ORDERING from . import BaseDomainClass @@ -98,3 +98,10 @@ class TaskOrders(BaseDomainClass): if not app.config.get("CLASSIFIED"): section_list["funding"] = TaskOrders.UNCLASSIFIED_FUNDING return section_list + + @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)) + return by_status diff --git a/atst/models/task_order.py b/atst/models/task_order.py index fce8fdfb..eda19ce1 100644 --- a/atst/models/task_order.py +++ b/atst/models/task_order.py @@ -18,6 +18,14 @@ class Status(Enum): UNSIGNED = "Unsigned" +SORT_ORDERING = { + status: order + for (order, status) in enumerate( + [Status.DRAFT, Status.ACTIVE, Status.UPCOMING, Status.EXPIRED, Status.UNSIGNED] + ) +} + + class TaskOrder(Base, mixins.TimestampsMixin): __tablename__ = "task_orders" diff --git a/atst/routes/task_orders/index.py b/atst/routes/task_orders/index.py index 544db001..1c85dab4 100644 --- a/atst/routes/task_orders/index.py +++ b/atst/routes/task_orders/index.py @@ -1,5 +1,3 @@ -from collections import defaultdict - from flask import g, render_template from . import task_orders_bp @@ -7,7 +5,6 @@ from atst.domain.authz.decorator import user_can_access_decorator as user_can from atst.domain.portfolios import Portfolios from atst.domain.task_orders import TaskOrders from atst.models import Permissions -from atst.models.task_order import Status as TaskOrderStatus @task_orders_bp.route("/task_orders/") @@ -34,7 +31,5 @@ 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) - - return render_template( - "portfolios/task_orders/index.html", task_orders=portfolio.task_orders - ) + task_orders = TaskOrders.sort(portfolio.task_orders) + return render_template("portfolios/task_orders/index.html", task_orders=task_orders) diff --git a/script/seed_sample.py b/script/seed_sample.py index d499de69..8fced973 100644 --- a/script/seed_sample.py +++ b/script/seed_sample.py @@ -174,7 +174,9 @@ def add_task_orders_to_portfolio(portfolio): clins = [ CLINFactory.build(task_order=unsigned_to, start_date=today, end_date=today), CLINFactory.build(task_order=upcoming_to, start_date=future, end_date=future), - CLINFactory.build(task_order=expired_to, start_date=yesterday, end_date=yesterday), + CLINFactory.build( + task_order=expired_to, start_date=yesterday, end_date=yesterday + ), CLINFactory.build(task_order=active_to, start_date=yesterday, end_date=future), ] From 1cb673af538aeba6e77bf951d5bc223f17afb708 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Mon, 10 Jun 2019 13:17:00 -0400 Subject: [PATCH 08/16] Proper colors for TO statuses --- atst/models/task_order.py | 2 +- atst/routes/task_orders/index.py | 10 +++++++++- styles/elements/_labels.scss | 4 ++++ styles/sections/_task_order.scss | 3 +++ templates/portfolios/task_orders/index.html | 2 +- 5 files changed, 18 insertions(+), 3 deletions(-) diff --git a/atst/models/task_order.py b/atst/models/task_order.py index eda19ce1..1fc70da3 100644 --- a/atst/models/task_order.py +++ b/atst/models/task_order.py @@ -15,7 +15,7 @@ class Status(Enum): ACTIVE = "Active" UPCOMING = "Upcoming" EXPIRED = "Expired" - UNSIGNED = "Unsigned" + UNSIGNED = "Not signed" SORT_ORDERING = { diff --git a/atst/routes/task_orders/index.py b/atst/routes/task_orders/index.py index 1c85dab4..c62573f5 100644 --- a/atst/routes/task_orders/index.py +++ b/atst/routes/task_orders/index.py @@ -4,6 +4,7 @@ from . import task_orders_bp from atst.domain.authz.decorator import user_can_access_decorator as user_can from atst.domain.portfolios import Portfolios from atst.domain.task_orders import TaskOrders +from atst.models.task_order import Status from atst.models import Permissions @@ -32,4 +33,11 @@ def review_task_order(task_order_id): def portfolio_funding(portfolio_id): portfolio = Portfolios.get(g.current_user, portfolio_id) task_orders = TaskOrders.sort(portfolio.task_orders) - return render_template("portfolios/task_orders/index.html", task_orders=task_orders) + label_colors = { + Status.DRAFT: "warning", + Status.ACTIVE: "success", + Status.UPCOMING: "info", + Status.EXPIRED: "error", + Status.UNSIGNED: "purple" + } + return render_template("portfolios/task_orders/index.html", task_orders=task_orders, label_colors=label_colors) diff --git a/styles/elements/_labels.scss b/styles/elements/_labels.scss index d28ab484..15758fe2 100644 --- a/styles/elements/_labels.scss +++ b/styles/elements/_labels.scss @@ -33,4 +33,8 @@ &.label--success { background-color: $color-green; } + + &.label--purple { + background-color: $color-purple; + } } diff --git a/styles/sections/_task_order.scss b/styles/sections/_task_order.scss index 7a0186b4..dc97dfc1 100644 --- a/styles/sections/_task_order.scss +++ b/styles/sections/_task_order.scss @@ -73,6 +73,9 @@ .task-order-card .label { font-size: $small-font-size; margin-right: 2 * $gap; + min-width: 7rem; + display: flex; + justify-content: space-around; } .task-order-card__buttons .usa-button { diff --git a/templates/portfolios/task_orders/index.html b/templates/portfolios/task_orders/index.html index 598bbc5f..0e4c52b8 100644 --- a/templates/portfolios/task_orders/index.html +++ b/templates/portfolios/task_orders/index.html @@ -45,7 +45,7 @@ {% for task_order in task_orders %}
- {{ task_order.display_status }} + {{ task_order.display_status }} {{ TaskOrderDate(task_order) }} From 373e802b466b3f662edb32ee078ee87363b02cb8 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Mon, 10 Jun 2019 13:17:32 -0400 Subject: [PATCH 09/16] Formatting --- atst/routes/task_orders/index.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/atst/routes/task_orders/index.py b/atst/routes/task_orders/index.py index c62573f5..47a0ff56 100644 --- a/atst/routes/task_orders/index.py +++ b/atst/routes/task_orders/index.py @@ -38,6 +38,10 @@ def portfolio_funding(portfolio_id): Status.ACTIVE: "success", Status.UPCOMING: "info", Status.EXPIRED: "error", - Status.UNSIGNED: "purple" + Status.UNSIGNED: "purple", } - return render_template("portfolios/task_orders/index.html", task_orders=task_orders, label_colors=label_colors) + return render_template( + "portfolios/task_orders/index.html", + task_orders=task_orders, + label_colors=label_colors, + ) From 6eb64d4aaefe588b29af46eef50c9d9a275d384c Mon Sep 17 00:00:00 2001 From: richard-dds Date: Mon, 10 Jun 2019 13:28:56 -0400 Subject: [PATCH 10/16] Add test for TaskOrders.sort --- tests/domain/test_task_orders.py | 81 +++++++++++++++++++++++++++----- 1 file changed, 70 insertions(+), 11 deletions(-) diff --git a/tests/domain/test_task_orders.py b/tests/domain/test_task_orders.py index 6a337505..cbc47345 100644 --- a/tests/domain/test_task_orders.py +++ b/tests/domain/test_task_orders.py @@ -1,19 +1,78 @@ import pytest -from datetime import date +from datetime import date, timedelta from decimal import Decimal -from atst.domain.task_orders import TaskOrders, TaskOrderError -from atst.domain.exceptions import UnauthorizedError -from atst.domain.permission_sets import PermissionSets -from atst.domain.portfolio_roles import PortfolioRoles +from atst.domain.task_orders import TaskOrders from atst.models.attachment import Attachment +from tests.factories import TaskOrderFactory, CLINFactory -from tests.factories import ( - TaskOrderFactory, - UserFactory, - PortfolioRoleFactory, - 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 @pytest.mark.skip(reason="Need to reimplement after new TO form is created") From a2d201e43b1219070287d857a8ad308fa924e372 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Mon, 10 Jun 2019 14:54:05 -0400 Subject: [PATCH 11/16] Use UTC tz for determining TO status --- atst/models/task_order.py | 5 +++-- atst/utils/clock.py | 10 ++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 atst/utils/clock.py diff --git a/atst/models/task_order.py b/atst/models/task_order.py index 1fc70da3..37700c4e 100644 --- a/atst/models/task_order.py +++ b/atst/models/task_order.py @@ -8,6 +8,7 @@ from werkzeug.datastructures import FileStorage from atst.models import Attachment, Base, mixins, types from atst.models.clin import JEDICLINType +from atst.utils.clock import Clock class Status(Enum): @@ -81,7 +82,7 @@ class TaskOrder(Base, mixins.TimestampsMixin): @property def status(self): - today = date.today() + today = Clock.today() if not self.is_completed and not self.is_signed: return Status.DRAFT @@ -105,7 +106,7 @@ class TaskOrder(Base, mixins.TimestampsMixin): @property def days_to_expiration(self): if self.end_date: - return (self.end_date - date.today()).days + return (self.end_date - Clock.today()).days @property def total_obligated_funds(self): diff --git a/atst/utils/clock.py b/atst/utils/clock.py new file mode 100644 index 00000000..8aa5e35a --- /dev/null +++ b/atst/utils/clock.py @@ -0,0 +1,10 @@ +import pendulum + +class Clock(object): + @classmethod + def today(cls, tz="UTC"): + return pendulum.today(tz=tz).date() + + @classmethod + def now(cls, tz="UTC"): + return pendulum.now(tz=tz) From c4d02bb026f9046ecd346a0c236b3d8a7aa10f74 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Mon, 10 Jun 2019 14:57:45 -0400 Subject: [PATCH 12/16] Remove unused imports --- tests/models/test_task_order.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/models/test_task_order.py b/tests/models/test_task_order.py index 183bd4e8..15bda933 100644 --- a/tests/models/test_task_order.py +++ b/tests/models/test_task_order.py @@ -1,7 +1,7 @@ from werkzeug.datastructures import FileStorage import pytest -from datetime import date, datetime -from unittest.mock import Mock, patch, PropertyMock +from datetime import date +from unittest.mock import patch, PropertyMock import pendulum from atst.models import * @@ -10,8 +10,6 @@ from atst.models.task_order import TaskOrder, Status from tests.factories import ( CLINFactory, - random_future_date, - random_past_date, TaskOrderFactory, ) from tests.mocks import PDF_FILENAME From 48d4b466b195ec04523820c6a4ea4993cd8b1465 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Mon, 10 Jun 2019 15:04:11 -0400 Subject: [PATCH 13/16] Formatting --- atst/utils/clock.py | 1 + tests/models/test_task_order.py | 5 +---- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/atst/utils/clock.py b/atst/utils/clock.py index 8aa5e35a..1ac4455e 100644 --- a/atst/utils/clock.py +++ b/atst/utils/clock.py @@ -1,5 +1,6 @@ import pendulum + class Clock(object): @classmethod def today(cls, tz="UTC"): diff --git a/tests/models/test_task_order.py b/tests/models/test_task_order.py index 15bda933..8b02effb 100644 --- a/tests/models/test_task_order.py +++ b/tests/models/test_task_order.py @@ -8,10 +8,7 @@ from atst.models import * from atst.models.clin import JEDICLINType from atst.models.task_order import TaskOrder, Status -from tests.factories import ( - CLINFactory, - TaskOrderFactory, -) +from tests.factories import CLINFactory, TaskOrderFactory from tests.mocks import PDF_FILENAME From 5339fd34d5e6cf20b734ade24feec0e7f1093d52 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Mon, 10 Jun 2019 15:11:17 -0400 Subject: [PATCH 14/16] Remove another unused import --- atst/models/task_order.py | 1 - 1 file changed, 1 deletion(-) diff --git a/atst/models/task_order.py b/atst/models/task_order.py index 37700c4e..5088ce70 100644 --- a/atst/models/task_order.py +++ b/atst/models/task_order.py @@ -1,5 +1,4 @@ from enum import Enum -from datetime import date from sqlalchemy import Column, DateTime, ForeignKey, String from sqlalchemy.ext.hybrid import hybrid_property From 40b599d1d0145ce03d007354e2407f9c9bb56184 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Mon, 10 Jun 2019 15:33:30 -0400 Subject: [PATCH 15/16] Fix rebase conflicts --- script/example_fetch_from_eda.py | 2 +- tests/domain/test_task_orders.py | 4 ++-- tests/factories.py | 4 ++-- tests/routes/task_orders/test_downloads.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/script/example_fetch_from_eda.py b/script/example_fetch_from_eda.py index 4cd88664..27757ead 100644 --- a/script/example_fetch_from_eda.py +++ b/script/example_fetch_from_eda.py @@ -31,5 +31,5 @@ contract = client.get_contract(contract_number=contract_number, status="Y") requested_clins = ",".join(["'0001'", "'0003'", "'1001'", "'1003'", "'2001'", "'2003'"]) clins = client.get_clins( - record_key=contract_number, duns_number="", cage_code="1U305", clins=requested_clins + record_key=contract_number, duns_number="", cage_code="1U305", with_clins=requested_clins ) diff --git a/tests/domain/test_task_orders.py b/tests/domain/test_task_orders.py index cbc47345..a08a75a2 100644 --- a/tests/domain/test_task_orders.py +++ b/tests/domain/test_task_orders.py @@ -4,7 +4,7 @@ from decimal import Decimal from atst.domain.task_orders import TaskOrders from atst.models.attachment import Attachment -from tests.factories import TaskOrderFactory, CLINFactory +from tests.factories import TaskOrderFactory, CLINFactory, PortfolioFactory def test_task_order_sorting(): @@ -177,7 +177,7 @@ def test_update_adds_clins(pdf_upload): def test_update_does_not_duplicate_clins(pdf_upload): - task_order = TaskOrderFactory.create(number="3453453456", clins=["123", "456"]) + task_order = TaskOrderFactory.create(number="3453453456", create_clins=["123", "456"]) clins = [ { "jedi_clin_type": "JEDI_CLIN_1", diff --git a/tests/factories.py b/tests/factories.py index 18bf7e66..3bbee856 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -276,10 +276,10 @@ class TaskOrderFactory(Base): @classmethod def _create(cls, model_class, *args, **kwargs): - with_clins = kwargs.pop("clins", []) + create_clins = kwargs.pop("create_clins", []) task_order = super()._create(model_class, *args, **kwargs) - for clin in with_clins: + for clin in create_clins: CLINFactory.create(task_order=task_order, number=clin) return task_order diff --git a/tests/routes/task_orders/test_downloads.py b/tests/routes/task_orders/test_downloads.py index 965d8d6d..173a0f27 100644 --- a/tests/routes/task_orders/test_downloads.py +++ b/tests/routes/task_orders/test_downloads.py @@ -18,7 +18,7 @@ def xml_translated(val): def test_download_summary(client, user_session): user = UserFactory.create() portfolio = PortfolioFactory.create(owner=user) - task_order = TaskOrderFactory.create(creator=user, portfolio=portfolio) + task_order = TaskOrderFactory.create(creator=user, portfolio=portfolio, _pdf=None) user_session(user) response = client.get( url_for("task_orders.download_summary", task_order_id=task_order.id) From ee46fb2320600a490b58e53e1543e09fb4259f2e Mon Sep 17 00:00:00 2001 From: richard-dds Date: Mon, 10 Jun 2019 15:34:01 -0400 Subject: [PATCH 16/16] Formatting --- script/example_fetch_from_eda.py | 5 ++++- tests/domain/test_task_orders.py | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/script/example_fetch_from_eda.py b/script/example_fetch_from_eda.py index 27757ead..dc297347 100644 --- a/script/example_fetch_from_eda.py +++ b/script/example_fetch_from_eda.py @@ -31,5 +31,8 @@ contract = client.get_contract(contract_number=contract_number, status="Y") requested_clins = ",".join(["'0001'", "'0003'", "'1001'", "'1003'", "'2001'", "'2003'"]) clins = client.get_clins( - record_key=contract_number, duns_number="", cage_code="1U305", with_clins=requested_clins + record_key=contract_number, + duns_number="", + cage_code="1U305", + with_clins=requested_clins, ) diff --git a/tests/domain/test_task_orders.py b/tests/domain/test_task_orders.py index a08a75a2..4a62844c 100644 --- a/tests/domain/test_task_orders.py +++ b/tests/domain/test_task_orders.py @@ -177,7 +177,9 @@ def test_update_adds_clins(pdf_upload): def test_update_does_not_duplicate_clins(pdf_upload): - task_order = TaskOrderFactory.create(number="3453453456", create_clins=["123", "456"]) + task_order = TaskOrderFactory.create( + number="3453453456", create_clins=["123", "456"] + ) clins = [ { "jedi_clin_type": "JEDI_CLIN_1",