From fc0af1558d6697b7328a653dbf610a88233f7473 Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Wed, 12 Feb 2020 11:36:08 -0500 Subject: [PATCH 1/8] Remove all references to funds expended on a TO level --- atst/models/task_order.py | 9 --------- templates/task_orders/fragments/task_order_view.html | 9 --------- templates/task_orders/index.html | 4 ---- translations.yaml | 2 -- 4 files changed, 24 deletions(-) diff --git a/atst/models/task_order.py b/atst/models/task_order.py index c6dda237..370cc325 100644 --- a/atst/models/task_order.py +++ b/atst/models/task_order.py @@ -1,5 +1,4 @@ from enum import Enum -from decimal import Decimal from sqlalchemy import Column, DateTime, ForeignKey, String from sqlalchemy.ext.hybrid import hybrid_property @@ -141,14 +140,6 @@ class TaskOrder(Base, mixins.TimestampsMixin): def total_contract_amount(self): return sum((clin.total_amount for clin in self.clins if clin.total_amount)) - @property - def invoiced_funds(self): - # TODO: implement this using reporting data from the CSP - if self.is_active: - return self.total_obligated_funds * Decimal(0.75) - else: - return 0 - @property def display_status(self): if self.status == Status.UNSIGNED: diff --git a/templates/task_orders/fragments/task_order_view.html b/templates/task_orders/fragments/task_order_view.html index 796bbfa6..b71a683c 100644 --- a/templates/task_orders/fragments/task_order_view.html +++ b/templates/task_orders/fragments/task_order_view.html @@ -27,15 +27,6 @@ {{ obligated_funds | dollars }}

-
-

- {{ 'task_orders.summary.expended' | translate }} - {{ Tooltip(("task_orders.review.tooltip.expended_funds" | translate), title="", classes="icon-tooltip--tight") }} -

-

- {{ expended_funds | dollars }} -

-

diff --git a/templates/task_orders/index.html b/templates/task_orders/index.html index db91fc22..cbc412d4 100644 --- a/templates/task_orders/index.html +++ b/templates/task_orders/index.html @@ -53,10 +53,6 @@
Total Obligated

{{ task_order.total_obligated_funds | dollars }}

-
-
Total Expended
-

{{ task_order.invoiced_funds | dollars }}

-
{%- endif %} diff --git a/translations.yaml b/translations.yaml index a147dc13..3b53ded1 100644 --- a/translations.yaml +++ b/translations.yaml @@ -531,7 +531,6 @@ task_orders: tooltip: obligated_funds: Funds committed to fund your portfolio. This may represent 100% of your total Task Order value, or a portion of it. total_value: All obligated and projected funds for the Task Order’s Base and Option CLINs. - expended_funds: All funds spent from the Task Order so far. form: add_clin: Add Another CLIN add_to_header: Enter the Task Order number @@ -591,7 +590,6 @@ task_orders: summary: obligated: Total Obligated total: Total Value - expended: Total Expended JEDICLINType: JEDI_CLIN_1: "IDIQ CLIN 0001 Unclassified IaaS/PaaS" JEDI_CLIN_2: "IDIQ CLIN 0002 Classified IaaS/PaaS" From 4917cde22d3d2018c9770ed91462c49fd4c311aa Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Wed, 12 Feb 2020 11:53:04 -0500 Subject: [PATCH 2/8] Fix styling to account for removed columns --- templates/task_orders/fragments/task_order_view.html | 6 +++--- templates/task_orders/index.html | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/templates/task_orders/fragments/task_order_view.html b/templates/task_orders/fragments/task_order_view.html index b71a683c..c78434ee 100644 --- a/templates/task_orders/fragments/task_order_view.html +++ b/templates/task_orders/fragments/task_order_view.html @@ -8,8 +8,8 @@ {% set expended_funds = task_order.invoiced_funds %}
-
-
+
+

{{ 'task_orders.summary.total' | translate }} {{ Tooltip(("task_orders.review.tooltip.total_value" | translate), title="", classes="icon-tooltip--tight") }} @@ -18,7 +18,7 @@ {{ contract_amount | dollars }}

-
+

{{ 'task_orders.summary.obligated' | translate }} {{ Tooltip(("task_orders.review.tooltip.obligated_funds" | translate), title="", classes="icon-tooltip--tight") }} diff --git a/templates/task_orders/index.html b/templates/task_orders/index.html index cbc412d4..f643894a 100644 --- a/templates/task_orders/index.html +++ b/templates/task_orders/index.html @@ -34,8 +34,8 @@

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

{% if status != 'Expired' -%} -
-
+
+
Current Period of Performance
@@ -45,11 +45,11 @@ {{ 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 }}

