Merge branch 'staging' into state-machine-unit-tests
This commit is contained in:
commit
be53a1fd1a
@ -96,6 +96,7 @@ commands:
|
|||||||
apk del --purge build
|
apk del --purge build
|
||||||
- run:
|
- run:
|
||||||
name: Login to Azure CLI
|
name: Login to Azure CLI
|
||||||
|
shell: /bin/sh -o pipefail
|
||||||
command: |
|
command: |
|
||||||
az login \
|
az login \
|
||||||
--service-principal \
|
--service-principal \
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"files": "^.secrets.baseline$|^.*pgsslrootcert.yml$",
|
"files": "^.secrets.baseline$|^.*pgsslrootcert.yml$",
|
||||||
"lines": null
|
"lines": null
|
||||||
},
|
},
|
||||||
"generated_at": "2020-02-12T18:51:01Z",
|
"generated_at": "2020-02-17T20:49:33Z",
|
||||||
"plugins_used": [
|
"plugins_used": [
|
||||||
{
|
{
|
||||||
"base64_limit": 4.5,
|
"base64_limit": 4.5,
|
||||||
@ -82,7 +82,7 @@
|
|||||||
"hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3",
|
"hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3",
|
||||||
"is_secret": false,
|
"is_secret": false,
|
||||||
"is_verified": false,
|
"is_verified": false,
|
||||||
"line_number": 44,
|
"line_number": 48,
|
||||||
"type": "Secret Keyword"
|
"type": "Secret Keyword"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -219,6 +219,10 @@ To generate coverage reports for the Javascript tests:
|
|||||||
|
|
||||||
- `ASSETS_URL`: URL to host which serves static assets (such as a CDN).
|
- `ASSETS_URL`: URL to host which serves static assets (such as a CDN).
|
||||||
- `AZURE_ACCOUNT_NAME`: The name for the Azure blob storage account
|
- `AZURE_ACCOUNT_NAME`: The name for the Azure blob storage account
|
||||||
|
- `AZURE_CALC_CLIENT_ID`: The client id used to generate a token for the Azure pricing calculator
|
||||||
|
- `AZURE_CALC_RESOURCE`: The resource URL used to generate a token for the Azure pricing calculator
|
||||||
|
- `AZURE_CALC_SECRET`: The secret key used to generate a token for the Azure pricing calculator
|
||||||
|
- `AZURE_CALC_URL`: The redirect URL for the Azure pricing calculator
|
||||||
- `AZURE_LOGIN_URL`: The URL used to login for an Azure instance.
|
- `AZURE_LOGIN_URL`: The URL used to login for an Azure instance.
|
||||||
- `AZURE_STORAGE_KEY`: A valid secret key for the Azure blob storage account
|
- `AZURE_STORAGE_KEY`: A valid secret key for the Azure blob storage account
|
||||||
- `AZURE_TO_BUCKET_NAME`: The Azure blob storage container name for task order uploads
|
- `AZURE_TO_BUCKET_NAME`: The Azure blob storage container name for task order uploads
|
||||||
|
@ -1738,7 +1738,6 @@ class AzureCloudProvider(CloudProviderInterface):
|
|||||||
cost_mgmt_url = (
|
cost_mgmt_url = (
|
||||||
f"/providers/Microsoft.CostManagement/query?api-version=2019-11-01"
|
f"/providers/Microsoft.CostManagement/query?api-version=2019-11-01"
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = self.sdk.requests.post(
|
result = self.sdk.requests.post(
|
||||||
f"{self.sdk.cloud.endpoints.resource_manager}{payload.invoice_section_id}{cost_mgmt_url}",
|
f"{self.sdk.cloud.endpoints.resource_manager}{payload.invoice_section_id}{cost_mgmt_url}",
|
||||||
@ -1770,3 +1769,17 @@ class AzureCloudProvider(CloudProviderInterface):
|
|||||||
result.status_code,
|
result.status_code,
|
||||||
f"azure application error getting reporting data. {str(exc)}",
|
f"azure application error getting reporting data. {str(exc)}",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _get_calculator_creds(self):
|
||||||
|
authority = f"{self.sdk.cloud.endpoints.active_directory}/{self.tenant_id}"
|
||||||
|
context = self.sdk.adal.AuthenticationContext(authority=authority)
|
||||||
|
response = context.acquire_token_with_client_credentials(
|
||||||
|
self.config.get("AZURE_CALC_RESOURCE"),
|
||||||
|
self.config.get("AZURE_CALC_CLIENT_ID"),
|
||||||
|
self.config.get("AZURE_CALC_SECRET"),
|
||||||
|
)
|
||||||
|
return response.get("accessToken")
|
||||||
|
|
||||||
|
def get_calculator_url(self):
|
||||||
|
calc_access_token = self._get_calculator_creds()
|
||||||
|
return f"{self.config.get('AZURE_CALC_URL')}?access_token={calc_access_token}"
|
||||||
|
@ -90,3 +90,13 @@ class TaskOrders(BaseDomainClass):
|
|||||||
)
|
)
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_clins_for_create_billing_instructions(cls):
|
||||||
|
return (
|
||||||
|
db.session.query(CLIN)
|
||||||
|
.filter(
|
||||||
|
CLIN.last_sent_at.is_(None), CLIN.start_date < pendulum.now(tz="UTC")
|
||||||
|
)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
@ -33,6 +33,7 @@ class PortfolioForm(BaseForm):
|
|||||||
class PortfolioCreationForm(PortfolioForm):
|
class PortfolioCreationForm(PortfolioForm):
|
||||||
defense_component = SelectMultipleField(
|
defense_component = SelectMultipleField(
|
||||||
translate("forms.portfolio.defense_component.title"),
|
translate("forms.portfolio.defense_component.title"),
|
||||||
|
description=translate("forms.portfolio.defense_component.help_text"),
|
||||||
choices=SERVICE_BRANCHES,
|
choices=SERVICE_BRANCHES,
|
||||||
widget=ListWidget(prefix_label=False),
|
widget=ListWidget(prefix_label=False),
|
||||||
option_widget=CheckboxInput(),
|
option_widget=CheckboxInput(),
|
||||||
|
46
atst/jobs.py
46
atst/jobs.py
@ -10,6 +10,7 @@ from atst.domain.csp.cloud import CloudProviderInterface
|
|||||||
from atst.domain.csp.cloud.exceptions import GeneralCSPException
|
from atst.domain.csp.cloud.exceptions import GeneralCSPException
|
||||||
from atst.domain.csp.cloud.models import (
|
from atst.domain.csp.cloud.models import (
|
||||||
ApplicationCSPPayload,
|
ApplicationCSPPayload,
|
||||||
|
BillingInstructionCSPPayload,
|
||||||
EnvironmentCSPPayload,
|
EnvironmentCSPPayload,
|
||||||
UserCSPPayload,
|
UserCSPPayload,
|
||||||
UserRoleCSPPayload,
|
UserRoleCSPPayload,
|
||||||
@ -111,6 +112,18 @@ def do_create_user(csp: CloudProviderInterface, application_role_ids=None):
|
|||||||
db.session.add(app_role)
|
db.session.add(app_role)
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
username = payload.user_principal_name
|
||||||
|
send_mail(
|
||||||
|
recipients=[user.email],
|
||||||
|
subject=translate("email.app_role_created.subject"),
|
||||||
|
body=translate(
|
||||||
|
"email.app_role_created.body",
|
||||||
|
{"url": app.config.get("AZURE_LOGIN_URL"), "username": username},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
app.logger.info(
|
||||||
|
f"Application role created notification email sent. User id: {user.id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def do_create_environment(csp: CloudProviderInterface, environment_id=None):
|
def do_create_environment(csp: CloudProviderInterface, environment_id=None):
|
||||||
@ -280,7 +293,7 @@ def dispatch_create_atat_admin_user(self):
|
|||||||
|
|
||||||
|
|
||||||
@celery.task(bind=True)
|
@celery.task(bind=True)
|
||||||
def dispatch_send_task_order_files(self):
|
def send_task_order_files(self):
|
||||||
task_orders = TaskOrders.get_for_send_task_order_files()
|
task_orders = TaskOrders.get_for_send_task_order_files()
|
||||||
recipients = [app.config.get("MICROSOFT_TASK_ORDER_EMAIL_ADDRESS")]
|
recipients = [app.config.get("MICROSOFT_TASK_ORDER_EMAIL_ADDRESS")]
|
||||||
|
|
||||||
@ -301,7 +314,36 @@ def dispatch_send_task_order_files(self):
|
|||||||
app.logger.exception(err)
|
app.logger.exception(err)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
task_order.pdf_last_sent_at = pendulum.now()
|
task_order.pdf_last_sent_at = pendulum.now(tz="UTC")
|
||||||
db.session.add(task_order)
|
db.session.add(task_order)
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
@celery.task(bind=True)
|
||||||
|
def create_billing_instruction(self):
|
||||||
|
clins = TaskOrders.get_clins_for_create_billing_instructions()
|
||||||
|
for clin in clins:
|
||||||
|
portfolio = clin.task_order.portfolio
|
||||||
|
|
||||||
|
payload = BillingInstructionCSPPayload(
|
||||||
|
tenant_id=portfolio.csp_data.get("tenant_id"),
|
||||||
|
billing_account_name=portfolio.csp_data.get("billing_account_name"),
|
||||||
|
billing_profile_name=portfolio.csp_data.get("billing_profile_name"),
|
||||||
|
initial_clin_amount=clin.obligated_amount,
|
||||||
|
initial_clin_start_date=str(clin.start_date),
|
||||||
|
initial_clin_end_date=str(clin.end_date),
|
||||||
|
initial_clin_type=clin.number,
|
||||||
|
initial_task_order_id=str(clin.task_order_id),
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
app.csp.cloud.create_billing_instruction(payload)
|
||||||
|
except (AzureError) as err:
|
||||||
|
app.logger.exception(err)
|
||||||
|
continue
|
||||||
|
|
||||||
|
clin.last_sent_at = pendulum.now(tz="UTC")
|
||||||
|
db.session.add(clin)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
@ -66,12 +66,14 @@ class CLIN(Base, mixins.TimestampsMixin):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def to_dictionary(self):
|
def to_dictionary(self):
|
||||||
return {
|
data = {
|
||||||
c.name: getattr(self, c.name)
|
c.name: getattr(self, c.name)
|
||||||
for c in self.__table__.columns
|
for c in self.__table__.columns
|
||||||
if c.name not in ["id"]
|
if c.name not in ["id"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_active(self):
|
def is_active(self):
|
||||||
return (
|
return (
|
||||||
|
@ -27,6 +27,14 @@ def update_celery(celery, app):
|
|||||||
"task": "atst.jobs.dispatch_create_environment_role",
|
"task": "atst.jobs.dispatch_create_environment_role",
|
||||||
"schedule": 60,
|
"schedule": 60,
|
||||||
},
|
},
|
||||||
|
"beat-send_task_order_files": {
|
||||||
|
"task": "atst.jobs.send_task_order_files",
|
||||||
|
"schedule": 60,
|
||||||
|
},
|
||||||
|
"beat-create_billing_instruction": {
|
||||||
|
"task": "atst.jobs.create_billing_instruction",
|
||||||
|
"schedule": 60,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
class ContextTask(celery.Task):
|
class ContextTask(celery.Task):
|
||||||
|
@ -50,9 +50,7 @@ def reports(portfolio_id):
|
|||||||
"portfolios/reports/index.html",
|
"portfolios/reports/index.html",
|
||||||
portfolio=portfolio,
|
portfolio=portfolio,
|
||||||
# wrapped in str() because this sum returns a Decimal object
|
# wrapped in str() because this sum returns a Decimal object
|
||||||
total_portfolio_value=str(
|
total_portfolio_value=str(portfolio.total_obligated_funds),
|
||||||
portfolio.total_obligated_funds + portfolio.upcoming_obligated_funds
|
|
||||||
),
|
|
||||||
current_obligated_funds=current_obligated_funds,
|
current_obligated_funds=current_obligated_funds,
|
||||||
expired_task_orders=Reports.expired_task_orders(portfolio),
|
expired_task_orders=Reports.expired_task_orders(portfolio),
|
||||||
retrieved=pendulum.now(), # mocked datetime of reporting data retrival
|
retrieved=pendulum.now(), # mocked datetime of reporting data retrival
|
||||||
|
@ -3,6 +3,10 @@ ASSETS_URL
|
|||||||
AZURE_AADP_QTY=5
|
AZURE_AADP_QTY=5
|
||||||
AZURE_ACCOUNT_NAME
|
AZURE_ACCOUNT_NAME
|
||||||
AZURE_CLIENT_ID
|
AZURE_CLIENT_ID
|
||||||
|
AZURE_CALC_CLIENT_ID
|
||||||
|
AZURE_CALC_RESOURCE="http://azurecom.onmicrosoft.com/acom-prod/"
|
||||||
|
AZURE_CALC_SECRET
|
||||||
|
AZURE_CALC_URL="https://azure.microsoft.com/en-us/pricing/calculator/"
|
||||||
AZURE_GRAPH_RESOURCE="https://graph.microsoft.com/"
|
AZURE_GRAPH_RESOURCE="https://graph.microsoft.com/"
|
||||||
AZURE_LOGIN_URL="https://portal.azure.com/"
|
AZURE_LOGIN_URL="https://portal.azure.com/"
|
||||||
AZURE_POLICY_LOCATION=policies
|
AZURE_POLICY_LOCATION=policies
|
||||||
|
@ -2,10 +2,6 @@
|
|||||||
.form-row {
|
.form-row {
|
||||||
margin: ($gap * 4) 0;
|
margin: ($gap * 4) 0;
|
||||||
|
|
||||||
&--bordered {
|
|
||||||
border-bottom: $color-gray-lighter 1px solid;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-col {
|
.form-col {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
@ -22,8 +18,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.usa-input {
|
.usa-input {
|
||||||
input {
|
input,
|
||||||
max-width: none;
|
textarea {
|
||||||
|
max-width: $max-input-width;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -204,6 +201,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-container__half {
|
.form-container {
|
||||||
max-width: 46rem;
|
margin-bottom: $action-footer-height + $large-spacing;
|
||||||
|
|
||||||
|
&--narrow {
|
||||||
|
max-width: $max-input-width;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,17 +5,11 @@
|
|||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input__inline-fields {
|
|
||||||
text-align: left;
|
|
||||||
|
|
||||||
.usa-input__choices label {
|
|
||||||
font-weight: $font-bold;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.input__inline-fields {
|
.input__inline-fields {
|
||||||
padding: $gap * 2;
|
padding: $gap * 2;
|
||||||
border: 1px solid $color-gray-lighter;
|
border: 1px solid $color-gray-lighter;
|
||||||
|
text-align: left;
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
&.checked {
|
&.checked {
|
||||||
border: 1px solid $color-blue;
|
border: 1px solid $color-blue;
|
||||||
@ -33,7 +27,7 @@
|
|||||||
|
|
||||||
.user-info {
|
.user-info {
|
||||||
.usa-input {
|
.usa-input {
|
||||||
width: 45rem;
|
max-width: $max-input-width;
|
||||||
|
|
||||||
input,
|
input,
|
||||||
label,
|
label,
|
||||||
@ -53,8 +47,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#modal--add-app-mem,
|
.form-content--member-form {
|
||||||
#modal--add-portfolio-manager {
|
|
||||||
.modal__body {
|
.modal__body {
|
||||||
min-width: 75rem;
|
min-width: 75rem;
|
||||||
}
|
}
|
||||||
|
@ -519,3 +519,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#portfolio-create {
|
||||||
|
.usa-input__choices {
|
||||||
|
.usa-input__title {
|
||||||
|
font-weight: $font-bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-size: $base-font-size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -98,7 +98,3 @@ hr {
|
|||||||
.usa-section {
|
.usa-section {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form {
|
|
||||||
margin-bottom: $action-footer-height + $large-spacing;
|
|
||||||
}
|
|
||||||
|
@ -21,6 +21,7 @@ $home-pg-icon-width: 6rem;
|
|||||||
$large-spacing: 4rem;
|
$large-spacing: 4rem;
|
||||||
$max-page-width: $max-panel-width + $sidenav-expanded-width + $large-spacing;
|
$max-page-width: $max-panel-width + $sidenav-expanded-width + $large-spacing;
|
||||||
$action-footer-height: 6rem;
|
$action-footer-height: 6rem;
|
||||||
|
$max-input-width: 46rem;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* USWDS Variables
|
* USWDS Variables
|
||||||
|
@ -58,7 +58,7 @@
|
|||||||
|
|
||||||
.usa-input {
|
.usa-input {
|
||||||
margin: ($gap * 2) 0;
|
margin: ($gap * 2) 0;
|
||||||
max-width: 75rem;
|
max-width: $max-input-width;
|
||||||
|
|
||||||
&-label-helper {
|
&-label-helper {
|
||||||
font-size: $small-font-size;
|
font-size: $small-font-size;
|
||||||
@ -111,8 +111,7 @@
|
|||||||
@include h5;
|
@include h5;
|
||||||
|
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
|
max-width: $max-input-width;
|
||||||
@include line-max;
|
|
||||||
|
|
||||||
.icon-link {
|
.icon-link {
|
||||||
padding: 0 ($gap / 2);
|
padding: 0 ($gap / 2);
|
||||||
@ -180,6 +179,10 @@
|
|||||||
left: -3rem;
|
left: -3rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.usa-input__title {
|
||||||
|
margin-bottom: $gap;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
@ -372,19 +375,15 @@ select {
|
|||||||
.phone-input {
|
.phone-input {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
&__phone {
|
&__phone {
|
||||||
margin-right: $gap * 4;
|
margin-right: $gap * 4;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
.usa-input {
|
.usa-input {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
||||||
input,
|
|
||||||
label,
|
|
||||||
.usa-input__message {
|
|
||||||
max-width: 20rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-validation {
|
.icon-validation {
|
||||||
left: 20rem;
|
left: 20rem;
|
||||||
}
|
}
|
||||||
@ -392,7 +391,8 @@ select {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__extension {
|
&__extension {
|
||||||
margin-left: $gap * 4;
|
margin-right: $gap * 4;
|
||||||
|
flex-grow: 0;
|
||||||
|
|
||||||
.usa-input {
|
.usa-input {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@ -140,6 +140,10 @@
|
|||||||
&__confirmation {
|
&__confirmation {
|
||||||
margin-left: $gap * 8;
|
margin-left: $gap * 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.usa-input {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-order__modal-cancel {
|
.task-order__modal-cancel {
|
||||||
|
@ -36,7 +36,7 @@
|
|||||||
{% set invite_expired = member.role_status == 'invite_expired' %}
|
{% set invite_expired = member.role_status == 'invite_expired' %}
|
||||||
{%- if user_can(permissions.EDIT_APPLICATION_MEMBER) %}
|
{%- if user_can(permissions.EDIT_APPLICATION_MEMBER) %}
|
||||||
{% set modal_name = "edit_member-{}".format(loop.index) %}
|
{% set modal_name = "edit_member-{}".format(loop.index) %}
|
||||||
{% call Modal(modal_name, classes="form-content--app-mem") %}
|
{% call Modal(modal_name, classes="form-content--member-form") %}
|
||||||
<div class="modal__form--header">
|
<div class="modal__form--header">
|
||||||
<h1>{{ Icon('avatar') }} {{ "portfolios.applications.members.form.edit_access_header" | translate({ "user": member.user_name }) }}</h1>
|
<h1>{{ Icon('avatar') }} {{ "portfolios.applications.members.form.edit_access_header" | translate({ "user": member.user_name }) }}</h1>
|
||||||
</div>
|
</div>
|
||||||
@ -56,7 +56,7 @@
|
|||||||
|
|
||||||
{%- if invite_pending or invite_expired %}
|
{%- if invite_pending or invite_expired %}
|
||||||
{% set resend_invite_modal = "resend_invite-{}".format(member.role_id) %}
|
{% set resend_invite_modal = "resend_invite-{}".format(member.role_id) %}
|
||||||
{% call Modal(resend_invite_modal, classes="form-content--app-mem") %}
|
{% call Modal(resend_invite_modal, classes="form-content--member-form") %}
|
||||||
<div class="modal__form--header">
|
<div class="modal__form--header">
|
||||||
<h1>{{ "portfolios.applications.members.new.verify" | translate }}</h1>
|
<h1>{{ "portfolios.applications.members.new.verify" | translate }}</h1>
|
||||||
</div>
|
</div>
|
||||||
@ -183,6 +183,7 @@
|
|||||||
modal=new_member_modal_name,
|
modal=new_member_modal_name,
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
classes="form-content--member-form",
|
||||||
) }}
|
) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
{% include "fragments/flash.html" %}
|
{% include "fragments/flash.html" %}
|
||||||
|
|
||||||
<base-form inline-template :enable-save="true">
|
<base-form inline-template :enable-save="true">
|
||||||
<form method="POST" action="{{ action }}" v-on:submit="handleSubmit" class="form">
|
<form method="POST" action="{{ action }}" v-on:submit="handleSubmit" class="form-container">
|
||||||
{{ form.csrf_token }}
|
{{ form.csrf_token }}
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-col">
|
<div class="form-col">
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
</p>
|
</p>
|
||||||
<hr>
|
<hr>
|
||||||
<application-environments inline-template v-bind:initial-data='{{ form.data|tojson }}'>
|
<application-environments inline-template v-bind:initial-data='{{ form.data|tojson }}'>
|
||||||
<form method="POST" action="{{ url_for('applications.update_new_application_step_2', portfolio_id=portfolio.id, application_id=application.id) }}" v-on:submit="handleSubmit" class="form">
|
<form method="POST" action="{{ url_for('applications.update_new_application_step_2', portfolio_id=portfolio.id, application_id=application.id) }}" v-on:submit="handleSubmit" class="form-container">
|
||||||
<div class="subheading">{{ 'portfolios.applications.environments_heading' | translate }}</div>
|
<div class="subheading">{{ 'portfolios.applications.environments_heading' | translate }}</div>
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<div class="panel__content">
|
<div class="panel__content">
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
{% if user_can(permissions.EDIT_APPLICATION) %}
|
{% if user_can(permissions.EDIT_APPLICATION) %}
|
||||||
<base-form inline-template>
|
<base-form inline-template>
|
||||||
<form method="POST" action="{{ url_for('applications.update', application_id=application.id) }}" class="col col--half">
|
<form method="POST" action="{{ url_for('applications.update', application_id=application.id) }}" class="form-container--narrow">
|
||||||
{{ application_form.csrf_token }}
|
{{ application_form.csrf_token }}
|
||||||
{{ TextInput(application_form.name, validation="applicationName", optional=False) }}
|
{{ TextInput(application_form.name, validation="applicationName", optional=False) }}
|
||||||
{{ TextInput(application_form.description, validation="defaultTextAreaField", paragraph=True, optional=True, showOptional=False) }}
|
{{ TextInput(application_form.description, validation="defaultTextAreaField", paragraph=True, optional=True, showOptional=False) }}
|
||||||
|
@ -25,10 +25,10 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro MultiStepModalForm(name, form, form_action, steps, dismissable=False) -%}
|
{% macro MultiStepModalForm(name, form, form_action, steps, dismissable=False, classes="") -%}
|
||||||
{% set step_count = steps|length %}
|
{% set step_count = steps|length %}
|
||||||
<multi-step-modal-form inline-template :steps={{ step_count }}>
|
<multi-step-modal-form inline-template :steps={{ step_count }}>
|
||||||
{% call Modal(name=name, dismissable=dismissable) %}
|
{% call Modal(name=name, dismissable=dismissable, classes=classes) %}
|
||||||
<form id="{{ name }}" action="{{ form_action }}" method="POST" v-on:submit="handleSubmit">
|
<form id="{{ name }}" action="{{ form_action }}" method="POST" v-on:submit="handleSubmit">
|
||||||
{{ form.csrf_token }}
|
{{ form.csrf_token }}
|
||||||
<div v-if="$root.activeModal === '{{ name }}'">
|
<div v-if="$root.activeModal === '{{ name }}'">
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
<div v-cloak class="portfolio-admin">
|
<div v-cloak class="portfolio-admin">
|
||||||
{% include "fragments/flash.html" %}
|
{% include "fragments/flash.html" %}
|
||||||
<!-- max width of this section is 460px -->
|
<!-- max width of this section is 460px -->
|
||||||
<section class="form-container__half">
|
<section class="form-container--narrow">
|
||||||
<h3>Portfolio name and component</h3>
|
<h3>Portfolio name and component</h3>
|
||||||
{% if user_can(permissions.EDIT_PORTFOLIO_NAME) %}
|
{% if user_can(permissions.EDIT_PORTFOLIO_NAME) %}
|
||||||
<base-form inline-template>
|
<base-form inline-template>
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
{% set invite_expired = member.status == 'invite_expired' %}
|
{% set invite_expired = member.status == 'invite_expired' %}
|
||||||
|
|
||||||
{% set modal_name = "edit_member-{}".format(loop.index) %}
|
{% set modal_name = "edit_member-{}".format(loop.index) %}
|
||||||
{% call Modal(modal_name, classes="form-content--app-mem") %}
|
{% call Modal(modal_name, classes="form-content--member-form") %}
|
||||||
<div class="modal__form--header">
|
<div class="modal__form--header">
|
||||||
<h1>{{ Icon('avatar') }} {{ "portfolios.applications.members.form.edit_access_header" | translate({ "user": member.user_name }) }}</h1>
|
<h1>{{ Icon('avatar') }} {{ "portfolios.applications.members.form.edit_access_header" | translate({ "user": member.user_name }) }}</h1>
|
||||||
</div>
|
</div>
|
||||||
@ -34,7 +34,7 @@
|
|||||||
|
|
||||||
{% if invite_pending or invite_expired -%}
|
{% if invite_pending or invite_expired -%}
|
||||||
{% set resend_invite_modal = "resend_invite-{}".format(member.role_id) %}
|
{% set resend_invite_modal = "resend_invite-{}".format(member.role_id) %}
|
||||||
{% call Modal(resend_invite_modal, classes="form-content--app-mem") %}
|
{% call Modal(resend_invite_modal, classes="form-content--member-form") %}
|
||||||
<div class="modal__form--header">
|
<div class="modal__form--header">
|
||||||
<h1>{{ "portfolios.applications.members.new.verify" | translate }}</h1>
|
<h1>{{ "portfolios.applications.members.new.verify" | translate }}</h1>
|
||||||
</div>
|
</div>
|
||||||
@ -182,6 +182,7 @@
|
|||||||
modal=new_manager_modal,
|
modal=new_manager_modal,
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
classes="form-content--member-form",
|
||||||
) }}
|
) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
@ -19,24 +19,25 @@
|
|||||||
{{ StickyCTA(text="portfolios.new.cta_step_1"|translate, context=("portfolios.new.sticky_header_context"|translate({"step": "1"}) )) }}
|
{{ StickyCTA(text="portfolios.new.cta_step_1"|translate, context=("portfolios.new.sticky_header_context"|translate({"step": "1"}) )) }}
|
||||||
<base-form inline-template>
|
<base-form inline-template>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<form id="portfolio-create" class="col form" action="{{ url_for('portfolios.create_portfolio') }}" method="POST">
|
<form id="portfolio-create" class="col form-container" action="{{ url_for('portfolios.create_portfolio') }}" method="POST">
|
||||||
{{ form.csrf_token }}
|
{{ form.csrf_token }}
|
||||||
<div class="form-row form-row--bordered">
|
<div class="form-row">
|
||||||
<div class="form-col">
|
<div class="form-col">
|
||||||
{{ TextInput(form.name, validation="portfolioName", optional=False, classes="form-col") }}
|
{{ TextInput(form.name, validation="portfolioName", optional=False, classes="form-col") }}
|
||||||
{{"forms.portfolio.name.help_text" | translate | safe }}
|
{{"forms.portfolio.name.help_text" | translate | safe }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row form-row--bordered">
|
<hr>
|
||||||
|
<div class="form-row">
|
||||||
<div class="form-col">
|
<div class="form-col">
|
||||||
{{ TextInput(form.description, validation="defaultTextAreaField", paragraph=True) }}
|
{{ TextInput(form.description, validation="defaultTextAreaField", paragraph=True) }}
|
||||||
{{"forms.portfolio.description.help_text" | translate | safe }}
|
{{"forms.portfolio.description.help_text" | translate | safe }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<hr>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-col">
|
<div class="form-col">
|
||||||
{{ MultiCheckboxInput(form.defense_component, optional=False) }}
|
{{ MultiCheckboxInput(form.defense_component, optional=False) }}
|
||||||
{{ "forms.portfolio.defense_component.help_text" | translate | safe }}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
@ -28,7 +28,7 @@
|
|||||||
|
|
||||||
{% include "fragments/flash.html" %}
|
{% include "fragments/flash.html" %}
|
||||||
|
|
||||||
<div class="task-order form">
|
<div class="task-order form-container">
|
||||||
{% block to_builder_form_field %}{% endblock %}
|
{% block to_builder_form_field %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
@ -1523,3 +1523,23 @@ def test_update_tenant_creds(mock_azure: AzureCloudProvider):
|
|||||||
assert updated_secret == KeyVaultCredentials(
|
assert updated_secret == KeyVaultCredentials(
|
||||||
**{**existing_secrets, **MOCK_CREDS}
|
**{**existing_secrets, **MOCK_CREDS}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_calculator_creds(mock_azure: AzureCloudProvider):
|
||||||
|
mock_azure.sdk.adal.AuthenticationContext.return_value.acquire_token_with_client_credentials.return_value = {
|
||||||
|
"accessToken": "TOKEN"
|
||||||
|
}
|
||||||
|
assert mock_azure._get_calculator_creds() == "TOKEN"
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_calculator_url(mock_azure: AzureCloudProvider):
|
||||||
|
with patch.object(
|
||||||
|
AzureCloudProvider,
|
||||||
|
"_get_calculator_creds",
|
||||||
|
wraps=mock_azure._get_calculator_creds,
|
||||||
|
) as _get_calculator_creds:
|
||||||
|
_get_calculator_creds.return_value = "TOKEN"
|
||||||
|
assert (
|
||||||
|
mock_azure.get_calculator_url()
|
||||||
|
== f"{mock_azure.config.get('AZURE_CALC_URL')}?access_token=TOKEN"
|
||||||
|
)
|
||||||
|
@ -9,6 +9,27 @@ from atst.models.task_order import TaskOrder, SORT_ORDERING, Status
|
|||||||
from tests.factories import TaskOrderFactory, CLINFactory, PortfolioFactory
|
from tests.factories import TaskOrderFactory, CLINFactory, PortfolioFactory
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def new_task_order():
|
||||||
|
return TaskOrderFactory.create(create_clins=[{}])
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def updated_task_order():
|
||||||
|
return TaskOrderFactory.create(
|
||||||
|
create_clins=[{"last_sent_at": pendulum.date(2020, 2, 1)}],
|
||||||
|
pdf_last_sent_at=pendulum.date(2020, 1, 1),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def sent_task_order():
|
||||||
|
return TaskOrderFactory.create(
|
||||||
|
create_clins=[{"last_sent_at": pendulum.date(2020, 1, 1)}],
|
||||||
|
pdf_last_sent_at=pendulum.date(2020, 1, 1),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_create_adds_clins():
|
def test_create_adds_clins():
|
||||||
portfolio = PortfolioFactory.create()
|
portfolio = PortfolioFactory.create()
|
||||||
clins = [
|
clins = [
|
||||||
@ -181,19 +202,18 @@ def test_allows_alphanumeric_number():
|
|||||||
assert TaskOrders.create(portfolio.id, number, [], None)
|
assert TaskOrders.create(portfolio.id, number, [], None)
|
||||||
|
|
||||||
|
|
||||||
def test_get_for_send_task_order_files():
|
def test_get_for_send_task_order_files(
|
||||||
new_to = TaskOrderFactory.create(create_clins=[{}])
|
new_task_order, updated_task_order, sent_task_order
|
||||||
updated_to = TaskOrderFactory.create(
|
):
|
||||||
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": 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()
|
updated_and_new_task_orders = TaskOrders.get_for_send_task_order_files()
|
||||||
assert len(updated_and_new_task_orders) == 2
|
assert len(updated_and_new_task_orders) == 2
|
||||||
assert sent_to not in updated_and_new_task_orders
|
assert sent_task_order not in updated_and_new_task_orders
|
||||||
assert updated_to in updated_and_new_task_orders
|
assert updated_task_order in updated_and_new_task_orders
|
||||||
assert new_to in updated_and_new_task_orders
|
assert new_task_order in updated_and_new_task_orders
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_clins_for_create_billing_instructions(new_task_order, sent_task_order):
|
||||||
|
new_clins = TaskOrders.get_clins_for_create_billing_instructions()
|
||||||
|
assert len(new_clins) == 1
|
||||||
|
assert new_task_order.clins[0] in new_clins
|
||||||
|
assert sent_task_order.clins[0] not in new_clins
|
||||||
|
@ -4,6 +4,9 @@ from unittest.mock import Mock
|
|||||||
from atst.domain.csp.cloud import AzureCloudProvider
|
from atst.domain.csp.cloud import AzureCloudProvider
|
||||||
|
|
||||||
AZURE_CONFIG = {
|
AZURE_CONFIG = {
|
||||||
|
"AZURE_CALC_CLIENT_ID": "MOCK",
|
||||||
|
"AZURE_CALC_SECRET": "MOCK", # pragma: allowlist secret
|
||||||
|
"AZURE_CALC_RESOURCE": "http://calc",
|
||||||
"AZURE_CLIENT_ID": "MOCK",
|
"AZURE_CLIENT_ID": "MOCK",
|
||||||
"AZURE_SECRET_KEY": "MOCK",
|
"AZURE_SECRET_KEY": "MOCK",
|
||||||
"AZURE_TENANT_ID": "MOCK",
|
"AZURE_TENANT_ID": "MOCK",
|
||||||
|
@ -6,7 +6,8 @@ from smtplib import SMTPException
|
|||||||
from azure.core.exceptions import AzureError
|
from azure.core.exceptions import AzureError
|
||||||
|
|
||||||
from atst.domain.csp.cloud import MockCloudProvider
|
from atst.domain.csp.cloud import MockCloudProvider
|
||||||
from atst.domain.csp.cloud.models import UserRoleCSPResult
|
from atst.domain.csp.cloud.models import BillingInstructionCSPPayload, UserRoleCSPResult
|
||||||
|
from atst.domain.portfolios import Portfolios
|
||||||
from atst.models import ApplicationRoleStatus, Portfolio, FSMStates
|
from atst.models import ApplicationRoleStatus, Portfolio, FSMStates
|
||||||
|
|
||||||
from atst.jobs import (
|
from atst.jobs import (
|
||||||
@ -16,7 +17,7 @@ from atst.jobs import (
|
|||||||
dispatch_create_user,
|
dispatch_create_user,
|
||||||
dispatch_create_environment_role,
|
dispatch_create_environment_role,
|
||||||
dispatch_provision_portfolio,
|
dispatch_provision_portfolio,
|
||||||
dispatch_send_task_order_files,
|
create_billing_instruction,
|
||||||
create_environment,
|
create_environment,
|
||||||
do_create_user,
|
do_create_user,
|
||||||
do_provision_portfolio,
|
do_provision_portfolio,
|
||||||
@ -24,10 +25,12 @@ from atst.jobs import (
|
|||||||
do_create_environment_role,
|
do_create_environment_role,
|
||||||
do_create_application,
|
do_create_application,
|
||||||
send_PPOC_email,
|
send_PPOC_email,
|
||||||
|
send_task_order_files,
|
||||||
)
|
)
|
||||||
from tests.factories import (
|
from tests.factories import (
|
||||||
ApplicationFactory,
|
ApplicationFactory,
|
||||||
ApplicationRoleFactory,
|
ApplicationRoleFactory,
|
||||||
|
CLINFactory,
|
||||||
EnvironmentFactory,
|
EnvironmentFactory,
|
||||||
EnvironmentRoleFactory,
|
EnvironmentRoleFactory,
|
||||||
PortfolioFactory,
|
PortfolioFactory,
|
||||||
@ -135,28 +138,63 @@ def test_create_application_job_is_idempotent(csp):
|
|||||||
csp.create_application.assert_not_called()
|
csp.create_application.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
def test_create_user_job(session, csp, app):
|
class TestCreateUserJob:
|
||||||
portfolio = PortfolioFactory.create(
|
@pytest.fixture
|
||||||
csp_data={
|
def portfolio(self, app):
|
||||||
"tenant_id": str(uuid4()),
|
return PortfolioFactory.create(
|
||||||
"domain_name": f"rebelalliance.{app.config.get('OFFICE_365_DOMAIN')}",
|
csp_data={
|
||||||
}
|
"tenant_id": str(uuid4()),
|
||||||
)
|
"domain_name": f"rebelalliance.{app.config.get('OFFICE_365_DOMAIN')}",
|
||||||
application = ApplicationFactory.create(portfolio=portfolio, cloud_id="321")
|
}
|
||||||
user = UserFactory.create(
|
)
|
||||||
first_name="Han", last_name="Solo", email="han@example.com"
|
|
||||||
)
|
|
||||||
app_role = ApplicationRoleFactory.create(
|
|
||||||
application=application,
|
|
||||||
user=user,
|
|
||||||
status=ApplicationRoleStatus.ACTIVE,
|
|
||||||
cloud_id=None,
|
|
||||||
)
|
|
||||||
|
|
||||||
do_create_user(csp, [app_role.id])
|
@pytest.fixture
|
||||||
session.refresh(app_role)
|
def app_1(self, portfolio):
|
||||||
|
return ApplicationFactory.create(portfolio=portfolio, cloud_id="321")
|
||||||
|
|
||||||
assert app_role.cloud_id
|
@pytest.fixture
|
||||||
|
def app_2(self, portfolio):
|
||||||
|
return ApplicationFactory.create(portfolio=portfolio, cloud_id="123")
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def user(self):
|
||||||
|
return UserFactory.create(
|
||||||
|
first_name="Han", last_name="Solo", email="han@example.com"
|
||||||
|
)
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def app_role_1(self, app_1, user):
|
||||||
|
return ApplicationRoleFactory.create(
|
||||||
|
application=app_1,
|
||||||
|
user=user,
|
||||||
|
status=ApplicationRoleStatus.ACTIVE,
|
||||||
|
cloud_id=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def app_role_2(self, app_2, user):
|
||||||
|
return ApplicationRoleFactory.create(
|
||||||
|
application=app_2,
|
||||||
|
user=user,
|
||||||
|
status=ApplicationRoleStatus.ACTIVE,
|
||||||
|
cloud_id=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_create_user_job(self, session, csp, app_role_1):
|
||||||
|
assert not app_role_1.cloud_id
|
||||||
|
|
||||||
|
session.begin_nested()
|
||||||
|
do_create_user(csp, [app_role_1.id])
|
||||||
|
session.rollback()
|
||||||
|
|
||||||
|
assert app_role_1.cloud_id
|
||||||
|
|
||||||
|
def test_create_user_sends_email(self, monkeypatch, csp, app_role_1, app_role_2):
|
||||||
|
mock = Mock()
|
||||||
|
monkeypatch.setattr("atst.jobs.send_mail", mock)
|
||||||
|
|
||||||
|
do_create_user(csp, [app_role_1.id, app_role_2.id])
|
||||||
|
assert mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_dispatch_create_environment(session, monkeypatch):
|
def test_dispatch_create_environment(session, monkeypatch):
|
||||||
@ -380,82 +418,152 @@ def test_create_environment_role():
|
|||||||
assert env_role.cloud_id == "a-cloud-id"
|
assert env_role.cloud_id == "a-cloud-id"
|
||||||
|
|
||||||
|
|
||||||
# TODO: Refactor the tests related to dispatch_send_task_order_files() into a class
|
class TestSendTaskOrderFiles:
|
||||||
# and separate the success test into two tests
|
@pytest.fixture(scope="function")
|
||||||
def test_dispatch_send_task_order_files(monkeypatch, app):
|
def send_mail(self, monkeypatch):
|
||||||
mock = Mock()
|
mock = Mock()
|
||||||
monkeypatch.setattr("atst.jobs.send_mail", mock)
|
monkeypatch.setattr("atst.jobs.send_mail", mock)
|
||||||
|
return mock
|
||||||
|
|
||||||
def _download_task_order(MockFileService, object_name):
|
@pytest.fixture(scope="function")
|
||||||
return {"name": object_name}
|
def download_task_order(self, monkeypatch):
|
||||||
|
def _download_task_order(MockFileService, object_name):
|
||||||
|
return {"name": object_name}
|
||||||
|
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
"atst.domain.csp.files.MockFileService.download_task_order",
|
"atst.domain.csp.files.MockFileService.download_task_order",
|
||||||
_download_task_order,
|
_download_task_order,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create 3 new Task Orders
|
def test_sends_multiple_emails(self, send_mail, download_task_order):
|
||||||
for i in range(3):
|
# Create 3 Task Orders
|
||||||
TaskOrderFactory.create(create_clins=[{"number": "0001"}])
|
for i in range(3):
|
||||||
|
TaskOrderFactory.create(create_clins=[{"number": "0001"}])
|
||||||
|
|
||||||
dispatch_send_task_order_files.run()
|
send_task_order_files.run()
|
||||||
|
|
||||||
# Check that send_with_attachment was called once for each task order
|
# Check that send_with_attachment was called once for each task order
|
||||||
assert mock.call_count == 3
|
assert send_mail.call_count == 3
|
||||||
mock.reset_mock()
|
|
||||||
|
|
||||||
# Create new TO
|
def test_kwargs(self, send_mail, download_task_order, app):
|
||||||
task_order = TaskOrderFactory.create(create_clins=[{"number": "0001"}])
|
task_order = TaskOrderFactory.create(create_clins=[{"number": "0001"}])
|
||||||
assert not task_order.pdf_last_sent_at
|
send_task_order_files.run()
|
||||||
|
|
||||||
dispatch_send_task_order_files.run()
|
# Check that send_with_attachment was called with correct kwargs
|
||||||
|
send_mail.assert_called_once_with(
|
||||||
|
recipients=[app.config.get("MICROSOFT_TASK_ORDER_EMAIL_ADDRESS")],
|
||||||
|
subject=translate(
|
||||||
|
"email.task_order_sent.subject", {"to_number": task_order.number}
|
||||||
|
),
|
||||||
|
body=translate(
|
||||||
|
"email.task_order_sent.body", {"to_number": task_order.number}
|
||||||
|
),
|
||||||
|
attachments=[
|
||||||
|
{
|
||||||
|
"name": task_order.pdf.object_name,
|
||||||
|
"maintype": "application",
|
||||||
|
"subtype": "pdf",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert task_order.pdf_last_sent_at
|
||||||
|
|
||||||
# Check that send_with_attachment was called with correct kwargs
|
def test_send_failure(self, monkeypatch):
|
||||||
mock.assert_called_once_with(
|
def _raise_smtp_exception(**kwargs):
|
||||||
recipients=[app.config.get("MICROSOFT_TASK_ORDER_EMAIL_ADDRESS")],
|
raise SMTPException
|
||||||
subject=translate(
|
|
||||||
"email.task_order_sent.subject", {"to_number": task_order.number}
|
|
||||||
),
|
|
||||||
body=translate("email.task_order_sent.body", {"to_number": task_order.number}),
|
|
||||||
attachments=[
|
|
||||||
{
|
|
||||||
"name": task_order.pdf.object_name,
|
|
||||||
"maintype": "application",
|
|
||||||
"subtype": "pdf",
|
|
||||||
}
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
assert task_order.pdf_last_sent_at
|
monkeypatch.setattr("atst.jobs.send_mail", _raise_smtp_exception)
|
||||||
|
task_order = TaskOrderFactory.create(create_clins=[{"number": "0001"}])
|
||||||
|
send_task_order_files.run()
|
||||||
|
|
||||||
|
# Check that pdf_last_sent_at has not been updated
|
||||||
|
assert not task_order.pdf_last_sent_at
|
||||||
|
|
||||||
|
def test_download_failure(self, send_mail, monkeypatch):
|
||||||
|
def _download_task_order(MockFileService, object_name):
|
||||||
|
raise AzureError("something went wrong")
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"atst.domain.csp.files.MockFileService.download_task_order",
|
||||||
|
_download_task_order,
|
||||||
|
)
|
||||||
|
task_order = TaskOrderFactory.create(create_clins=[{"number": "0002"}])
|
||||||
|
send_task_order_files.run()
|
||||||
|
|
||||||
|
# Check that pdf_last_sent_at has not been updated
|
||||||
|
assert not task_order.pdf_last_sent_at
|
||||||
|
|
||||||
|
|
||||||
def test_dispatch_send_task_order_files_send_failure(monkeypatch):
|
class TestCreateBillingInstructions:
|
||||||
def _raise_smtp_exception(**kwargs):
|
@pytest.fixture
|
||||||
raise SMTPException
|
def unsent_clin(self):
|
||||||
|
start_date = pendulum.now().subtract(days=1)
|
||||||
|
portfolio = PortfolioFactory.create(
|
||||||
|
csp_data={
|
||||||
|
"tenant_id": str(uuid4()),
|
||||||
|
"billing_account_name": "fake",
|
||||||
|
"billing_profile_name": "fake",
|
||||||
|
},
|
||||||
|
task_orders=[{"create_clins": [{"start_date": start_date}]}],
|
||||||
|
)
|
||||||
|
return portfolio.task_orders[0].clins[0]
|
||||||
|
|
||||||
monkeypatch.setattr("atst.jobs.send_mail", _raise_smtp_exception)
|
def test_update_clin_last_sent_at(self, session, unsent_clin):
|
||||||
|
assert not unsent_clin.last_sent_at
|
||||||
|
|
||||||
task_order = TaskOrderFactory.create(create_clins=[{"number": "0001"}])
|
# The session needs to be nested to prevent detached SQLAlchemy instance
|
||||||
dispatch_send_task_order_files.run()
|
session.begin_nested()
|
||||||
|
create_billing_instruction()
|
||||||
|
|
||||||
# Check that pdf_last_sent_at has not been updated
|
# check that last_sent_at has been updated
|
||||||
assert not task_order.pdf_last_sent_at
|
assert unsent_clin.last_sent_at
|
||||||
|
session.rollback()
|
||||||
|
|
||||||
|
def test_failure(self, monkeypatch, session, unsent_clin):
|
||||||
|
def _create_billing_instruction(MockCloudProvider, object_name):
|
||||||
|
raise AzureError("something went wrong")
|
||||||
|
|
||||||
def test_dispatch_send_task_order_files_download_failure(monkeypatch):
|
monkeypatch.setattr(
|
||||||
mock = Mock()
|
"atst.domain.csp.cloud.MockCloudProvider.create_billing_instruction",
|
||||||
monkeypatch.setattr("atst.jobs.send_mail", mock)
|
_create_billing_instruction,
|
||||||
|
)
|
||||||
|
|
||||||
def _download_task_order(MockFileService, object_name):
|
# The session needs to be nested to prevent detached SQLAlchemy instance
|
||||||
raise AzureError("something went wrong")
|
session.begin_nested()
|
||||||
|
create_billing_instruction()
|
||||||
|
|
||||||
monkeypatch.setattr(
|
# check that last_sent_at has not been updated
|
||||||
"atst.domain.csp.files.MockFileService.download_task_order",
|
assert not unsent_clin.last_sent_at
|
||||||
_download_task_order,
|
session.rollback()
|
||||||
)
|
|
||||||
|
|
||||||
task_order = TaskOrderFactory.create(create_clins=[{"number": "0002"}])
|
def test_task_order_with_multiple_clins(self, session):
|
||||||
dispatch_send_task_order_files.run()
|
start_date = pendulum.now(tz="UTC").subtract(days=1)
|
||||||
|
portfolio = PortfolioFactory.create(
|
||||||
|
csp_data={
|
||||||
|
"tenant_id": str(uuid4()),
|
||||||
|
"billing_account_name": "fake",
|
||||||
|
"billing_profile_name": "fake",
|
||||||
|
},
|
||||||
|
task_orders=[
|
||||||
|
{
|
||||||
|
"create_clins": [
|
||||||
|
{"start_date": start_date, "last_sent_at": start_date}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
task_order = portfolio.task_orders[0]
|
||||||
|
sent_clin = task_order.clins[0]
|
||||||
|
|
||||||
# Check that pdf_last_sent_at has not been updated
|
# Add new CLIN to the Task Order
|
||||||
assert not task_order.pdf_last_sent_at
|
new_clin = CLINFactory.create(task_order=task_order)
|
||||||
|
assert not new_clin.last_sent_at
|
||||||
|
|
||||||
|
session.begin_nested()
|
||||||
|
create_billing_instruction()
|
||||||
|
session.add(sent_clin)
|
||||||
|
|
||||||
|
# check that last_sent_at has been update for the new clin only
|
||||||
|
assert new_clin.last_sent_at
|
||||||
|
assert sent_clin.last_sent_at != new_clin.last_sent_at
|
||||||
|
session.rollback()
|
||||||
|
@ -83,7 +83,10 @@ errors:
|
|||||||
not_found_sub: This page does not exist.
|
not_found_sub: This page does not exist.
|
||||||
email:
|
email:
|
||||||
application_invite: "{inviter_name} has invited you to a JEDI cloud application"
|
application_invite: "{inviter_name} has invited you to a JEDI cloud application"
|
||||||
portfolio_invite: "{inviter_name} has invited you to a JEDI cloud portfolio"
|
app_role_created:
|
||||||
|
subject: Application Role Created
|
||||||
|
body: "Your application role has been created.\nVisit {url}, and use your username, {username}, to log in."
|
||||||
|
portfolio_invite: "{inviter_name} has invited you to a JEDI cloud portfolio."
|
||||||
portfolio_ready:
|
portfolio_ready:
|
||||||
subject: Portfolio Provisioned
|
subject: Portfolio Provisioned
|
||||||
body: "Your portfolio has been provisioned.\nVisit {password_reset_address}, and use your username, {username}, to create a password."
|
body: "Your portfolio has been provisioned.\nVisit {password_reset_address}, and use your username, {username}, to create a password."
|
||||||
@ -268,7 +271,7 @@ forms:
|
|||||||
label: Portfolio Name
|
label: Portfolio Name
|
||||||
length_validation_message: Portfolio names can be between 4-100 characters
|
length_validation_message: Portfolio names can be between 4-100 characters
|
||||||
help_text: |
|
help_text: |
|
||||||
<div>
|
<div class="usa-input__help">
|
||||||
<p>
|
<p>
|
||||||
Naming can be difficult. Choose a name that is descriptive enough for users to identify the Portfolio. You may consider naming based on your organization.
|
Naming can be difficult. Choose a name that is descriptive enough for users to identify the Portfolio. You may consider naming based on your organization.
|
||||||
</p>
|
</p>
|
||||||
@ -282,7 +285,7 @@ forms:
|
|||||||
description:
|
description:
|
||||||
label: Portfolio Description
|
label: Portfolio Description
|
||||||
help_text: |
|
help_text: |
|
||||||
<div>
|
<div class="usa-input__help">
|
||||||
<p>
|
<p>
|
||||||
Add a brief one to two sentence description of your Portfolio. Consider this your statement of work.
|
Add a brief one to two sentence description of your Portfolio. Consider this your statement of work.
|
||||||
</p>
|
</p>
|
||||||
@ -307,11 +310,9 @@ forms:
|
|||||||
title: Select DoD component(s) funding your Portfolio
|
title: Select DoD component(s) funding your Portfolio
|
||||||
validation_message: You must select at least one defense component.
|
validation_message: You must select at least one defense component.
|
||||||
help_text: |
|
help_text: |
|
||||||
<p>
|
|
||||||
Select the DOD component(s) that will fund all Applications within this Portfolio.
|
Select the DOD component(s) that will fund all Applications within this Portfolio.
|
||||||
In JEDI, multiple DoD organizations can fund the same Portfolio.<br/>
|
In JEDI, multiple DoD organizations can fund the same Portfolio.<br/>
|
||||||
Select all that apply.<br/>
|
Select all that apply.
|
||||||
</p>
|
|
||||||
attachment:
|
attachment:
|
||||||
object_name:
|
object_name:
|
||||||
length_error: Object name may be no longer than 40 characters.
|
length_error: Object name may be no longer than 40 characters.
|
||||||
@ -515,7 +516,7 @@ portfolios:
|
|||||||
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.
|
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:
|
total_value:
|
||||||
header: Total Portfolio Value
|
header: Total Portfolio Value
|
||||||
tooltip: Total portfolio value is all obligated funds for current and upcoming task orders in this portfolio.
|
tooltip: Total portfolio value is all obligated funds for active task orders in this portfolio.
|
||||||
task_orders:
|
task_orders:
|
||||||
add_new_button: Add New Task Order
|
add_new_button: Add New Task Order
|
||||||
review:
|
review:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user