From e32bad0d30b75879f321a22380f2998254c7f976 Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Wed, 4 Dec 2019 17:01:22 -0500 Subject: [PATCH 01/14] Display TOs grouped by status --- atst/domain/task_orders.py | 12 ++-- atst/models/task_order.py | 7 +- atst/routes/task_orders/index.py | 12 +--- js/index.js | 4 +- js/mixins/toggle.js | 1 + templates/task_orders/index.html | 5 +- tests/domain/test_task_orders.py | 115 +++++++++++++------------------ 7 files changed, 65 insertions(+), 91 deletions(-) diff --git a/atst/domain/task_orders.py b/atst/domain/task_orders.py index 0c67e1d4..eceb01c3 100644 --- a/atst/domain/task_orders.py +++ b/atst/domain/task_orders.py @@ -64,10 +64,14 @@ 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 = {} + for status in SORT_ORDERING: + by_status[status] = [] + + for task_order in task_orders: + by_status[task_order.status].append(task_order) + return by_status @classmethod diff --git a/atst/models/task_order.py b/atst/models/task_order.py index 85bf363a..8bae6a7b 100644 --- a/atst/models/task_order.py +++ b/atst/models/task_order.py @@ -20,12 +20,7 @@ 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): diff --git a/atst/routes/task_orders/index.py b/atst/routes/task_orders/index.py index a58ea8da..4c0adc07 100644 --- a/atst/routes/task_orders/index.py +++ b/atst/routes/task_orders/index.py @@ -28,14 +28,8 @@ 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", - } + 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, label_colors=label_colors + "task_orders/index.html", task_orders=task_orders ) 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/templates/task_orders/index.html b/templates/task_orders/index.html index 0c319403..9e867bde 100644 --- a/templates/task_orders/index.html +++ b/templates/task_orders/index.html @@ -13,7 +13,6 @@ {% 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 %} @@ -63,7 +62,9 @@
{% if task_orders %} - {{ TaskOrderList(task_orders) }} + {% for status, to_list in task_orders.items() %} + {{ TaskOrderList(to_list, status.value) }} + {% endfor %} {% 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..d2005c50 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[Status.DRAFT]) == 3 + assert len(sorted_by_status[Status.ACTIVE]) == 1 + assert len(sorted_by_status[Status.UPCOMING]) == 1 + assert len(sorted_by_status[Status.EXPIRED]) == 2 + assert len(sorted_by_status[Status.UNSIGNED]) == 1 + assert list(sorted_by_status.keys()) == SORT_ORDERING From e79b1d1524c6b42855e048827006bce52420a205 Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Thu, 5 Dec 2019 16:22:20 -0500 Subject: [PATCH 02/14] Create AccordionList macro and vue component to collapse multiple accordion components --- js/components/accordion.js | 6 ++++++ js/components/accordion_list.js | 16 ++++++++++++++++ styles/elements/_accordions.scss | 4 ++++ templates/components/accordion_list.html | 11 +++++++++++ templates/task_orders/index.html | 9 ++++++--- 5 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 js/components/accordion_list.js create mode 100644 templates/components/accordion_list.html diff --git a/js/components/accordion.js b/js/components/accordion.js index d281a9e7..2de9315d 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..ae0042fa --- /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/styles/elements/_accordions.scss b/styles/elements/_accordions.scss index a6e61692..75848f7a 100644 --- a/styles/elements/_accordions.scss +++ b/styles/elements/_accordions.scss @@ -47,4 +47,8 @@ } } } + + &-list { + max-width: $max-panel-width; + } } diff --git a/templates/components/accordion_list.html b/templates/components/accordion_list.html new file mode 100644 index 00000000..dab63e3d --- /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 9e867bde..c2a1f4f5 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 %} @@ -62,9 +63,11 @@
{% if task_orders %} - {% for status, to_list in task_orders.items() %} - {{ TaskOrderList(to_list, status.value) }} - {% endfor %} + {% call AccordionList() %} + {% for status, to_list in task_orders.items() %} + {{ TaskOrderList(to_list, status.value) }} + {% endfor %} + {% endcall %} {% else %} {{ EmptyState( header="task_orders.empty_state.header"|translate, From 3fdde78531ebccb4249827774be4a1a49b432f21 Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Thu, 5 Dec 2019 16:27:25 -0500 Subject: [PATCH 03/14] Set default text for TOs without a number --- templates/task_orders/index.html | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/templates/task_orders/index.html b/templates/task_orders/index.html index c2a1f4f5..1ef4e55b 100644 --- a/templates/task_orders/index.html +++ b/templates/task_orders/index.html @@ -17,8 +17,15 @@
{% 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" ) }}

+

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

From ac8dd662d17b7f890987cdf202e4d8d11a188d81 Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Fri, 6 Dec 2019 16:26:09 -0500 Subject: [PATCH 04/14] Fake task order's expended funds, default task order start and end date to None, fix how task orders are sorted by status --- .secrets.baseline | 4 ++-- atst/domain/task_orders.py | 4 ++-- atst/models/task_order.py | 21 +++++++++++++++++---- atst/routes/task_orders/index.py | 4 +--- templates/task_orders/index.html | 4 ++-- tests/domain/test_task_orders.py | 12 ++++++------ tests/routes/task_orders/test_index.py | 4 +++- tests/routes/task_orders/test_new.py | 25 ++++++++++++++++++------- 8 files changed, 51 insertions(+), 27 deletions(-) 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 eceb01c3..6a4f66a0 100644 --- a/atst/domain/task_orders.py +++ b/atst/domain/task_orders.py @@ -67,10 +67,10 @@ class TaskOrders(BaseDomainClass): def sort_by_status(cls, task_orders): by_status = {} for status in SORT_ORDERING: - by_status[status] = [] + by_status[status.value] = [] for task_order in task_orders: - by_status[task_order.status].append(task_order) + by_status[task_order.display_status].append(task_order) return by_status diff --git a/atst/models/task_order.py b/atst/models/task_order.py index 8bae6a7b..aa925878 100644 --- a/atst/models/task_order.py +++ b/atst/models/task_order.py @@ -1,10 +1,12 @@ from datetime import timedelta from enum import Enum +import random from sqlalchemy import Column, DateTime, ForeignKey, String from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.orm import relationship +from atst.domain.csp.reports import MockReportingProvider from atst.models.base import Base import atst.models.types as types import atst.models.mixins as mixins @@ -20,7 +22,13 @@ class Status(Enum): UNSIGNED = "Not signed" -SORT_ORDERING = [Status.ACTIVE, Status.DRAFT, Status.UPCOMING, Status.EXPIRED, Status.UNSIGNED] +SORT_ORDERING = [ + Status.ACTIVE, + Status.DRAFT, + Status.UPCOMING, + Status.EXPIRED, + Status.UNSIGNED, +] class TaskOrder(Base, mixins.TimestampsMixin): @@ -126,12 +134,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): @@ -165,6 +172,12 @@ 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 + percentage_spent = random.randrange(50, 100) + return (self.total_obligated_funds * percentage_spent) / 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 4c0adc07..5e2a778e 100644 --- a/atst/routes/task_orders/index.py +++ b/atst/routes/task_orders/index.py @@ -30,6 +30,4 @@ def portfolio_funding(portfolio_id): portfolio = Portfolios.get(g.current_user, portfolio_id) 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 - ) + return render_template("task_orders/index.html", task_orders=task_orders) diff --git a/templates/task_orders/index.html b/templates/task_orders/index.html index 1ef4e55b..441977a5 100644 --- a/templates/task_orders/index.html +++ b/templates/task_orders/index.html @@ -47,7 +47,7 @@
Total Expended
-

$0

+

{{ task_order.invoiced_funds | dollars }}

@@ -72,7 +72,7 @@ {% if task_orders %} {% call AccordionList() %} {% for status, to_list in task_orders.items() %} - {{ TaskOrderList(to_list, status.value) }} + {{ TaskOrderList(to_list, status) }} {% endfor %} {% endcall %} {% else %} diff --git a/tests/domain/test_task_orders.py b/tests/domain/test_task_orders.py index d2005c50..acc2d158 100644 --- a/tests/domain/test_task_orders.py +++ b/tests/domain/test_task_orders.py @@ -148,9 +148,9 @@ def test_task_order_sort_by_status(): ] sorted_by_status = TaskOrders.sort_by_status(initial_to_list) - assert len(sorted_by_status[Status.DRAFT]) == 3 - assert len(sorted_by_status[Status.ACTIVE]) == 1 - assert len(sorted_by_status[Status.UPCOMING]) == 1 - assert len(sorted_by_status[Status.EXPIRED]) == 2 - assert len(sorted_by_status[Status.UNSIGNED]) == 1 - assert list(sorted_by_status.keys()) == SORT_ORDERING + 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 From e772a4b84bfb5cc859a9bb0054a2705e6626f2cf Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Tue, 10 Dec 2019 10:58:55 -0500 Subject: [PATCH 05/14] Use AccordionList macro on applications index page, remove duped class on task order index page --- styles/elements/_accordions.scss | 4 ++++ templates/applications/index.html | 5 +++-- templates/components/accordion_list.html | 4 ++-- templates/task_orders/index.html | 2 +- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/styles/elements/_accordions.scss b/styles/elements/_accordions.scss index 75848f7a..4f56e9bb 100644 --- a/styles/elements/_accordions.scss +++ b/styles/elements/_accordions.scss @@ -50,5 +50,9 @@ &-list { max-width: $max-panel-width; + + &__collapse { + cursor: pointer; + } } } 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 index dab63e3d..216e51b1 100644 --- a/templates/components/accordion_list.html +++ b/templates/components/accordion_list.html @@ -1,8 +1,8 @@ {% macro AccordionList() %} -
+
{{ caller() }} diff --git a/templates/task_orders/index.html b/templates/task_orders/index.html index 441977a5..b9fed6da 100644 --- a/templates/task_orders/index.html +++ b/templates/task_orders/index.html @@ -14,7 +14,7 @@ {% macro TaskOrderList(task_orders, status) %} -
+
{% call Accordion(title=status, id=status, heading_tag="h4") %} {% for task_order in task_orders %} {% set to_number %} From b49208ca57f09757362507fb78d3d43ad1a30b83 Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Tue, 10 Dec 2019 11:30:05 -0500 Subject: [PATCH 06/14] Remove unused imports and formatting, fake TaskOrder.invoiced_funds with hard coded percentage instead of random number --- atst/models/task_order.py | 6 +----- atst/routes/task_orders/index.py | 1 - js/components/accordion.js | 4 ++-- js/components/accordion_list.js | 6 +++--- 4 files changed, 6 insertions(+), 11 deletions(-) diff --git a/atst/models/task_order.py b/atst/models/task_order.py index aa925878..926824b9 100644 --- a/atst/models/task_order.py +++ b/atst/models/task_order.py @@ -1,12 +1,9 @@ -from datetime import timedelta from enum import Enum -import random from sqlalchemy import Column, DateTime, ForeignKey, String from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.orm import relationship -from atst.domain.csp.reports import MockReportingProvider from atst.models.base import Base import atst.models.types as types import atst.models.mixins as mixins @@ -175,8 +172,7 @@ class TaskOrder(Base, mixins.TimestampsMixin): @property def invoiced_funds(self): # TODO: implement this using reporting data from the CSP - percentage_spent = random.randrange(50, 100) - return (self.total_obligated_funds * percentage_spent) / 100 + return self.total_obligated_funds * 0.75 @property def display_status(self): diff --git a/atst/routes/task_orders/index.py b/atst/routes/task_orders/index.py index 5e2a778e..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") diff --git a/js/components/accordion.js b/js/components/accordion.js index 2de9315d..b96e1b80 100644 --- a/js/components/accordion.js +++ b/js/components/accordion.js @@ -15,6 +15,6 @@ export default { methods: { collapse: function() { this.isVisible = false - } - } + }, + }, } diff --git a/js/components/accordion_list.js b/js/components/accordion_list.js index ae0042fa..41e11d33 100644 --- a/js/components/accordion_list.js +++ b/js/components/accordion_list.js @@ -4,13 +4,13 @@ export default { name: 'accordion-list', components: { - Accordion + Accordion, }, methods: { handleClick: function(e) { e.preventDefault() this.$children.forEach(el => el.collapse()) - } - } + }, + }, } From 2fe7d4bb5bd93985c59913c292c26d8cbec405c5 Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Tue, 10 Dec 2019 11:31:20 -0500 Subject: [PATCH 07/14] Removed unused styling --- styles/sections/_task_order.scss | 15 --------------- 1 file changed, 15 deletions(-) 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; From 4d7af1bab468b9d8a0127589dde7b2fade2d4314 Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Thu, 12 Dec 2019 14:57:49 -0500 Subject: [PATCH 08/14] Remove extra details from expired TO display, small refactors in TO methods --- atst/domain/task_orders.py | 4 +-- atst/models/task_order.py | 2 +- templates/task_orders/index.html | 48 +++++++++++++++++--------------- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/atst/domain/task_orders.py b/atst/domain/task_orders.py index 6a4f66a0..24acdee7 100644 --- a/atst/domain/task_orders.py +++ b/atst/domain/task_orders.py @@ -65,9 +65,7 @@ class TaskOrders(BaseDomainClass): @classmethod def sort_by_status(cls, task_orders): - by_status = {} - for status in SORT_ORDERING: - by_status[status.value] = [] + by_status = {status.value: [] for status in SORT_ORDERING} for task_order in task_orders: by_status[task_order.display_status].append(task_order) diff --git a/atst/models/task_order.py b/atst/models/task_order.py index 926824b9..64693831 100644 --- a/atst/models/task_order.py +++ b/atst/models/task_order.py @@ -172,7 +172,7 @@ class TaskOrder(Base, mixins.TimestampsMixin): @property def invoiced_funds(self): # TODO: implement this using reporting data from the CSP - return self.total_obligated_funds * 0.75 + return self.total_obligated_funds * 75 / 100 @property def display_status(self): diff --git a/templates/task_orders/index.html b/templates/task_orders/index.html index b9fed6da..dfabbd02 100644 --- a/templates/task_orders/index.html +++ b/templates/task_orders/index.html @@ -26,30 +26,32 @@ {% endset %}

{{ to_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") }} -

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

{{ task_order.invoiced_funds | dollars }}

-
-
+ {%- endif %}
{% endfor %} {% endcall %} From 2801e074540b35eda1e28cc601667c58e8771fed Mon Sep 17 00:00:00 2001 From: tomdds Date: Mon, 9 Dec 2019 13:58:24 -0500 Subject: [PATCH 09/14] Add Azure Management Group Dependency --- Pipfile | 1 + Pipfile.lock | 177 ++++++++++++++++++++++++++------------------------- 2 files changed, 91 insertions(+), 87 deletions(-) diff --git a/Pipfile b/Pipfile index 6456afb1..edc8bbfc 100644 --- a/Pipfile +++ b/Pipfile @@ -29,6 +29,7 @@ azure-mgmt-subscription = "*" azure-graphrbac = "*" msrestazure = "*" azure-mgmt-authorization = "*" +azure-mgmt-managementgroups = "*" [dev-packages] bandit = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 9c5233bb..80f6fc96 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "6d2ab855267daac877ae7464de9dba5b62b7d89288992f87d8fc6ff0c0d2520f" + "sha256": "c2b19c436646705ea3bf4df8c35c2833083f048da37fc619e66f7236153607c5" }, "pipfile-spec": 6, "requires": { @@ -39,11 +39,11 @@ }, "apache-libcloud": { "hashes": [ - "sha256:201751f738109f25d58dcdfb5804e17216e0dc8f68b522e9e26ac16e0b9ff2ea", - "sha256:40215db1bd489d17dc1abfdb289d7f035313c7297b6a7462c79d8287cbbeae91" + "sha256:9bc5cd5c32151bb7a04a7c7de0be9b4a4b8271e348ac91dd79eaaeeae627115f", + "sha256:fcc165f2cc2db9a379c6d3a17b3beb9081bb64ba5c0bf7bbb58da864810092f0" ], "index": "pypi", - "version": "==2.6.0" + "version": "==2.6.1" }, "azure-common": { "hashes": [ @@ -68,6 +68,14 @@ "index": "pypi", "version": "==0.60.0" }, + "azure-mgmt-managementgroups": { + "hashes": [ + "sha256:3d5237947458dc94b4a392141174b1c1258d26611241ee104e9006d1d798f682", + "sha256:8194ee6274df865eccd1ed9d385ea625aeba9b8058b9e4fdf547f5207271a775" + ], + "index": "pypi", + "version": "==0.2.0" + }, "azure-mgmt-subscription": { "hashes": [ "sha256:504b4c42ba859070c3c50637ec07ca36aca600e613fcccaa398db22822fe21f1", @@ -117,10 +125,10 @@ }, "certifi": { "hashes": [ - "sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50", - "sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef" + "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3", + "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f" ], - "version": "==2019.9.11" + "version": "==2019.11.28" }, "cffi": { "hashes": [ @@ -248,10 +256,10 @@ }, "importlib-metadata": { "hashes": [ - "sha256:aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26", - "sha256:d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af" + "sha256:b044f07694ef14a6683b097ba56bd081dbc7cdc7c7fe46011e499dfecc082f21", + "sha256:e6ac600a142cf2db707b1998382cc7fc3b02befb7273876e01b8ad10b9652742" ], - "version": "==0.23" + "version": "==1.1.0" }, "isodate": { "hashes": [ @@ -330,10 +338,10 @@ }, "more-itertools": { "hashes": [ - "sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832", - "sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4" + "sha256:53ff73f186307d9c8ef17a9600309154a6ae27f25579e80af4db8f047ba14bc2", + "sha256:a0ea684c39bc4315ba7aae406596ef191fd84f873d2d2751f84d64e81a7a2d45" ], - "version": "==7.2.0" + "version": "==8.0.0" }, "msrest": { "hashes": [ @@ -422,11 +430,11 @@ }, "pyopenssl": { "hashes": [ - "sha256:aeca66338f6de19d1aa46ed634c3b9ae519a64b458f8468aec688e7e3c20f200", - "sha256:c727930ad54b10fc157015014b666f2d8b41f70c0d03e83ab67624fd3dd5d1e6" + "sha256:621880965a720b8ece2f1b2f54ea2071966ab00e2970ad2ce11d596102063504", + "sha256:9a24494b2602aaf402be5c9e30a0b82d4a5c67528fe8fb475e3f3bc00dd69507" ], "index": "pypi", - "version": "==19.0.0" + "version": "==19.1.0" }, "python-dateutil": { "hashes": [ @@ -459,22 +467,20 @@ }, "pyyaml": { "hashes": [ - "sha256:0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9", - "sha256:01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4", - "sha256:5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8", - "sha256:5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696", - "sha256:7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34", - "sha256:7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9", - "sha256:87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73", - "sha256:9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299", - "sha256:a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b", - "sha256:b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae", - "sha256:b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681", - "sha256:bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41", - "sha256:f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8" + "sha256:0e7f69397d53155e55d10ff68fdfb2cf630a35e6daf65cf0bdeaf04f127c09dc", + "sha256:2e9f0b7c5914367b0916c3c104a024bb68f269a486b9d04a2e8ac6f6597b7803", + "sha256:35ace9b4147848cafac3db142795ee42deebe9d0dad885ce643928e88daebdcc", + "sha256:38a4f0d114101c58c0f3a88aeaa44d63efd588845c5a2df5290b73db8f246d15", + "sha256:483eb6a33b671408c8529106df3707270bfacb2447bf8ad856a4b4f57f6e3075", + "sha256:4b6be5edb9f6bb73680f5bf4ee08ff25416d1400fbd4535fe0069b2994da07cd", + "sha256:7f38e35c00e160db592091751d385cd7b3046d6d51f578b29943225178257b31", + "sha256:8100c896ecb361794d8bfdb9c11fce618c7cf83d624d73d5ab38aef3bc82d43f", + "sha256:c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c", + "sha256:e4c015484ff0ff197564917b4b4246ca03f411b9bd7f16e02a2f586eb48b6d04", + "sha256:ebc4ed52dcc93eeebeae5cf5deb2ae4347b3a81c3fa12b0b8c976544829396a4" ], "index": "pypi", - "version": "==5.1.2" + "version": "==5.2" }, "redis": { "hashes": [ @@ -650,10 +656,10 @@ }, "certifi": { "hashes": [ - "sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50", - "sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef" + "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3", + "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f" ], - "version": "==2019.9.11" + "version": "==2019.11.28" }, "chardet": { "hashes": [ @@ -785,10 +791,10 @@ }, "importlib-metadata": { "hashes": [ - "sha256:aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26", - "sha256:d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af" + "sha256:b044f07694ef14a6683b097ba56bd081dbc7cdc7c7fe46011e499dfecc082f21", + "sha256:e6ac600a142cf2db707b1998382cc7fc3b02befb7273876e01b8ad10b9652742" ], - "version": "==0.23" + "version": "==1.1.0" }, "ipdb": { "hashes": [ @@ -799,11 +805,11 @@ }, "ipython": { "hashes": [ - "sha256:dfd303b270b7b5232b3d08bd30ec6fd685d8a58cabd54055e3d69d8f029f7280", - "sha256:ed7ebe1cba899c1c3ccad6f7f1c2d2369464cc77dba8eebc65e2043e19cda995" + "sha256:c66c7e27239855828a764b1e8fc72c24a6f4498a2637572094a78c5551fb9d51", + "sha256:f186b01b36609e0c5d0de27c7ef8e80c990c70478f8c880863004b3489a9030e" ], "index": "pypi", - "version": "==7.9.0" + "version": "==7.10.1" }, "ipython-genutils": { "hashes": [ @@ -908,30 +914,30 @@ }, "more-itertools": { "hashes": [ - "sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832", - "sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4" + "sha256:53ff73f186307d9c8ef17a9600309154a6ae27f25579e80af4db8f047ba14bc2", + "sha256:a0ea684c39bc4315ba7aae406596ef191fd84f873d2d2751f84d64e81a7a2d45" ], - "version": "==7.2.0" + "version": "==8.0.0" }, "mypy": { "hashes": [ - "sha256:1521c186a3d200c399bd5573c828ea2db1362af7209b2adb1bb8532cea2fb36f", - "sha256:31a046ab040a84a0fc38bc93694876398e62bc9f35eca8ccbf6418b7297f4c00", - "sha256:3b1a411909c84b2ae9b8283b58b48541654b918e8513c20a400bb946aa9111ae", - "sha256:48c8bc99380575deb39f5d3400ebb6a8a1cb5cc669bbba4d3bb30f904e0a0e7d", - "sha256:540c9caa57a22d0d5d3c69047cc9dd0094d49782603eb03069821b41f9e970e9", - "sha256:672e418425d957e276c291930a3921b4a6413204f53fe7c37cad7bc57b9a3391", - "sha256:6ed3b9b3fdc7193ea7aca6f3c20549b377a56f28769783a8f27191903a54170f", - "sha256:9371290aa2cad5ad133e4cdc43892778efd13293406f7340b9ffe99d5ec7c1d9", - "sha256:ace6ac1d0f87d4072f05b5468a084a45b4eda970e4d26704f201e06d47ab2990", - "sha256:b428f883d2b3fe1d052c630642cc6afddd07d5cd7873da948644508be3b9d4a7", - "sha256:d5bf0e6ec8ba346a2cf35cb55bf4adfddbc6b6576fcc9e10863daa523e418dbb", - "sha256:d7574e283f83c08501607586b3167728c58e8442947e027d2d4c7dcd6d82f453", - "sha256:dc889c84241a857c263a2b1cd1121507db7d5b5f5e87e77147097230f374d10b", - "sha256:f4748697b349f373002656bf32fede706a0e713d67bfdcf04edf39b1f61d46eb" + "sha256:02d9bdd3398b636723ecb6c5cfe9773025a9ab7f34612c1cde5c7f2292e2d768", + "sha256:088f758a50af31cf8b42688118077292370c90c89232c783ba7979f39ea16646", + "sha256:28e9fbc96d13397a7ddb7fad7b14f373f91b5cff538e0772e77c270468df083c", + "sha256:30e123b24931f02c5d99307406658ac8f9cd6746f0d45a3dcac2fe5fbdd60939", + "sha256:3294821b5840d51a3cd7a2bb63b40fc3f901f6a3cfb3c6046570749c4c7ef279", + "sha256:41696a7d912ce16fdc7c141d87e8db5144d4be664a0c699a2b417d393994b0c2", + "sha256:4f42675fa278f3913340bb8c3371d191319704437758d7c4a8440346c293ecb2", + "sha256:54d205ccce6ed930a8a2ccf48404896d456e8b87812e491cb907a355b1a9c640", + "sha256:6992133c95a2847d309b4b0c899d7054adc60481df6f6b52bb7dee3d5fd157f7", + "sha256:6ecbd0e8e371333027abca0922b0c2c632a5b4739a0c61ffbd0733391e39144c", + "sha256:83fa87f556e60782c0fc3df1b37b7b4a840314ba1ac27f3e1a1e10cb37c89c17", + "sha256:c87ac7233c629f305602f563db07f5221950fe34fe30af072ac838fa85395f78", + "sha256:de9ec8dba773b78c49e7bec9a35c9b6fc5235682ad1fc2105752ae7c22f4b931", + "sha256:f385a0accf353ca1bca4bbf473b9d83ed18d923fdb809d3a70a385da23e25b6a" ], "index": "pypi", - "version": "==0.740" + "version": "==0.750" }, "mypy-extensions": { "hashes": [ @@ -961,10 +967,10 @@ }, "pbr": { "hashes": [ - "sha256:2c8e420cd4ed4cec4e7999ee47409e876af575d4c35a45840d59e8b5f3155ab8", - "sha256:b32c8ccaac7b1a20c0ce00ce317642e6cf231cf038f9875e0280e28af5bf7ac9" + "sha256:139d2625547dbfa5fb0b81daebb39601c478c21956dc57e2e07b74450a8c506b", + "sha256:61aa52a0f18b71c5cc58232d2cf8f8d09cd67fcad60b742a60124cb8d6951488" ], - "version": "==5.4.3" + "version": "==5.4.4" }, "pexpect": { "hashes": [ @@ -983,18 +989,17 @@ }, "pluggy": { "hashes": [ - "sha256:0db4b7601aae1d35b4a033282da476845aa19185c1e6964b25cf324b5e4ec3e6", - "sha256:fa5fa1622fa6dd5c030e9cad086fa19ef6a0cf6d7a2d12318e10cb49d6d68f34" + "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", + "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" ], - "version": "==0.13.0" + "version": "==0.13.1" }, "prompt-toolkit": { "hashes": [ - "sha256:46642344ce457641f28fc9d1c9ca939b63dadf8df128b86f1b9860e59c73a5e4", - "sha256:e7f8af9e3d70f514373bf41aa51bc33af12a6db3f71461ea47fea985defb2c31", - "sha256:f15af68f66e664eaa559d4ac8a928111eebd5feda0c11738b5998045224829db" + "sha256:0278d2f51b5ceba6ea8da39f76d15684e84c996b325475f6e5720edc584326a7", + "sha256:63daee79aa8366c8f1c637f1a4876b890da5fc92a19ebd2f7080ebacb901e990" ], - "version": "==2.0.10" + "version": "==3.0.2" }, "ptyprocess": { "hashes": [ @@ -1012,10 +1017,10 @@ }, "pygments": { "hashes": [ - "sha256:71e430bc85c88a430f000ac1d9b331d2407f681d6f6aec95e8bcfbc3df5b0127", - "sha256:881c4c157e45f30af185c1ffe8d549d48ac9127433f2c380c24b84572ad66297" + "sha256:2a3fe295e54a20164a9df49c75fa58526d3be48e14aceba6d6b1e8ac0bfd6f1b", + "sha256:98c8aa5a9f778fcd1026a17361ddaf7330d1b7c62ae97c3bb0ae73e0b9b6b0fe" ], - "version": "==2.4.2" + "version": "==2.5.2" }, "pylint": { "hashes": [ @@ -1058,11 +1063,11 @@ }, "pytest-mock": { "hashes": [ - "sha256:b3514caac35fe3f05555923eabd9546abce11571cc2ddf7d8615959d04f2c89e", - "sha256:ea502c3891599c26243a3a847ccf0b1d20556678c528f86c98e3cd6d40c5cf11" + "sha256:96a0cebc66e09930be2a15b03333d90b59584d3fb011924f81c14b50ee0afbba", + "sha256:e5381be2608e49547f5e47633c5f81241ebf6206d17ce516a7a18d5a917e3859" ], "index": "pypi", - "version": "==1.11.2" + "version": "==1.12.1" }, "pytest-watch": { "hashes": [ @@ -1080,22 +1085,20 @@ }, "pyyaml": { "hashes": [ - "sha256:0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9", - "sha256:01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4", - "sha256:5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8", - "sha256:5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696", - "sha256:7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34", - "sha256:7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9", - "sha256:87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73", - "sha256:9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299", - "sha256:a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b", - "sha256:b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae", - "sha256:b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681", - "sha256:bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41", - "sha256:f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8" + "sha256:0e7f69397d53155e55d10ff68fdfb2cf630a35e6daf65cf0bdeaf04f127c09dc", + "sha256:2e9f0b7c5914367b0916c3c104a024bb68f269a486b9d04a2e8ac6f6597b7803", + "sha256:35ace9b4147848cafac3db142795ee42deebe9d0dad885ce643928e88daebdcc", + "sha256:38a4f0d114101c58c0f3a88aeaa44d63efd588845c5a2df5290b73db8f246d15", + "sha256:483eb6a33b671408c8529106df3707270bfacb2447bf8ad856a4b4f57f6e3075", + "sha256:4b6be5edb9f6bb73680f5bf4ee08ff25416d1400fbd4535fe0069b2994da07cd", + "sha256:7f38e35c00e160db592091751d385cd7b3046d6d51f578b29943225178257b31", + "sha256:8100c896ecb361794d8bfdb9c11fce618c7cf83d624d73d5ab38aef3bc82d43f", + "sha256:c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c", + "sha256:e4c015484ff0ff197564917b4b4246ca03f411b9bd7f16e02a2f586eb48b6d04", + "sha256:ebc4ed52dcc93eeebeae5cf5deb2ae4347b3a81c3fa12b0b8c976544829396a4" ], "index": "pypi", - "version": "==5.1.2" + "version": "==5.2" }, "regex": { "hashes": [ From 8a1ed5b1936dd603d5c887b590012271a9568eaa Mon Sep 17 00:00:00 2001 From: tomdds Date: Mon, 9 Dec 2019 14:00:36 -0500 Subject: [PATCH 10/14] Sketch in Management Group integration for Azure Add mocks and real implementations for creating nested management groups that reflect the Portfolio->Application->Environment->Subscription hierarchy. --- atst/domain/csp/cloud.py | 124 ++++++++++++++++++++------- tests/domain/cloud/test_azure_csp.py | 60 ++++++++++++- tests/mock_azure.py | 13 ++- 3 files changed, 159 insertions(+), 38 deletions(-) diff --git a/atst/domain/csp/cloud.py b/atst/domain/csp/cloud.py index 4cd69d3c..2da48642 100644 --- a/atst/domain/csp/cloud.py +++ b/atst/domain/csp/cloud.py @@ -3,6 +3,7 @@ import re from uuid import uuid4 from atst.models.user import User +from atst.models.application import Application from atst.models.environment import Environment from atst.models.environment_role import EnvironmentRole @@ -399,13 +400,14 @@ REMOTE_ROOT_ROLE_DEF_ID = "/providers/Microsoft.Authorization/roleDefinitions/00 class AzureSDKProvider(object): def __init__(self): - from azure.mgmt import subscription, authorization + from azure.mgmt import subscription, authorization, managementgroups import azure.graphrbac as graphrbac import azure.common.credentials as credentials from msrestazure.azure_cloud import AZURE_PUBLIC_CLOUD self.subscription = subscription self.authorization = authorization + self.managementgroups = managementgroups self.graphrbac = graphrbac self.credentials = credentials # may change to a JEDI cloud @@ -428,42 +430,23 @@ class AzureCloudProvider(CloudProviderInterface): def create_environment( self, auth_credentials: Dict, user: User, environment: Environment ): + # since this operation would only occur within a tenant, should we source the tenant + # via lookup from environment once we've created the portfolio csp data schema + # something like this: + # environment_tenant = environment.application.portfolio.csp_data.get('tenant_id', None) + # though we'd probably source the whole credentials for these calls from the portfolio csp + # data, as it would have to be where we store the creds for the at-at user within the portfolio tenant + # credentials = self._get_credential_obj(environment.application.portfolio.csp_data.get_creds()) credentials = self._get_credential_obj(self._root_creds) - sub_client = self.sdk.subscription.SubscriptionClient(credentials) - display_name = f"{environment.application.name}_{environment.name}_{environment.id}" # proposed format + management_group_id = "?" # management group id chained from environment + parent_id = "?" # from environment.application - billing_profile_id = "?" # something chained from environment? - sku_id = AZURE_SKU_ID - # we want to set AT-AT as an owner here - # we could potentially associate subscriptions with "management groups" per DOD component - body = self.sdk.subscription.models.ModernSubscriptionCreationParameters( - display_name, - billing_profile_id, - sku_id, - # owner= + management_group = self._create_management_group( + credentials, management_group_id, display_name, parent_id, ) - # These 2 seem like something that might be worthwhile to allow tiebacks to - # TOs filed for the environment - billing_account_name = "?" - invoice_section_name = "?" - # We may also want to create billing sections in the enrollment account - sub_creation_operation = sub_client.subscription_factory.create_subscription( - billing_account_name, invoice_section_name, body - ) - - # the resulting object from this process is a link to the new subscription - # not a subscription model, so we'll have to unpack the ID - new_sub = sub_creation_operation.result() - - subscription_id = self._extract_subscription_id(new_sub.subscription_link) - if subscription_id: - return subscription_id - else: - # troublesome error, subscription should exist at this point - # but we just don't have a valid ID - pass + return management_group def create_atat_admin_user( self, auth_credentials: Dict, csp_environment_id: str @@ -502,6 +485,83 @@ class AzureCloudProvider(CloudProviderInterface): "role_name": role_assignment_id, } + def _create_application(self, auth_credentials: Dict, application: Application): + management_group_name = str(uuid4()) # can be anything, not just uuid + display_name = application.name # Does this need to be unique? + credentials = self._get_credential_obj(auth_credentials) + parent_id = "?" # application.portfolio.csp_details.management_group_id + + return self._create_management_group( + credentials, management_group_name, display_name, parent_id, + ) + + def _create_management_group( + self, credentials, management_group_id, display_name, parent_id=None, + ): + mgmgt_group_client = self.sdk.managementgroups.ManagementGroupsAPI(credentials) + create_parent_grp_info = self.sdk.managementgroups.models.CreateParentGroupInfo( + id=parent_id + ) + create_mgmt_grp_details = self.sdk.managementgroups.models.CreateManagementGroupDetails( + parent=create_parent_grp_info + ) + mgmt_grp_create = self.sdk.managementgroups.models.CreateManagementGroupRequest( + name=management_group_id, + display_name=display_name, + details=create_mgmt_grp_details, + ) + create_request = mgmgt_group_client.management_groups.create_or_update( + management_group_id, mgmt_grp_create + ) + + # result is a synchronous wait, might need to do a poll instead to handle first mgmt group create + # since we were told it could take 10+ minutes to complete, unless this handles that polling internally + return create_request.result() + + def _create_subscription( + self, + credentials, + display_name, + billing_profile_id, + sku_id, + management_group_id, + billing_account_name, + invoice_section_name, + ): + sub_client = self.sdk.subscription.SubscriptionClient(credentials) + + display_name = f"{environment.application.name}_{environment.name}_{environment.id}" # proposed format + billing_profile_id = "?" # where do we source this? + sku_id = AZURE_SKU_ID + # These 2 seem like something that might be worthwhile to allow tiebacks to + # TOs filed for the environment + billing_account_name = "?" # from TO? + invoice_section_name = "?" # from TO? + + body = self.sdk.subscription.models.ModernSubscriptionCreationParameters( + display_name=display_name, + billing_profile_id=billing_profile_id, + sku_id=sku_id, + management_group_id=management_group_id, + ) + + # We may also want to create billing sections in the enrollment account + sub_creation_operation = sub_client.subscription_factory.create_subscription( + billing_account_name, invoice_section_name, body + ) + + # the resulting object from this process is a link to the new subscription + # not a subscription model, so we'll have to unpack the ID + new_sub = sub_creation_operation.result() + + subscription_id = self._extract_subscription_id(new_sub.subscription_link) + if subscription_id: + return subscription_id + else: + # troublesome error, subscription should exist at this point + # but we just don't have a valid ID + pass + def _get_management_service_principal(self): # we really should be using graph.microsoft.com, but i'm getting # "expired token" errors for that diff --git a/tests/domain/cloud/test_azure_csp.py b/tests/domain/cloud/test_azure_csp.py index 19ad63c8..39c6655e 100644 --- a/tests/domain/cloud/test_azure_csp.py +++ b/tests/domain/cloud/test_azure_csp.py @@ -1,27 +1,81 @@ import pytest +from unittest.mock import Mock from uuid import uuid4 from atst.domain.csp.cloud import AzureCloudProvider from tests.mock_azure import mock_azure, AUTH_CREDENTIALS -from tests.factories import EnvironmentFactory +from tests.factories import EnvironmentFactory, ApplicationFactory -def test_create_environment_succeeds(mock_azure: AzureCloudProvider): +# TODO: Directly test create subscription, provide all args √ +# TODO: Test create environment (create management group with parent) +# TODO: Test create application (create manageemnt group with parent) +# Create reusable mock for mocking the management group calls for multiple services +# + + +def test_create_subscription_succeeds(mock_azure: AzureCloudProvider): environment = EnvironmentFactory.create() subscription_id = str(uuid4()) + credentials = mock_azure._get_credential_obj(AUTH_CREDENTIALS) + display_name = "Test Subscription" + billing_profile_id = str(uuid4()) + sku_id = str(uuid4()) + management_group_id = ( + environment.cloud_id # environment.csp_details.management_group_id? + ) + billing_account_name = ( + "?" # environment.application.portfilio.csp_details.billing_account.name? + ) + invoice_section_name = "?" # environment.name? or something specific to billing? + mock_azure.sdk.subscription.SubscriptionClient.return_value.subscription_factory.create_subscription.return_value.result.return_value.subscription_link = ( f"subscriptions/{subscription_id}" ) + result = mock_azure._create_subscription( + credentials, + display_name, + billing_profile_id, + sku_id, + management_group_id, + billing_account_name, + invoice_section_name, + ) + + assert result == subscription_id + + +def mock_management_group_create(mock_azure, spec_dict): + mock_azure.sdk.managementgroups.ManagementGroupsAPI.return_value.management_groups.create_or_update.return_value.result.return_value = Mock( + **spec_dict + ) + + +def test_create_environment_succeeds(mock_azure: AzureCloudProvider): + environment = EnvironmentFactory.create() + + mock_management_group_create(mock_azure, {"id": "Test Id"}) + result = mock_azure.create_environment( AUTH_CREDENTIALS, environment.creator, environment ) - assert result == subscription_id + assert result.id == "Test Id" + + +def test_create_application_succeeds(mock_azure: AzureCloudProvider): + application = ApplicationFactory.create() + + mock_management_group_create(mock_azure, {"id": "Test Id"}) + + result = mock_azure._create_application(AUTH_CREDENTIALS, application) + + assert result.id == "Test Id" def test_create_atat_admin_user_succeeds(mock_azure: AzureCloudProvider): diff --git a/tests/mock_azure.py b/tests/mock_azure.py index 34d0aa53..a360df64 100644 --- a/tests/mock_azure.py +++ b/tests/mock_azure.py @@ -10,9 +10,9 @@ AZURE_CONFIG = { } AUTH_CREDENTIALS = { - "CLIENT_ID": AZURE_CONFIG["AZURE_CLIENT_ID"], - "SECRET_KEY": AZURE_CONFIG["AZURE_SECRET_KEY"], - "TENANT_ID": AZURE_CONFIG["AZURE_TENANT_ID"], + "client_id": AZURE_CONFIG["AZURE_CLIENT_ID"], + "secret_key": AZURE_CONFIG["AZURE_SECRET_KEY"], + "tenant_id": AZURE_CONFIG["AZURE_TENANT_ID"], } @@ -28,6 +28,12 @@ def mock_authorization(): return Mock(spec=authorization) +def mock_managementgroups(): + from azure.mgmt import managementgroups + + return Mock(spec=managementgroups) + + def mock_graphrbac(): import azure.graphrbac as graphrbac @@ -46,6 +52,7 @@ class MockAzureSDK(object): self.subscription = mock_subscription() self.authorization = mock_authorization() + self.managementgroups = mock_managementgroups() self.graphrbac = mock_graphrbac() self.credentials = mock_credentials() # may change to a JEDI cloud From 2c2b69affea33f8c63194bd77f01c0bae8768307 Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Fri, 13 Dec 2019 10:54:00 -0500 Subject: [PATCH 11/14] Fix TO index blank states and number type issue --- atst/models/task_order.py | 3 +- atst/routes/task_orders/index.py | 5 +- templates/task_orders/index.html | 82 +++++++++++++++++--------------- translations.yaml | 2 + 4 files changed, 51 insertions(+), 41 deletions(-) diff --git a/atst/models/task_order.py b/atst/models/task_order.py index 64693831..7baef38d 100644 --- a/atst/models/task_order.py +++ b/atst/models/task_order.py @@ -1,4 +1,5 @@ from enum import Enum +from decimal import Decimal from sqlalchemy import Column, DateTime, ForeignKey, String from sqlalchemy.ext.hybrid import hybrid_property @@ -172,7 +173,7 @@ class TaskOrder(Base, mixins.TimestampsMixin): @property def invoiced_funds(self): # TODO: implement this using reporting data from the CSP - return self.total_obligated_funds * 75 / 100 + return self.total_obligated_funds * Decimal(0.75) @property def display_status(self): diff --git a/atst/routes/task_orders/index.py b/atst/routes/task_orders/index.py index a3d72cf7..e02780ca 100644 --- a/atst/routes/task_orders/index.py +++ b/atst/routes/task_orders/index.py @@ -28,5 +28,8 @@ def review_task_order(task_order_id): def portfolio_funding(portfolio_id): portfolio = Portfolios.get(g.current_user, portfolio_id) task_orders = TaskOrders.sort_by_status(portfolio.task_orders) + to_count = len(portfolio.task_orders) # TODO: Get expended amount from the CSP - return render_template("task_orders/index.html", task_orders=task_orders) + return render_template( + "task_orders/index.html", task_orders=task_orders, to_count=to_count + ) diff --git a/templates/task_orders/index.html b/templates/task_orders/index.html index dfabbd02..a272fade 100644 --- a/templates/task_orders/index.html +++ b/templates/task_orders/index.html @@ -15,45 +15,49 @@ {% macro TaskOrderList(task_orders, status) %}
- {% 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 %} -
-

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

+ {% call Accordion(title=("task_orders.status_list_title"|translate({'status': status})), id=status, heading_tag="h4") %} + {% if task_orders|length > 0 %} + {% for task_order in task_orders %} + {% set to_number %} + {% if task_order.number != "" %} + Task Order #{{ task_order.number }} + {% else %} + New Task Order + {% endif %} + {% endset %} +
+

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

{{ task_order.invoiced_funds | dollars }}

-
-
- {%- endif %} -
- {% endfor %} + {%- endif %} +
+ {% endfor %} + {% else %} + {{ "task_orders.status_empty_state" | translate({ 'status': status }) }} + {% endif %} {% endcall %}
{% endmacro %} @@ -71,7 +75,7 @@
- {% if task_orders %} + {% if to_count > 0 %} {% call AccordionList() %} {% for status, to_list in task_orders.items() %} {{ TaskOrderList(to_list, status) }} diff --git a/translations.yaml b/translations.yaml index d63b1a5e..cb6b5376 100644 --- a/translations.yaml +++ b/translations.yaml @@ -529,6 +529,8 @@ task_orders: team_title: Your team sign: digital_signature_description: I acknowledge that the uploaded task order contains the required KO signature. + status_empty_state: 'This Portfolio has no {status} Task Orders.' + status_list_title: '{status} Task Orders' JEDICLINType: JEDI_CLIN_1: 'IDIQ CLIN 0001 Unclassified IaaS/PaaS' JEDI_CLIN_2: 'IDIQ CLIN 0002 Classified IaaS/PaaS' From 8f94d9e6ec5cb68d3f1dcc94b3b06d95c6e9be94 Mon Sep 17 00:00:00 2001 From: dandds Date: Fri, 13 Dec 2019 06:20:38 -0500 Subject: [PATCH 12/14] Log any CSP errors that occur when disabling a user. When one user disables another's environment role in Azure, sometimes an exception will be raised. Since we catch the exception and display an error message to the user, we should also log the exception so that the error is traceable later. --- atst/routes/applications/settings.py | 4 ++- tests/routes/applications/test_settings.py | 39 ++++++++++++++++++++++ tests/utils.py | 3 ++ 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/atst/routes/applications/settings.py b/atst/routes/applications/settings.py index a4aea550..f2d252a9 100644 --- a/atst/routes/applications/settings.py +++ b/atst/routes/applications/settings.py @@ -20,6 +20,7 @@ from atst.domain.permission_sets import PermissionSets from atst.utils.flash import formatted_flash as flash from atst.utils.localization import translate from atst.jobs import send_mail +from atst.routes.errors import log_error def get_environments_obj_for_app(application): @@ -234,7 +235,8 @@ def handle_update_member(application_id, application_role_id, form_data): flash("application_member_updated", user_name=app_role.user_name) - except GeneralCSPException: + except GeneralCSPException as exc: + log_error(exc) flash( "application_member_update_error", user_name=app_role.user_name, ) diff --git a/tests/routes/applications/test_settings.py b/tests/routes/applications/test_settings.py index 4961065e..08c979ad 100644 --- a/tests/routes/applications/test_settings.py +++ b/tests/routes/applications/test_settings.py @@ -12,6 +12,7 @@ from atst.domain.application_roles import ApplicationRoles from atst.domain.environment_roles import EnvironmentRoles from atst.domain.invitations import ApplicationInvitations from atst.domain.common import Paginator +from atst.domain.csp.cloud import GeneralCSPException from atst.domain.permission_sets import PermissionSets from atst.models.application_role import Status as ApplicationRoleStatus from atst.models.environment_role import CSPRole, EnvironmentRole @@ -748,3 +749,41 @@ def test_handle_update_member(set_g): assert len(application.roles) == 1 assert len(app_role.environment_roles) == 1 assert app_role.environment_roles[0].environment == env + + +def test_handle_update_member_with_error(set_g, monkeypatch, mock_logger): + exception = "An error occurred." + + def _raise_csp_exception(*args, **kwargs): + raise GeneralCSPException(exception) + + monkeypatch.setattr( + "atst.domain.environments.Environments.update_env_role", _raise_csp_exception + ) + + user = UserFactory.create() + application = ApplicationFactory.create( + environments=[{"name": "Naboo"}, {"name": "Endor"}] + ) + (env, env_1) = application.environments + app_role = ApplicationRoleFactory(application=application) + set_g("current_user", application.portfolio.owner) + set_g("portfolio", application.portfolio) + set_g("application", application) + + form_data = ImmutableMultiDict( + { + "environment_roles-0-environment_id": env.id, + "environment_roles-0-role": "Basic Access", + "environment_roles-0-environment_name": env.name, + "environment_roles-1-environment_id": env_1.id, + "environment_roles-1-role": NO_ACCESS, + "environment_roles-1-environment_name": env_1.name, + "perms_env_mgmt": True, + "perms_team_mgmt": True, + "perms_del_env": True, + } + ) + handle_update_member(application.id, app_role.id, form_data) + + assert mock_logger.messages[-1] == exception diff --git a/tests/utils.py b/tests/utils.py index 152c347a..66bf2b18 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -40,6 +40,9 @@ class FakeLogger: def error(self, msg, *args, **kwargs): self._log("error", msg, *args, **kwargs) + def exception(self, msg, *args, **kwargs): + self._log("exception", msg, *args, **kwargs) + def _log(self, _lvl, msg, *args, **kwargs): self.messages.append(msg) if "extra" in kwargs: From 2552d4c700544d733c47cdf4703e4df96e56656d Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Fri, 13 Dec 2019 11:39:25 -0500 Subject: [PATCH 13/14] Styling for empty status accordion and update Not signed to Unsigned --- atst/models/task_order.py | 2 +- styles/elements/_accordions.scss | 7 +++++++ templates/task_orders/index.html | 4 +++- tests/domain/test_task_orders.py | 2 +- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/atst/models/task_order.py b/atst/models/task_order.py index 7baef38d..1a46e505 100644 --- a/atst/models/task_order.py +++ b/atst/models/task_order.py @@ -17,7 +17,7 @@ class Status(Enum): ACTIVE = "Active" UPCOMING = "Upcoming" EXPIRED = "Expired" - UNSIGNED = "Not signed" + UNSIGNED = "Unsigned" SORT_ORDERING = [ diff --git a/styles/elements/_accordions.scss b/styles/elements/_accordions.scss index 4f56e9bb..8d21ef62 100644 --- a/styles/elements/_accordions.scss +++ b/styles/elements/_accordions.scss @@ -46,6 +46,13 @@ margin: 0; } } + + &--empty { + font-weight: $font-bold; + color: $color-gray-dark; + padding: $gap * 8; + text-align: center; + } } &-list { diff --git a/templates/task_orders/index.html b/templates/task_orders/index.html index a272fade..b9277123 100644 --- a/templates/task_orders/index.html +++ b/templates/task_orders/index.html @@ -56,7 +56,9 @@
{% endfor %} {% else %} - {{ "task_orders.status_empty_state" | translate({ 'status': status }) }} +
+ {{ "task_orders.status_empty_state" | translate({ 'status': status }) }} +
{% endif %} {% endcall %}
diff --git a/tests/domain/test_task_orders.py b/tests/domain/test_task_orders.py index acc2d158..7bc4cf41 100644 --- a/tests/domain/test_task_orders.py +++ b/tests/domain/test_task_orders.py @@ -152,5 +152,5 @@ def test_task_order_sort_by_status(): 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 len(sorted_by_status["Unsigned"]) == 1 assert list(sorted_by_status.keys()) == [status.value for status in SORT_ORDERING] From 1466a302b24d0aa47a0d91c7aff6bdf40ae285b3 Mon Sep 17 00:00:00 2001 From: dandds Date: Wed, 11 Dec 2019 05:41:54 -0500 Subject: [PATCH 14/14] K8s YAML integer values need to be quoted. --- deploy/azure/atst-envvars-configmap.yml | 6 +++--- deploy/azure/atst-worker-envvars-configmap.yml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/deploy/azure/atst-envvars-configmap.yml b/deploy/azure/atst-envvars-configmap.yml index 8907493d..d2b0ba45 100644 --- a/deploy/azure/atst-envvars-configmap.yml +++ b/deploy/azure/atst-envvars-configmap.yml @@ -13,10 +13,10 @@ data: CDN_ORIGIN: https://azure.atat.code.mil CELERY_DEFAULT_QUEUE: celery-master CSP: azure - DEBUG: 0 + DEBUG: "0" FLASK_ENV: master LOG_JSON: "true" - MAIL_PORT: 587 + MAIL_PORT: "587" MAIL_SENDER: postmaster@atat.code.mil MAIL_SERVER: smtp.mailgun.org MAIL_TLS: "true" @@ -24,7 +24,7 @@ data: PGAPPNAME: atst PGDATABASE: staging PGHOST: atat-db.postgres.database.azure.com - PGPORT: 5432 + PGPORT: "5432" PGSSLMODE: verify-full PGSSLROOTCERT: /opt/atat/atst/ssl/pgsslrootcert.crt PGUSER: atat_master@atat-db diff --git a/deploy/azure/atst-worker-envvars-configmap.yml b/deploy/azure/atst-worker-envvars-configmap.yml index ab10c118..4323952c 100644 --- a/deploy/azure/atst-worker-envvars-configmap.yml +++ b/deploy/azure/atst-worker-envvars-configmap.yml @@ -9,9 +9,9 @@ data: AZURE_TO_BUCKET_NAME: task-order-pdfs CAC_URL: https://auth-staging.atat.code.mil/login-redirect CELERY_DEFAULT_QUEUE: celery-master - DEBUG: 0 + DEBUG: "0" DISABLE_CRL_CHECK: "true" - MAIL_PORT: 587 + MAIL_PORT: "587" MAIL_SENDER: postmaster@atat.code.mil MAIL_SERVER: smtp.mailgun.org MAIL_TLS: "true" @@ -19,7 +19,7 @@ data: PGAPPNAME: atst PGDATABASE: staging PGHOST: atat-db.postgres.database.azure.com - PGPORT: 5432 + PGPORT: "5432" PGSSLMODE: verify-full PGSSLROOTCERT: /opt/atat/atst/ssl/pgsslrootcert.crt PGUSER: atat_master@atat-db