From 108f65f928e497ac06a6e2670b7618b75c7b0f70 Mon Sep 17 00:00:00 2001 From: graham-dds Date: Fri, 7 Feb 2020 10:38:59 -0500 Subject: [PATCH 3/8] Use pendulum for datetime operations when possible Currently, we use both Python's built-in datetime library and Pendulum to do datetime operations. For the sake of consistency, we should try to stick to one library for datetimes. We could have used either, but Pendulum has a more ergonomic API, so I decided to go with it when possible. The places where were we didn't / couldn't replace datetime are: - checking instances of datetimes. Pendulum's objects are subclasses of python native datetime objects, so it's still useful to import datetime in those cases of using is_instance() - WTForms date validators expect datetime style string formats -- Pendulum has its own format for formatting/ parsing strings. As such, our custom validator DateRange needs to use datetime.stptime() to account for this format. --- atst/app.py | 10 +++--- atst/domain/authnid/crl/__init__.py | 1 - atst/domain/csp/files.py | 8 ++--- atst/domain/invitations.py | 6 ++-- atst/domain/task_orders.py | 5 ++- atst/domain/users.py | 4 +-- atst/filters.py | 6 ---- atst/models/clin.py | 4 +-- atst/models/mixins/invites.py | 4 +-- atst/routes/portfolios/index.py | 5 ++- atst/routes/users.py | 10 +++--- script/seed_sample.py | 15 ++++---- tests/conftest.py | 14 ++++---- tests/domain/test_applications.py | 6 ++-- tests/domain/test_invitations.py | 4 +-- tests/domain/test_task_orders.py | 40 ++++++++++----------- tests/domain/test_users.py | 1 - tests/factories.py | 11 +++--- tests/forms/test_task_order.py | 18 +++++----- tests/models/test_application_role.py | 3 +- tests/models/test_portfolio.py | 9 +++-- tests/models/test_portfolio_invitations.py | 4 +-- tests/models/test_portfolio_role.py | 4 +-- tests/models/test_task_order.py | 10 +++--- tests/models/test_user.py | 4 +-- tests/routes/applications/test_settings.py | 4 +-- tests/routes/portfolios/test_invitations.py | 10 +++--- tests/routes/task_orders/test_index.py | 2 -- tests/routes/task_orders/test_new.py | 6 ++-- tests/test_auth.py | 6 ++-- 30 files changed, 108 insertions(+), 126 deletions(-) diff --git a/atst/app.py b/atst/app.py index 1499671c..d510ff82 100644 --- a/atst/app.py +++ b/atst/app.py @@ -1,7 +1,7 @@ import os import re from configparser import ConfigParser -from datetime import datetime +import pendulum from flask import Flask, request, g, session, url_for as flask_url_for from flask_session import Session import redis @@ -187,11 +187,11 @@ def map_config(config): "CELERY_RESULT_EXPIRES": 0, "CELERY_RESULT_EXTENDED": True, "OFFICE_365_DOMAIN": "onmicrosoft.com", - "CONTRACT_START_DATE": datetime.strptime( - config.get("default", "CONTRACT_START_DATE"), "%Y-%m-%d" + "CONTRACT_START_DATE": pendulum.from_format( + config.get("default", "CONTRACT_START_DATE"), "YYYY-MM-DD" ).date(), - "CONTRACT_END_DATE": datetime.strptime( - config.get("default", "CONTRACT_END_DATE"), "%Y-%m-%d" + "CONTRACT_END_DATE": pendulum.from_format( + config.get("default", "CONTRACT_END_DATE"), "YYYY-MM-DD" ).date(), "SESSION_COOKIE_SECURE": config.getboolean("default", "SESSION_COOKIE_SECURE"), } diff --git a/atst/domain/authnid/crl/__init__.py b/atst/domain/authnid/crl/__init__.py index 037a8bb4..9a8f6638 100644 --- a/atst/domain/authnid/crl/__init__.py +++ b/atst/domain/authnid/crl/__init__.py @@ -4,7 +4,6 @@ import hashlib import logging from OpenSSL import crypto, SSL -from datetime import datetime from flask import current_app as app from .util import load_crl_locations_cache, serialize_crl_locations_cache, CRL_LIST diff --git a/atst/domain/csp/files.py b/atst/domain/csp/files.py index 0f3e05a0..f2f9383c 100644 --- a/atst/domain/csp/files.py +++ b/atst/domain/csp/files.py @@ -1,5 +1,5 @@ -from datetime import datetime, timedelta from uuid import uuid4 +import pendulum class FileService: @@ -39,7 +39,7 @@ class AzureFileService(FileService): self.account_name = config["AZURE_ACCOUNT_NAME"] self.storage_key = config["AZURE_STORAGE_KEY"] self.container_name = config["AZURE_TO_BUCKET_NAME"] - self.timeout = timedelta(seconds=config["PERMANENT_SESSION_LIFETIME"]) + self.timeout = config["PERMANENT_SESSION_LIFETIME"] from azure.storage.common import CloudStorageAccount from azure.storage.blob import BlobSasPermissions @@ -68,7 +68,7 @@ class AzureFileService(FileService): self.container_name, object_name, permission=self.BlobSasPermissions(create=True), - expiry=datetime.utcnow() + self.timeout, + expiry=pendulum.now(tz="utc").add(self.timeout), protocol="https", ) return ({"token": sas_token}, object_name) @@ -81,7 +81,7 @@ class AzureFileService(FileService): container_name=self.container_name, blob_name=object_name, permission=self.BlobPermissions(read=True), - expiry=datetime.utcnow() + self.timeout, + expiry=pendulum.now(tz="utc").add(self.timeout), content_disposition=f"attachment; filename={filename}", protocol="https", ) diff --git a/atst/domain/invitations.py b/atst/domain/invitations.py index 069be936..f6bfeacb 100644 --- a/atst/domain/invitations.py +++ b/atst/domain/invitations.py @@ -1,5 +1,5 @@ -import datetime from sqlalchemy.orm.exc import NoResultFound +import pendulum from atst.database import db from atst.models import ApplicationInvitation, InvitationStatus, PortfolioInvitation @@ -99,9 +99,7 @@ class BaseInvitations(object): @classmethod def current_expiration_time(cls): - return datetime.datetime.now() + datetime.timedelta( - minutes=cls.EXPIRATION_LIMIT_MINUTES - ) + return pendulum.now(tz="utc").add(minutes=cls.EXPIRATION_LIMIT_MINUTES) @classmethod def _update_status(cls, invite, new_status): diff --git a/atst/domain/task_orders.py b/atst/domain/task_orders.py index 499bccb0..44d6b47e 100644 --- a/atst/domain/task_orders.py +++ b/atst/domain/task_orders.py @@ -1,5 +1,5 @@ -from datetime import datetime from sqlalchemy import or_ +import pendulum from atst.database import db from atst.models.clin import CLIN @@ -41,8 +41,7 @@ class TaskOrders(BaseDomainClass): @classmethod def sign(cls, task_order, signer_dod_id): task_order.signer_dod_id = signer_dod_id - task_order.signed_at = datetime.now() - + task_order.signed_at = pendulum.now(tz="utc") db.session.add(task_order) db.session.commit() diff --git a/atst/domain/users.py b/atst/domain/users.py index e5fdbad7..6ad61cde 100644 --- a/atst/domain/users.py +++ b/atst/domain/users.py @@ -1,6 +1,6 @@ from sqlalchemy.orm.exc import NoResultFound from sqlalchemy.exc import IntegrityError -from datetime import datetime +import pendulum from atst.database import db from atst.models import User @@ -111,7 +111,7 @@ class Users(object): @classmethod def update_last_login(cls, user): - user.last_login = datetime.now() + user.last_login = pendulum.now(tz="utc") db.session.add(user) db.session.commit() diff --git a/atst/filters.py b/atst/filters.py index 84191017..0eb93d2b 100644 --- a/atst/filters.py +++ b/atst/filters.py @@ -1,5 +1,4 @@ import re -import datetime from atst.utils.localization import translate from flask import render_template from jinja2 import contextfilter @@ -54,10 +53,6 @@ def formattedDate(value, formatter="%m/%d/%Y"): return "-" -def dateFromString(value, formatter="%m/%Y"): - return datetime.datetime.strptime(value, formatter) - - def pageWindow(pagination, size=2): page = pagination.page num_pages = pagination.pages @@ -81,7 +76,6 @@ def register_filters(app): app.jinja_env.filters["dollars"] = dollars app.jinja_env.filters["usPhone"] = usPhone app.jinja_env.filters["formattedDate"] = formattedDate - app.jinja_env.filters["dateFromString"] = dateFromString app.jinja_env.filters["pageWindow"] = pageWindow app.jinja_env.filters["renderAuditEvent"] = renderAuditEvent app.jinja_env.filters["withExtraParams"] = with_extra_params diff --git a/atst/models/clin.py b/atst/models/clin.py index 13a63cee..accab107 100644 --- a/atst/models/clin.py +++ b/atst/models/clin.py @@ -9,7 +9,7 @@ from sqlalchemy import ( String, ) from sqlalchemy.orm import relationship -from datetime import date +import pendulum from atst.models.base import Base import atst.models.mixins as mixins @@ -75,5 +75,5 @@ class CLIN(Base, mixins.TimestampsMixin): @property def is_active(self): return ( - self.start_date <= date.today() <= self.end_date + self.start_date <= pendulum.today() <= self.end_date ) and self.task_order.signed_at diff --git a/atst/models/mixins/invites.py b/atst/models/mixins/invites.py index 18916dc4..c87917ba 100644 --- a/atst/models/mixins/invites.py +++ b/atst/models/mixins/invites.py @@ -1,4 +1,4 @@ -import datetime +import pendulum from enum import Enum import secrets @@ -90,7 +90,7 @@ class InvitesMixin(object): @property def is_expired(self): return ( - datetime.datetime.now(self.expiration_time.tzinfo) > self.expiration_time + pendulum.now(tz=self.expiration_time.tzinfo) > self.expiration_time and not self.status == Status.ACCEPTED ) diff --git a/atst/routes/portfolios/index.py b/atst/routes/portfolios/index.py index 44cac768..6472fca1 100644 --- a/atst/routes/portfolios/index.py +++ b/atst/routes/portfolios/index.py @@ -1,5 +1,4 @@ -from datetime import datetime - +import pendulum from flask import redirect, render_template, url_for, request as http_request, g from .blueprint import portfolios_bp @@ -56,5 +55,5 @@ def reports(portfolio_id): ), current_obligated_funds=current_obligated_funds, expired_task_orders=Reports.expired_task_orders(portfolio), - retrieved=datetime.now(), # mocked datetime of reporting data retrival + retrieved=pendulum.now(), # mocked datetime of reporting data retrival ) diff --git a/atst/routes/users.py b/atst/routes/users.py index ec5557aa..b055bcb6 100644 --- a/atst/routes/users.py +++ b/atst/routes/users.py @@ -1,4 +1,4 @@ -import datetime as dt +import pendulum from flask import Blueprint, render_template, g, request as http_request, redirect from atst.forms.edit_user import EditUserForm from atst.domain.users import Users @@ -23,8 +23,8 @@ def user(): next=next_, form=form, user=user, - mindate=(dt.datetime.now() - dt.timedelta(days=365)), - maxdate=dt.datetime.now(), + mindate=pendulum.now(tz="utc").subtract(days=365), + maxdate=pendulum.now(tz="utc"), ) @@ -44,6 +44,6 @@ def update_user(): form=form, user=user, next=next_url, - mindate=(dt.datetime.now() - dt.timedelta(days=365)), - maxdate=dt.datetime.now(), + mindate=pendulum.now(tz="utc").subtract(days=365), + maxdate=pendulum.now(tz="utc"), ) diff --git a/script/seed_sample.py b/script/seed_sample.py index d1cf1c9e..bc8d13ba 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 +import pendulum import random from faker import Faker from werkzeug.datastructures import FileStorage @@ -170,10 +170,9 @@ def add_members_to_portfolio(portfolio): def add_task_orders_to_portfolio(portfolio): - today = date.today() - future = today + timedelta(days=100) - yesterday = today - timedelta(days=1) - five_days = timedelta(days=5) + today = pendulum.today() + future = today.add(days=100) + yesterday = today.subtract(days=1) def build_pdf(): return {"filename": "sample_task_order.pdf", "object_name": str(uuid4())} @@ -192,13 +191,13 @@ def add_task_orders_to_portfolio(portfolio): clins = [ CLINFactory.build( - task_order=unsigned_to, start_date=(today - five_days), end_date=today + task_order=unsigned_to, start_date=today.subtract(days=5), end_date=today ), CLINFactory.build( - task_order=upcoming_to, start_date=(today + five_days), end_date=future + task_order=upcoming_to, start_date=today.add(days=5), end_date=future ), CLINFactory.build( - task_order=expired_to, start_date=(today - five_days), end_date=yesterday + task_order=expired_to, start_date=today.subtract(days=5), end_date=yesterday ), CLINFactory.build( task_order=active_to, diff --git a/tests/conftest.py b/tests/conftest.py index 03d07a12..26b2a1dc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,7 +13,7 @@ import tests.factories as factories from tests.mocks import PDF_FILENAME, PDF_FILENAME2 from tests.utils import FakeLogger, FakeNotificationSender -from datetime import datetime, timedelta +import pendulum from cryptography.hazmat.primitives.asymmetric import rsa from cryptography import x509 from cryptography.hazmat.backends import default_backend @@ -175,7 +175,7 @@ def extended_financial_verification_data(pdf_upload): return { "funding_type": "RDTE", "funding_type_other": "other", - "expiration_date": "1/1/{}".format(datetime.date.today().year + 1), + "expiration_date": "1/1/{}".format(pendulum.today().year + 1), "clin_0001": "50000", "clin_0003": "13000", "clin_1001": "30000", @@ -214,7 +214,6 @@ def make_x509(): if signer_key is None: signer_key = private_key - one_day = timedelta(1, 0, 0) public_key = private_key.public_key() builder = x509.CertificateBuilder() builder = builder.subject_name( @@ -227,8 +226,8 @@ def make_x509(): builder = builder.add_extension( x509.BasicConstraints(ca=True, path_length=None), critical=True ) - builder = builder.not_valid_before(datetime.today() - (one_day * 2)) - builder = builder.not_valid_after(datetime.today() + (one_day * 30)) + builder = builder.not_valid_before(pendulum.today().subtract(days=2)) + builder = builder.not_valid_after(pendulum.today().add(days=30)) builder = builder.serial_number(x509.random_serial_number()) builder = builder.public_key(public_key) certificate = builder.sign( @@ -249,13 +248,12 @@ def make_crl(): cn="ATAT", expired_serials=None, ): - one_day = timedelta(1, 0, 0) builder = x509.CertificateRevocationListBuilder() builder = builder.issuer_name( x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, cn)]) ) - last_update = datetime.today() + (one_day * last_update_days) - next_update = datetime.today() + (one_day * next_update_days) + last_update = pendulum.today().add(days=last_update_days) + next_update = pendulum.today().add(days=next_update_days) builder = builder.last_update(last_update) builder = builder.next_update(next_update) if expired_serials: diff --git a/tests/domain/test_applications.py b/tests/domain/test_applications.py index 02dd3124..37b167d0 100644 --- a/tests/domain/test_applications.py +++ b/tests/domain/test_applications.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +import pendulum import pytest from uuid import uuid4 @@ -200,8 +200,8 @@ def test_update_does_not_duplicate_names_within_portfolio(): def test_get_applications_pending_creation(): - now = datetime.now() - later = now + timedelta(minutes=30) + now = pendulum.now(tz="utc") + later = now.add(minutes=30) portfolio1 = PortfolioFactory.create(state="COMPLETED") app_ready = ApplicationFactory.create(portfolio=portfolio1) diff --git a/tests/domain/test_invitations.py b/tests/domain/test_invitations.py index 4d8073be..862f48ca 100644 --- a/tests/domain/test_invitations.py +++ b/tests/domain/test_invitations.py @@ -1,6 +1,6 @@ -import datetime import pytest import re +import pendulum from atst.domain.audit_log import AuditLog from atst.domain.invitations import ( @@ -53,7 +53,7 @@ def test_accept_expired_invitation(): portfolio = PortfolioFactory.create() role = PortfolioRoleFactory.create(portfolio=portfolio) increment = PortfolioInvitations.EXPIRATION_LIMIT_MINUTES + 1 - expiration_time = datetime.datetime.now() - datetime.timedelta(minutes=increment) + expiration_time = pendulum.now(tz="utc").subtract(minutes=increment) invite = PortfolioInvitationFactory.create( expiration_time=expiration_time, status=InvitationStatus.PENDING, diff --git a/tests/domain/test_task_orders.py b/tests/domain/test_task_orders.py index 93182df0..5999677a 100644 --- a/tests/domain/test_task_orders.py +++ b/tests/domain/test_task_orders.py @@ -1,5 +1,5 @@ import pytest -from datetime import date, datetime, timedelta +import pendulum from decimal import Decimal from atst.domain.exceptions import AlreadyExistsError @@ -15,16 +15,16 @@ def test_create_adds_clins(): { "jedi_clin_type": "JEDI_CLIN_1", "number": "12312", - "start_date": date(2020, 1, 1), - "end_date": date(2021, 1, 1), + "start_date": pendulum.date(2020, 1, 1), + "end_date": pendulum.date(2021, 1, 1), "obligated_amount": Decimal("5000"), "total_amount": Decimal("10000"), }, { "jedi_clin_type": "JEDI_CLIN_1", "number": "12312", - "start_date": date(2020, 1, 1), - "end_date": date(2021, 1, 1), + "start_date": pendulum.date(2020, 1, 1), + "end_date": pendulum.date(2021, 1, 1), "obligated_amount": Decimal("5000"), "total_amount": Decimal("10000"), }, @@ -45,16 +45,16 @@ def test_update_adds_clins(): { "jedi_clin_type": "JEDI_CLIN_1", "number": "12312", - "start_date": date(2020, 1, 1), - "end_date": date(2021, 1, 1), + "start_date": pendulum.date(2020, 1, 1), + "end_date": pendulum.date(2021, 1, 1), "obligated_amount": Decimal("5000"), "total_amount": Decimal("10000"), }, { "jedi_clin_type": "JEDI_CLIN_1", "number": "12312", - "start_date": date(2020, 1, 1), - "end_date": date(2021, 1, 1), + "start_date": pendulum.date(2020, 1, 1), + "end_date": pendulum.date(2021, 1, 1), "obligated_amount": Decimal("5000"), "total_amount": Decimal("10000"), }, @@ -77,16 +77,16 @@ def test_update_does_not_duplicate_clins(): { "jedi_clin_type": "JEDI_CLIN_1", "number": "123", - "start_date": date(2020, 1, 1), - "end_date": date(2021, 1, 1), + "start_date": pendulum.date(2020, 1, 1), + "end_date": pendulum.date(2021, 1, 1), "obligated_amount": Decimal("5000"), "total_amount": Decimal("10000"), }, { "jedi_clin_type": "JEDI_CLIN_1", "number": "111", - "start_date": date(2020, 1, 1), - "end_date": date(2021, 1, 1), + "start_date": pendulum.date(2020, 1, 1), + "end_date": pendulum.date(2021, 1, 1), "obligated_amount": Decimal("5000"), "total_amount": Decimal("10000"), }, @@ -114,9 +114,9 @@ def test_delete_task_order_with_clins(session): def test_task_order_sort_by_status(): - today = date.today() - yesterday = today - timedelta(days=1) - future = today + timedelta(days=100) + today = pendulum.today() + yesterday = today.subtract(days=1) + future = today.add(days=100) initial_to_list = [ # Draft @@ -184,12 +184,12 @@ def test_allows_alphanumeric_number(): def test_get_for_send_task_order_files(): new_to = TaskOrderFactory.create(create_clins=[{}]) updated_to = TaskOrderFactory.create( - create_clins=[{"last_sent_at": datetime(2020, 2, 1)}], - pdf_last_sent_at=datetime(2020, 1, 1), + create_clins=[{"last_sent_at": pendulum.datetime(2020, 2, 1)}], + pdf_last_sent_at=pendulum.datetime(2020, 1, 1), ) sent_to = TaskOrderFactory.create( - create_clins=[{"last_sent_at": datetime(2020, 1, 1)}], - pdf_last_sent_at=datetime(2020, 1, 1), + create_clins=[{"last_sent_at": pendulum.datetime(2020, 1, 1)}], + pdf_last_sent_at=pendulum.datetime(2020, 1, 1), ) updated_and_new_task_orders = TaskOrders.get_for_send_task_order_files() diff --git a/tests/domain/test_users.py b/tests/domain/test_users.py index 20bd8266..4f46f5bc 100644 --- a/tests/domain/test_users.py +++ b/tests/domain/test_users.py @@ -1,5 +1,4 @@ import pytest -from datetime import datetime from uuid import uuid4 from atst.domain.users import Users diff --git a/tests/factories.py b/tests/factories.py index aa0a986a..ee548d40 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -3,7 +3,7 @@ import random import string import factory from uuid import uuid4 -import datetime +import pendulum from atst.forms import data from atst.models import * @@ -56,8 +56,8 @@ def _random_date(year_min, year_max, operation): else: inc = random.randrange(year_min, year_max) - return datetime.date( - operation(datetime.date.today().year, inc), + return pendulum.date( + operation(pendulum.today().year, inc), random.randrange(1, 12), random.randrange(1, 28), ) @@ -99,8 +99,7 @@ class UserFactory(Base): citizenship = "United States" designation = "military" date_latest_training = factory.LazyFunction( - lambda: datetime.date.today() - + datetime.timedelta(days=-(random.randrange(1, 365))) + lambda: pendulum.today().add(days=-(random.randrange(1, 365))) ) @classmethod @@ -341,7 +340,7 @@ class CLINFactory(Base): task_order = factory.SubFactory(TaskOrderFactory) number = factory.LazyFunction(random_clin_number) - start_date = datetime.date.today() + start_date = pendulum.today() end_date = factory.LazyFunction(random_future_date) total_amount = factory.LazyFunction(lambda *args: random.randint(50000, 999999)) obligated_amount = factory.LazyFunction(lambda *args: random.randint(100, 50000)) diff --git a/tests/forms/test_task_order.py b/tests/forms/test_task_order.py index ae4fd3c6..7b87d649 100644 --- a/tests/forms/test_task_order.py +++ b/tests/forms/test_task_order.py @@ -1,4 +1,4 @@ -import datetime +import pendulum from dateutil.relativedelta import relativedelta from flask import current_app as app @@ -17,8 +17,8 @@ def test_clin_form_jedi_clin_type(): def test_clin_form_start_date_before_end_date(): - invalid_start = datetime.date(2020, 12, 12) - invalid_end = datetime.date(2020, 1, 1) + invalid_start = pendulum.date(2020, 12, 12) + invalid_end = pendulum.date(2020, 1, 1) invalid_clin = factories.CLINFactory.create( start_date=invalid_start, end_date=invalid_end ) @@ -28,8 +28,8 @@ def test_clin_form_start_date_before_end_date(): translate("forms.task_order.pop_errors.date_order") in clin_form.start_date.errors ) - valid_start = datetime.date(2020, 1, 1) - valid_end = datetime.date(2020, 12, 12) + valid_start = pendulum.date(2020, 1, 1) + valid_end = pendulum.date(2020, 12, 12) valid_clin = factories.CLINFactory.create( start_date=valid_start, end_date=valid_end ) @@ -81,8 +81,8 @@ def test_clin_form_obligated_greater_than_total(): invalid_clin = factories.CLINFactory.create( total_amount=0, obligated_amount=1, - start_date=datetime.date(2019, 9, 15), - end_date=datetime.date(2020, 9, 14), + start_date=pendulum.date(2019, 9, 15), + end_date=pendulum.date(2020, 9, 14), ) invalid_clin_form = CLINForm(obj=invalid_clin) assert not invalid_clin_form.validate() @@ -95,8 +95,8 @@ def test_clin_form_dollar_amounts_out_of_range(): invalid_clin = factories.CLINFactory.create( total_amount=-1, obligated_amount=1000000001, - start_date=datetime.date(2019, 9, 15), - end_date=datetime.date(2020, 9, 14), + start_date=pendulum.date(2019, 9, 15), + end_date=pendulum.date(2020, 9, 14), ) invalid_clin_form = CLINForm(obj=invalid_clin) assert not invalid_clin_form.validate() diff --git a/tests/models/test_application_role.py b/tests/models/test_application_role.py index 1807988e..0e3f4f86 100644 --- a/tests/models/test_application_role.py +++ b/tests/models/test_application_role.py @@ -1,4 +1,5 @@ import pytest +import pendulum from atst.domain.permission_sets import PermissionSets from atst.domain.environment_roles import EnvironmentRoles @@ -61,7 +62,7 @@ def test_environment_roles(): def test_display_status(): - yesterday = datetime.date.today() - datetime.timedelta(days=1) + yesterday = pendulum.today().subtract(days=1) expired_invite = ApplicationInvitationFactory.create(expiration_time=yesterday) assert expired_invite.role.display_status == "invite_expired" diff --git a/tests/models/test_portfolio.py b/tests/models/test_portfolio.py index 71e11bb3..ed6e2513 100644 --- a/tests/models/test_portfolio.py +++ b/tests/models/test_portfolio.py @@ -6,7 +6,6 @@ from tests.factories import ( random_future_date, random_past_date, ) -import datetime import pendulum from decimal import Decimal import pytest @@ -83,7 +82,7 @@ def test_funding_duration(session): portfolio=portfolio, signed_at=random_past_date(), create_clins=[ - {"start_date": datetime.datetime.now(), "end_date": funding_end_date,} + {"start_date": pendulum.now(tz="utc"), "end_date": funding_end_date,} ], ) @@ -106,7 +105,7 @@ def test_days_remaining(session): assert ( portfolio.days_to_funding_expiration - == (funding_end_date - datetime.date.today()).days + == (funding_end_date - pendulum.today()).days ) # empty portfolio @@ -121,8 +120,8 @@ def test_active_task_orders(session): signed_at=random_past_date(), create_clins=[ { - "start_date": datetime.date(2019, 1, 1), - "end_date": datetime.date(2019, 10, 31), + "start_date": pendulum.date(2019, 1, 1), + "end_date": pendulum.date(2019, 10, 31), } ], ) diff --git a/tests/models/test_portfolio_invitations.py b/tests/models/test_portfolio_invitations.py index faf9365d..dc033c4f 100644 --- a/tests/models/test_portfolio_invitations.py +++ b/tests/models/test_portfolio_invitations.py @@ -1,4 +1,4 @@ -import datetime +import pendulum from atst.models import InvitationStatus, PortfolioRoleStatus @@ -17,7 +17,7 @@ def test_expired_invite_is_not_revokable(): portfolio=portfolio, user=user, status=PortfolioRoleStatus.PENDING ) invite = PortfolioInvitationFactory.create( - expiration_time=datetime.datetime.now() - datetime.timedelta(minutes=60), + expiration_time=pendulum.now(tz="utc").subtract(minutes=60), role=portfolio_role, ) assert not invite.is_revokable diff --git a/tests/models/test_portfolio_role.py b/tests/models/test_portfolio_role.py index 5a12d55e..e3d08d26 100644 --- a/tests/models/test_portfolio_role.py +++ b/tests/models/test_portfolio_role.py @@ -1,5 +1,5 @@ import pytest -import datetime +import pendulum from atst.domain.environments import Environments from atst.domain.portfolios import Portfolios @@ -204,7 +204,7 @@ def test_status_when_invitation_is_expired(): PortfolioInvitationFactory.create( role=portfolio_role, status=InvitationStatus.PENDING, - expiration_time=datetime.datetime.now() - datetime.timedelta(seconds=1), + expiration_time=pendulum.now(tz="utc").subtract(seconds=1), ) assert portfolio_role.display_status == "invite_expired" diff --git a/tests/models/test_task_order.py b/tests/models/test_task_order.py index 27512bf1..0b03cbf8 100644 --- a/tests/models/test_task_order.py +++ b/tests/models/test_task_order.py @@ -1,6 +1,6 @@ from werkzeug.datastructures import FileStorage import pytest -from datetime import date +import pendulum from unittest.mock import patch, PropertyMock import pendulum @@ -13,11 +13,11 @@ from tests.mocks import PDF_FILENAME def test_period_of_performance_is_first_to_last_clin(): - start_date = date(2019, 6, 6) - end_date = date(2020, 6, 6) + start_date = pendulum.date(2019, 6, 6) + end_date = pendulum.date(2020, 6, 6) - intermediate_start_date = date(2019, 7, 1) - intermediate_end_date = date(2020, 3, 1) + intermediate_start_date = pendulum.date(2019, 7, 1) + intermediate_end_date = pendulum.date(2020, 3, 1) task_order = TaskOrderFactory.create( clins=[ diff --git a/tests/models/test_user.py b/tests/models/test_user.py index f4e29235..7dbeb6f4 100644 --- a/tests/models/test_user.py +++ b/tests/models/test_user.py @@ -1,6 +1,6 @@ import pytest from sqlalchemy.exc import InternalError -from datetime import datetime +import pendulum from atst.database import db from atst.domain.users import Users @@ -44,7 +44,7 @@ def test_deleted_application_roles_are_ignored(session): def test_does_not_log_user_update_when_updating_last_login(mock_logger): user = UserFactory.create() - user.last_login = datetime.now() + user.last_login = pendulum.now(tz="utc") db.session.add(user) db.session.commit() assert "Audit Event update" not in mock_logger.messages diff --git a/tests/routes/applications/test_settings.py b/tests/routes/applications/test_settings.py index 04d59f03..415dab4c 100644 --- a/tests/routes/applications/test_settings.py +++ b/tests/routes/applications/test_settings.py @@ -1,4 +1,4 @@ -import datetime +import pendulum import uuid from unittest.mock import Mock, patch @@ -613,7 +613,7 @@ def test_filter_environment_roles(): user = UserFactory.create() # need to set the time created to yesterday, otherwise the original invite and resent # invite have the same time_created and then we can't rely on time to order the invites - yesterday = datetime.date.today() - datetime.timedelta(days=1) + yesterday = pendulum.today().subtract(days=1) invite = ApplicationInvitationFactory.create( user=user, time_created=yesterday, email="original@example.com" ) diff --git a/tests/routes/portfolios/test_invitations.py b/tests/routes/portfolios/test_invitations.py index f54fca3b..f8872c08 100644 --- a/tests/routes/portfolios/test_invitations.py +++ b/tests/routes/portfolios/test_invitations.py @@ -1,4 +1,4 @@ -import datetime +import pendulum from unittest.mock import Mock from flask import url_for @@ -123,7 +123,7 @@ def test_user_accepts_expired_invite(client, user_session): user_id=user.id, role=ws_role, status=InvitationStatus.REJECTED_EXPIRED, - expiration_time=datetime.datetime.now() - datetime.timedelta(seconds=1), + expiration_time=pendulum.now(tz="utc").subtract(seconds=1), ) user_session(user) response = client.get( @@ -143,7 +143,7 @@ def test_revoke_invitation(client, user_session): user_id=user.id, role=ws_role, status=InvitationStatus.REJECTED_EXPIRED, - expiration_time=datetime.datetime.now() - datetime.timedelta(seconds=1), + expiration_time=pendulum.now(tz="utc").subtract(seconds=1), ) user_session(portfolio.owner) response = client.post( @@ -169,7 +169,7 @@ def test_user_can_only_revoke_invites_in_their_portfolio(client, user_session): user_id=user.id, role=portfolio_role, status=InvitationStatus.REJECTED_EXPIRED, - expiration_time=datetime.datetime.now() - datetime.timedelta(seconds=1), + expiration_time=pendulum.now(tz="utc").subtract(seconds=1), ) user_session(portfolio.owner) response = client.post( @@ -199,7 +199,7 @@ def test_user_can_only_resend_invites_in_their_portfolio( user_id=user.id, role=portfolio_role, status=InvitationStatus.REJECTED_EXPIRED, - expiration_time=datetime.datetime.now() - datetime.timedelta(seconds=1), + expiration_time=pendulum.now(tz="utc").subtract(seconds=1), ) user_session(portfolio.owner) response = client.post( diff --git a/tests/routes/task_orders/test_index.py b/tests/routes/task_orders/test_index.py index e9b81f7a..d3bd672d 100644 --- a/tests/routes/task_orders/test_index.py +++ b/tests/routes/task_orders/test_index.py @@ -1,7 +1,5 @@ -from datetime import date from flask import url_for import pytest -from datetime import timedelta, date from atst.domain.permission_sets import PermissionSets from atst.domain.task_orders import TaskOrders diff --git a/tests/routes/task_orders/test_new.py b/tests/routes/task_orders/test_new.py index 9929a992..3f27c19f 100644 --- a/tests/routes/task_orders/test_new.py +++ b/tests/routes/task_orders/test_new.py @@ -1,6 +1,6 @@ import pytest from flask import url_for, get_flashed_messages -from datetime import timedelta, date +import pendulum from uuid import uuid4 from atst.domain.task_orders import TaskOrders @@ -339,7 +339,7 @@ def test_task_orders_submit_task_order(client, user_session, task_order): ) assert response.status_code == 302 - active_start_date = date.today() - timedelta(days=1) + active_start_date = pendulum.today().subtract(days=1) active_task_order = TaskOrderFactory(portfolio=task_order.portfolio) CLINFactory(task_order=active_task_order, start_date=active_start_date) assert active_task_order.status == TaskOrderStatus.UNSIGNED @@ -348,7 +348,7 @@ def test_task_orders_submit_task_order(client, user_session, task_order): ) assert active_task_order.status == TaskOrderStatus.ACTIVE - upcoming_start_date = date.today() + timedelta(days=1) + upcoming_start_date = pendulum.today().add(days=1) upcoming_task_order = TaskOrderFactory(portfolio=task_order.portfolio) CLINFactory(task_order=upcoming_task_order, start_date=upcoming_start_date) assert upcoming_task_order.status == TaskOrderStatus.UNSIGNED diff --git a/tests/test_auth.py b/tests/test_auth.py index 8444c14f..88381218 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -3,11 +3,11 @@ from urllib.parse import urlparse import pytest from datetime import datetime +import pendulum from flask import session, url_for from cryptography.hazmat.primitives.serialization import Encoding from atst.domain.users import Users -from atst.domain.permission_sets import PermissionSets from atst.domain.exceptions import NotFoundError from atst.domain.authnid.crl import CRLInvalidException from atst.domain.auth import UNPROTECTED_ROUTES @@ -262,7 +262,7 @@ def test_error_on_invalid_crl(client, monkeypatch): def test_last_login_set_when_user_logs_in(client, monkeypatch): - last_login = datetime.now() + last_login = pendulum.now(tz="utc") user = UserFactory.create(last_login=last_login) monkeypatch.setattr( "atst.domain.authnid.AuthenticationContext.authenticate", lambda *args: True @@ -270,7 +270,7 @@ def test_last_login_set_when_user_logs_in(client, monkeypatch): monkeypatch.setattr( "atst.domain.authnid.AuthenticationContext.get_user", lambda *args: user ) - response = _login(client) + _login(client) assert session["last_login"] assert user.last_login > session["last_login"] assert isinstance(session["last_login"], datetime) From a13716e7edceb44f65d049620d0ddb6aa980f6f5 Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Mon, 17 Feb 2020 13:57:56 -0500 Subject: [PATCH 4/8] update copy --- translations.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/translations.yaml b/translations.yaml index adee11af..af76c602 100644 --- a/translations.yaml +++ b/translations.yaml @@ -506,12 +506,12 @@ portfolios: archive_button: Delete member reports: days_remaining: - header: Days Remaining + header: Days remaining toolip: Days remaining are the days of funding remaining in the portfolio. duration: - header: Funding Duration + header: Funding duration tooltip: Funding duration is the period of time that there is a valid task order funding the portfolio. - estimate_warning: Reports displayed in JEDI are estimates and not a system of record. + estimate_warning: Reports displayed in JEDI are estimates and not a system of record. To manage your costs, go to Azure by selecting the Login to Azure button above. total_value: header: Total Portfolio Value tooltip: Total portfolio value is all obligated funds for current and upcoming task orders in this portfolio. From 40be760fa08fb8df4ede66ca65156eae86e6f34d Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Mon, 17 Feb 2020 14:08:28 -0500 Subject: [PATCH 5/8] Update optional tag --- styles/elements/_inputs.scss | 6 ++++++ templates/components/multi_checkbox_input.html | 2 +- templates/components/options_input.html | 2 +- templates/components/text_input.html | 2 +- translations.yaml | 1 + 5 files changed, 10 insertions(+), 3 deletions(-) diff --git a/styles/elements/_inputs.scss b/styles/elements/_inputs.scss index 9e74ff50..c424b723 100644 --- a/styles/elements/_inputs.scss +++ b/styles/elements/_inputs.scss @@ -60,6 +60,12 @@ margin: ($gap * 2) 0; max-width: 75rem; + &-label-helper { + font-size: $small-font-size; + margin-left: $gap; + margin-right: $gap; + } + label { padding: 0 0 ($gap / 2) 0; margin: 0; diff --git a/templates/components/multi_checkbox_input.html b/templates/components/multi_checkbox_input.html index bfda1fa2..820efe3b 100644 --- a/templates/components/multi_checkbox_input.html +++ b/templates/components/multi_checkbox_input.html @@ -26,7 +26,7 @@
{{ field.label | striptags }} {% if optional %} - (optional) + {{ "common.optional" | translate }} {% endif %} {% if tooltip %}{{ Tooltip(tooltip) }}{% endif %} {% if not field.description %} diff --git a/templates/components/options_input.html b/templates/components/options_input.html index 405e7f23..47d21097 100644 --- a/templates/components/options_input.html +++ b/templates/components/options_input.html @@ -27,7 +27,7 @@
{{ field.label | striptags}} {% if optional %} - (optional) + {{ "common.optional" | translate }} {% endif %} {% if tooltip %}{{ Tooltip(tooltip) }}{% endif %}
diff --git a/templates/components/text_input.html b/templates/components/text_input.html index 5bc24568..daad6514 100644 --- a/templates/components/text_input.html +++ b/templates/components/text_input.html @@ -40,7 +40,7 @@
{{ label }} {% if optional and showOptional %} - (optional) + {{ "common.optional" | translate }} {% endif %} {% if tooltip and not disabled %} {{ Tooltip(tooltip, tooltip_title) }} diff --git a/translations.yaml b/translations.yaml index af76c602..4bb463cc 100644 --- a/translations.yaml +++ b/translations.yaml @@ -51,6 +51,7 @@ common: lorem: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. name: Name next: Next + optional: Optional previous: Previous save: Save save_changes: Save Changes From b6b093850aca562ff757ac2d43104e7fd99cf76e Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Tue, 11 Feb 2020 10:56:16 -0500 Subject: [PATCH 6/8] Add div with col class around the div.row that contains the header links. Remove display:table from the .col styling --- styles/components/_portfolio_layout.scss | 1 - templates/portfolios/header.html | 59 ++++++++++++------------ 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/styles/components/_portfolio_layout.scss b/styles/components/_portfolio_layout.scss index 06fecf63..1fb8a9b6 100644 --- a/styles/components/_portfolio_layout.scss +++ b/styles/components/_portfolio_layout.scss @@ -20,7 +20,6 @@ .col--grow { overflow: inherit; - display: table; min-height: 10rem; } diff --git a/templates/portfolios/header.html b/templates/portfolios/header.html index 71d9a550..5280ee1c 100644 --- a/templates/portfolios/header.html +++ b/templates/portfolios/header.html @@ -17,36 +17,37 @@

{{ portfolio.name }}

- From 541aa965fb0a0778ccde62bce75fded199075056 Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Tue, 11 Feb 2020 11:08:35 -0500 Subject: [PATCH 7/8] Center portfolio name vertically --- styles/components/_portfolio_layout.scss | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/styles/components/_portfolio_layout.scss b/styles/components/_portfolio_layout.scss index 1fb8a9b6..e9af6d5e 100644 --- a/styles/components/_portfolio_layout.scss +++ b/styles/components/_portfolio_layout.scss @@ -20,13 +20,11 @@ .col--grow { overflow: inherit; - min-height: 10rem; + align-self: center; } &__name { @include h1; - display: table-cell; - vertical-align: middle; h1 { margin: 0; From 9e501c106754ac727240d4c95aace28984ea54fb Mon Sep 17 00:00:00 2001 From: dandds Date: Tue, 18 Feb 2020 11:55:19 -0500 Subject: [PATCH 8/8] Update Azure CLI login to container registry. The Azure CLI now returns an exit code of "1" if there are any warnings when logging into a container registry. Since our longterm goal is to move away from CircleCI, I chose to fix this in-place rather than integrate a credentials store or pass more config values to a `docker login` command. Instead, this just grep the output from the Azure CLI command to ensure it succeeded. --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 1bb49bb3..c28b28a7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -103,7 +103,7 @@ commands: --password $AZURE_SP_PASSWORD \ --username $AZURE_SP echo "Successfully logged in to Azure CLI." - az acr login --name $AZURE_REGISTRY + az acr login --name $AZURE_REGISTRY | grep "Succeeded" - run: name: Install kubectl command: |