Merge branch 'staging' into environment-role-creation
This commit is contained in:
commit
3f60d3494e
@ -101,5 +101,7 @@ RUN mkdir /var/run/uwsgi && \
|
|||||||
chown -R atst:atat /var/run/uwsgi && \
|
chown -R atst:atat /var/run/uwsgi && \
|
||||||
chown -R atst:atat "${APP_DIR}"
|
chown -R atst:atat "${APP_DIR}"
|
||||||
|
|
||||||
|
RUN update-ca-certificates
|
||||||
|
|
||||||
# Run as the unprivileged APP user
|
# Run as the unprivileged APP user
|
||||||
USER atst
|
USER atst
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""change to environment_roles.cloud_Id
|
"""change to environment_roles.cloud_Id
|
||||||
|
|
||||||
Revision ID: 418b52c1cedf
|
Revision ID: 418b52c1cedf
|
||||||
Revises: 0039308c6351
|
Revises: 567bfb019a87
|
||||||
Create Date: 2020-02-05 13:40:37.870183
|
Create Date: 2020-02-05 13:40:37.870183
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@ -11,7 +11,7 @@ import sqlalchemy as sa
|
|||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
revision = '418b52c1cedf' # pragma: allowlist secret
|
revision = '418b52c1cedf' # pragma: allowlist secret
|
||||||
down_revision = '0039308c6351' # pragma: allowlist secret
|
down_revision = '567bfb019a87' # pragma: allowlist secret
|
||||||
branch_labels = None
|
branch_labels = None
|
||||||
depends_on = None
|
depends_on = None
|
||||||
|
|
||||||
|
@ -0,0 +1,29 @@
|
|||||||
|
"""add last_sent column to clins and pdf_last_sent to task_orders
|
||||||
|
|
||||||
|
Revision ID: 567bfb019a87
|
||||||
|
Revises: 0039308c6351
|
||||||
|
Create Date: 2020-01-31 14:06:21.926019
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '567bfb019a87' # pragma: allowlist secret
|
||||||
|
down_revision = '0039308c6351' # pragma: allowlist secret
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('clins', sa.Column('last_sent_at', sa.DateTime(), nullable=True))
|
||||||
|
op.add_column('task_orders', sa.Column('pdf_last_sent_at', sa.DateTime(), nullable=True))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('task_orders', 'pdf_last_sent_at')
|
||||||
|
op.drop_column('clins', 'last_sent_at')
|
||||||
|
# ### end Alembic commands ###
|
@ -43,10 +43,12 @@ class AzureFileService(FileService):
|
|||||||
|
|
||||||
from azure.storage.common import CloudStorageAccount
|
from azure.storage.common import CloudStorageAccount
|
||||||
from azure.storage.blob import BlobSasPermissions
|
from azure.storage.blob import BlobSasPermissions
|
||||||
|
from azure.storage.blob.models import BlobPermissions
|
||||||
from azure.storage.blob.blockblobservice import BlockBlobService
|
from azure.storage.blob.blockblobservice import BlockBlobService
|
||||||
|
|
||||||
self.CloudStorageAccount = CloudStorageAccount
|
self.CloudStorageAccount = CloudStorageAccount
|
||||||
self.BlobSasPermissions = BlobSasPermissions
|
self.BlobSasPermissions = BlobSasPermissions
|
||||||
|
self.BlobPermissions = BlobPermissions
|
||||||
self.BlockBlobService = BlockBlobService
|
self.BlockBlobService = BlockBlobService
|
||||||
|
|
||||||
def get_token(self):
|
def get_token(self):
|
||||||
@ -72,20 +74,22 @@ class AzureFileService(FileService):
|
|||||||
return ({"token": sas_token}, object_name)
|
return ({"token": sas_token}, object_name)
|
||||||
|
|
||||||
def generate_download_link(self, object_name, filename):
|
def generate_download_link(self, object_name, filename):
|
||||||
account = self.CloudStorageAccount(
|
block_blob_service = self.BlockBlobService(
|
||||||
account_name=self.account_name, account_key=self.storage_key
|
account_name=self.account_name, account_key=self.storage_key
|
||||||
)
|
)
|
||||||
bbs = account.create_block_blob_service()
|
sas_token = block_blob_service.generate_blob_shared_access_signature(
|
||||||
sas_token = bbs.generate_blob_shared_access_signature(
|
container_name=self.container_name,
|
||||||
self.container_name,
|
blob_name=object_name,
|
||||||
object_name,
|
permission=self.BlobPermissions(read=True),
|
||||||
permission=self.BlobSasPermissions(read=True),
|
|
||||||
expiry=datetime.utcnow() + self.timeout,
|
expiry=datetime.utcnow() + self.timeout,
|
||||||
content_disposition=f"attachment; filename={filename}",
|
content_disposition=f"attachment; filename={filename}",
|
||||||
protocol="https",
|
protocol="https",
|
||||||
)
|
)
|
||||||
return bbs.make_blob_url(
|
return block_blob_service.make_blob_url(
|
||||||
self.container_name, object_name, protocol="https", sas_token=sas_token
|
container_name=self.container_name,
|
||||||
|
blob_name=object_name,
|
||||||
|
protocol="https",
|
||||||
|
sas_token=sas_token,
|
||||||
)
|
)
|
||||||
|
|
||||||
def download_task_order(self, object_name):
|
def download_task_order(self, object_name):
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import datetime
|
from datetime import datetime
|
||||||
|
from sqlalchemy import or_
|
||||||
|
|
||||||
from atst.database import db
|
from atst.database import db
|
||||||
from atst.models.clin import CLIN
|
from atst.models.clin import CLIN
|
||||||
@ -40,7 +41,7 @@ class TaskOrders(BaseDomainClass):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def sign(cls, task_order, signer_dod_id):
|
def sign(cls, task_order, signer_dod_id):
|
||||||
task_order.signer_dod_id = signer_dod_id
|
task_order.signer_dod_id = signer_dod_id
|
||||||
task_order.signed_at = datetime.datetime.now()
|
task_order.signed_at = datetime.now()
|
||||||
|
|
||||||
db.session.add(task_order)
|
db.session.add(task_order)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
@ -76,3 +77,17 @@ class TaskOrders(BaseDomainClass):
|
|||||||
task_order = TaskOrders.get(task_order_id)
|
task_order = TaskOrders.get(task_order_id)
|
||||||
db.session.delete(task_order)
|
db.session.delete(task_order)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_for_send_task_order_files(cls):
|
||||||
|
return (
|
||||||
|
db.session.query(TaskOrder)
|
||||||
|
.join(CLIN)
|
||||||
|
.filter(
|
||||||
|
or_(
|
||||||
|
TaskOrder.pdf_last_sent_at < CLIN.last_sent_at,
|
||||||
|
TaskOrder.pdf_last_sent_at.is_(None),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
44
atst/jobs.py
44
atst/jobs.py
@ -1,5 +1,7 @@
|
|||||||
import pendulum
|
import pendulum
|
||||||
from flask import current_app as app
|
from flask import current_app as app
|
||||||
|
from smtplib import SMTPException
|
||||||
|
from azure.core.exceptions import AzureError
|
||||||
|
|
||||||
from atst.database import db
|
from atst.database import db
|
||||||
from atst.domain.application_roles import ApplicationRoles
|
from atst.domain.application_roles import ApplicationRoles
|
||||||
@ -16,8 +18,10 @@ from atst.domain.environments import Environments
|
|||||||
from atst.domain.environment_roles import EnvironmentRoles
|
from atst.domain.environment_roles import EnvironmentRoles
|
||||||
from atst.domain.portfolios import Portfolios
|
from atst.domain.portfolios import Portfolios
|
||||||
from atst.models import CSPRole, JobFailure
|
from atst.models import CSPRole, JobFailure
|
||||||
|
from atst.domain.task_orders import TaskOrders
|
||||||
from atst.models.utils import claim_for_update, claim_many_for_update
|
from atst.models.utils import claim_for_update, claim_many_for_update
|
||||||
from atst.queue import celery
|
from atst.queue import celery
|
||||||
|
from atst.utils.localization import translate
|
||||||
|
|
||||||
|
|
||||||
class RecordFailure(celery.Task):
|
class RecordFailure(celery.Task):
|
||||||
@ -45,8 +49,8 @@ class RecordFailure(celery.Task):
|
|||||||
|
|
||||||
|
|
||||||
@celery.task(ignore_result=True)
|
@celery.task(ignore_result=True)
|
||||||
def send_mail(recipients, subject, body):
|
def send_mail(recipients, subject, body, attachments=[]):
|
||||||
app.mailer.send(recipients, subject, body)
|
app.mailer.send(recipients, subject, body, attachments)
|
||||||
|
|
||||||
|
|
||||||
@celery.task(ignore_result=True)
|
@celery.task(ignore_result=True)
|
||||||
@ -244,3 +248,39 @@ def dispatch_create_environment(self):
|
|||||||
pendulum.now()
|
pendulum.now()
|
||||||
):
|
):
|
||||||
create_environment.delay(environment_id=environment_id)
|
create_environment.delay(environment_id=environment_id)
|
||||||
|
|
||||||
|
|
||||||
|
@celery.task(bind=True)
|
||||||
|
def dispatch_create_atat_admin_user(self):
|
||||||
|
for environment_id in Environments.get_environments_pending_atat_user_creation(
|
||||||
|
pendulum.now()
|
||||||
|
):
|
||||||
|
create_atat_admin_user.delay(environment_id=environment_id)
|
||||||
|
|
||||||
|
|
||||||
|
@celery.task(bind=True)
|
||||||
|
def dispatch_send_task_order_files(self):
|
||||||
|
task_orders = TaskOrders.get_for_send_task_order_files()
|
||||||
|
recipients = [app.config.get("MICROSOFT_TASK_ORDER_EMAIL_ADDRESS")]
|
||||||
|
|
||||||
|
for task_order in task_orders:
|
||||||
|
subject = translate(
|
||||||
|
"email.task_order_sent.subject", {"to_number": task_order.number}
|
||||||
|
)
|
||||||
|
body = translate("email.task_order_sent.body", {"to_number": task_order.number})
|
||||||
|
|
||||||
|
try:
|
||||||
|
file = app.csp.files.download_task_order(task_order.pdf.object_name)
|
||||||
|
file["maintype"] = "application"
|
||||||
|
file["subtype"] = "pdf"
|
||||||
|
send_mail(
|
||||||
|
recipients=recipients, subject=subject, body=body, attachments=[file]
|
||||||
|
)
|
||||||
|
except (AzureError, SMTPException) as err:
|
||||||
|
app.logger.exception(err)
|
||||||
|
continue
|
||||||
|
|
||||||
|
task_order.pdf_last_sent_at = pendulum.now()
|
||||||
|
db.session.add(task_order)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
@ -1,5 +1,13 @@
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from sqlalchemy import Column, Date, Enum as SQLAEnum, ForeignKey, Numeric, String
|
from sqlalchemy import (
|
||||||
|
Column,
|
||||||
|
Date,
|
||||||
|
DateTime,
|
||||||
|
Enum as SQLAEnum,
|
||||||
|
ForeignKey,
|
||||||
|
Numeric,
|
||||||
|
String,
|
||||||
|
)
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
||||||
@ -29,6 +37,7 @@ class CLIN(Base, mixins.TimestampsMixin):
|
|||||||
total_amount = Column(Numeric(scale=2), nullable=False)
|
total_amount = Column(Numeric(scale=2), nullable=False)
|
||||||
obligated_amount = Column(Numeric(scale=2), nullable=False)
|
obligated_amount = Column(Numeric(scale=2), nullable=False)
|
||||||
jedi_clin_type = Column(SQLAEnum(JEDICLINType, native_enum=False), nullable=False)
|
jedi_clin_type = Column(SQLAEnum(JEDICLINType, native_enum=False), nullable=False)
|
||||||
|
last_sent_at = Column(DateTime)
|
||||||
|
|
||||||
#
|
#
|
||||||
# NOTE: For now obligated CLINS are CLIN 1 + CLIN 3
|
# NOTE: For now obligated CLINS are CLIN 1 + CLIN 3
|
||||||
|
@ -61,6 +61,10 @@ class Environment(
|
|||||||
def portfolio_id(self):
|
def portfolio_id(self):
|
||||||
return self.application.portfolio_id
|
return self.application.portfolio_id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_pending(self):
|
||||||
|
return self.cloud_id is None
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<Environment(name='{}', num_users='{}', application='{}', portfolio='{}', id='{}')>".format(
|
return "<Environment(name='{}', num_users='{}', application='{}', portfolio='{}', id='{}')>".format(
|
||||||
self.name,
|
self.name,
|
||||||
|
@ -39,6 +39,7 @@ class TaskOrder(Base, mixins.TimestampsMixin):
|
|||||||
|
|
||||||
pdf_attachment_id = Column(ForeignKey("attachments.id"))
|
pdf_attachment_id = Column(ForeignKey("attachments.id"))
|
||||||
_pdf = relationship("Attachment", foreign_keys=[pdf_attachment_id])
|
_pdf = relationship("Attachment", foreign_keys=[pdf_attachment_id])
|
||||||
|
pdf_last_sent_at = Column(DateTime)
|
||||||
number = Column(String, unique=True,) # Task Order Number
|
number = Column(String, unique=True,) # Task Order Number
|
||||||
signer_dod_id = Column(String)
|
signer_dod_id = Column(String)
|
||||||
signed_at = Column(DateTime)
|
signed_at = Column(DateTime)
|
||||||
|
@ -39,6 +39,7 @@ def get_environments_obj_for_app(application):
|
|||||||
{
|
{
|
||||||
"id": env.id,
|
"id": env.id,
|
||||||
"name": env.name,
|
"name": env.name,
|
||||||
|
"pending": env.is_pending,
|
||||||
"edit_form": EditEnvironmentForm(obj=env),
|
"edit_form": EditEnvironmentForm(obj=env),
|
||||||
"member_count": len(env.roles),
|
"member_count": len(env.roles),
|
||||||
"members": sorted(
|
"members": sorted(
|
||||||
|
@ -19,6 +19,9 @@ from atst.models import (
|
|||||||
def get_resources_from_context(view_args):
|
def get_resources_from_context(view_args):
|
||||||
query = None
|
query = None
|
||||||
|
|
||||||
|
if view_args is None:
|
||||||
|
view_args = {}
|
||||||
|
|
||||||
if "portfolio_token" in view_args:
|
if "portfolio_token" in view_args:
|
||||||
query = (
|
query = (
|
||||||
db.session.query(Portfolio)
|
db.session.query(Portfolio)
|
||||||
|
@ -17,7 +17,7 @@ export default {
|
|||||||
filename: {
|
filename: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
objectName: {
|
initialObjectName: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
initialErrors: {
|
initialErrors: {
|
||||||
@ -42,6 +42,7 @@ export default {
|
|||||||
filenameError: false,
|
filenameError: false,
|
||||||
downloadLink: '',
|
downloadLink: '',
|
||||||
fileSizeLimit: this.sizeLimit,
|
fileSizeLimit: this.sizeLimit,
|
||||||
|
objectName: this.initialObjectName,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -72,6 +73,7 @@ export default {
|
|||||||
const response = await uploader.upload(file)
|
const response = await uploader.upload(file)
|
||||||
if (uploadResponseOkay(response)) {
|
if (uploadResponseOkay(response)) {
|
||||||
this.attachment = e.target.value
|
this.attachment = e.target.value
|
||||||
|
this.objectName = uploader.objectName
|
||||||
this.$refs.attachmentFilename.value = file.name
|
this.$refs.attachmentFilename.value = file.name
|
||||||
this.$refs.attachmentObjectName.value = response.objectName
|
this.$refs.attachmentObjectName.value = response.objectName
|
||||||
this.$refs.attachmentInput.disabled = true
|
this.$refs.attachmentInput.disabled = true
|
||||||
|
@ -9,6 +9,12 @@ export default {
|
|||||||
unmask: [],
|
unmask: [],
|
||||||
validationError: 'Please enter a response',
|
validationError: 'Please enter a response',
|
||||||
},
|
},
|
||||||
|
applicationName: {
|
||||||
|
mask: false,
|
||||||
|
match: /^[A-Za-z0-9\-_,'".\s]{4,100}$$/,
|
||||||
|
unmask: [],
|
||||||
|
validationError: 'Application names can be between 4-100 characters',
|
||||||
|
},
|
||||||
clinNumber: {
|
clinNumber: {
|
||||||
mask: false,
|
mask: false,
|
||||||
match: /^\d{4}$/,
|
match: /^\d{4}$/,
|
||||||
@ -42,14 +48,14 @@ export default {
|
|||||||
},
|
},
|
||||||
defaultStringField: {
|
defaultStringField: {
|
||||||
mask: false,
|
mask: false,
|
||||||
match: /^[A-Za-z0-9\-_ \.]{1,100}$/,
|
match: /^[A-Za-z0-9\-_,'".\s]{1,1000}$/,
|
||||||
unmask: [],
|
unmask: [],
|
||||||
validationError:
|
validationError:
|
||||||
'Please enter a response of no more than 100 alphanumeric characters',
|
'Please enter a response of no more than 100 alphanumeric characters',
|
||||||
},
|
},
|
||||||
defaultTextAreaField: {
|
defaultTextAreaField: {
|
||||||
mask: false,
|
mask: false,
|
||||||
match: /^[A-Za-z0-9\-_ \.]{1,1000}$/,
|
match: /^[A-Za-z0-9\-_,'".\s]{1,1000}$/,
|
||||||
unmask: [],
|
unmask: [],
|
||||||
validationError:
|
validationError:
|
||||||
'Please enter a response of no more than 1000 alphanumeric characters',
|
'Please enter a response of no more than 1000 alphanumeric characters',
|
||||||
@ -94,7 +100,7 @@ export default {
|
|||||||
},
|
},
|
||||||
portfolioName: {
|
portfolioName: {
|
||||||
mask: false,
|
mask: false,
|
||||||
match: /^.{4,100}$/,
|
match: /^[A-Za-z0-9\-_,'".\s]{4,100}$$/,
|
||||||
unmask: [],
|
unmask: [],
|
||||||
validationError: 'Portfolio names can be between 4-100 characters',
|
validationError: 'Portfolio names can be between 4-100 characters',
|
||||||
},
|
},
|
||||||
|
@ -98,3 +98,7 @@ hr {
|
|||||||
.usa-section {
|
.usa-section {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
margin-bottom: $action-footer-height + $large-spacing;
|
||||||
|
}
|
||||||
|
@ -20,6 +20,7 @@ $max-panel-width: 90rem;
|
|||||||
$home-pg-icon-width: 6rem;
|
$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;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* USWDS Variables
|
* USWDS Variables
|
||||||
|
@ -42,6 +42,7 @@
|
|||||||
border-top: 1px solid $color-gray-lighter;
|
border-top: 1px solid $color-gray-lighter;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: $action-footer-height;
|
||||||
|
|
||||||
&.action-group-footer--expand-offset {
|
&.action-group-footer--expand-offset {
|
||||||
padding-left: $sidenav-expanded-width;
|
padding-left: $sidenav-expanded-width;
|
||||||
|
@ -228,6 +228,7 @@
|
|||||||
|
|
||||||
&--validation {
|
&--validation {
|
||||||
&--anything,
|
&--anything,
|
||||||
|
&--applicationName,
|
||||||
&--portfolioName,
|
&--portfolioName,
|
||||||
&--requiredField,
|
&--requiredField,
|
||||||
&--defaultStringField,
|
&--defaultStringField,
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
.task-order {
|
.task-order {
|
||||||
margin-top: $gap * 4;
|
margin-top: $gap * 4;
|
||||||
margin-bottom: $footer-height;
|
|
||||||
width: 900px;
|
width: 900px;
|
||||||
|
|
||||||
&__amount {
|
&__amount {
|
||||||
|
@ -47,7 +47,7 @@
|
|||||||
<span>
|
<span>
|
||||||
{{ env['name'] }}
|
{{ env['name'] }}
|
||||||
</span>
|
</span>
|
||||||
{{ Label(type="pending_creation", classes='label--below')}}
|
{{ Label(type="pending_creation")}}
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
{% if user_can(permissions.EDIT_ENVIRONMENT) -%}
|
{% if user_can(permissions.EDIT_ENVIRONMENT) -%}
|
||||||
{{
|
{{
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
action_new,
|
action_new,
|
||||||
action_update) %}
|
action_update) %}
|
||||||
|
|
||||||
<h3 id="application-members">
|
<h3 id="application-members">
|
||||||
{{ 'portfolios.applications.settings.team_members' | translate }}
|
{{ 'portfolios.applications.settings.team_members' | translate }}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
@ -22,7 +22,7 @@
|
|||||||
{% include "fragments/flash.html" %}
|
{% include "fragments/flash.html" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="panel">
|
<div class="panel{% if new_member_form%} form{% endif %}">
|
||||||
{% if not application.members %}
|
{% if not application.members %}
|
||||||
<div class='empty-state empty-state--centered empty-state--white panel__content'>
|
<div class='empty-state empty-state--centered empty-state--white panel__content'>
|
||||||
<p class='empty-state__message'>
|
<p class='empty-state__message'>
|
||||||
|
@ -22,11 +22,11 @@
|
|||||||
{% 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">
|
<form method="POST" action="{{ action }}" v-on:submit="handleSubmit" class="form">
|
||||||
{{ form.csrf_token }}
|
{{ form.csrf_token }}
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-col">
|
<div class="form-col">
|
||||||
{{ TextInput(form.name, validation="name", optional=False) }}
|
{{ TextInput(form.name, validation="applicationName", optional=False) }}
|
||||||
{{ ('portfolios.applications.new.step_1_form_help_text.name' | translate | safe) }}
|
{{ ('portfolios.applications.new.step_1_form_help_text.name' | translate | safe) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -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">
|
<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">
|
||||||
<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">
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
<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="col col--half">
|
||||||
{{ application_form.csrf_token }}
|
{{ application_form.csrf_token }}
|
||||||
{{ TextInput(application_form.name, validation="name", 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) }}
|
||||||
<div class="action-group action-group--tight">
|
<div class="action-group action-group--tight">
|
||||||
{{ SaveButton(text='common.save_changes'|translate) }}
|
{{ SaveButton(text='common.save_changes'|translate) }}
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
inline-template
|
inline-template
|
||||||
{% if not field.errors %}
|
{% if not field.errors %}
|
||||||
v-bind:filename='{{ field.filename.data | tojson }}'
|
v-bind:filename='{{ field.filename.data | tojson }}'
|
||||||
v-bind:object-name='{{ field.object_name.data | tojson }}'
|
v-bind:initial-object-name='{{ field.object_name.data | tojson }}'
|
||||||
{% else %}
|
{% else %}
|
||||||
v-bind:initial-errors='true'
|
v-bind:initial-errors='true'
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -46,7 +46,7 @@
|
|||||||
v-bind:value="attachment"
|
v-bind:value="attachment"
|
||||||
type="file">
|
type="file">
|
||||||
<input type="hidden" name="{{ field.filename.name }}" id="{{ field.filename.name }}" ref="attachmentFilename">
|
<input type="hidden" name="{{ field.filename.name }}" id="{{ field.filename.name }}" ref="attachmentFilename">
|
||||||
<input type="hidden" name="{{ field.object_name.name }}" id="{{ field.object_name.name }}" ref="attachmentObjectName">
|
<input type="hidden" name="{{ field.object_name.name }}" id="{{ field.object_name.name }}" ref="attachmentObjectName" v-bind:value='objectName'>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="uploadError">
|
<template v-if="uploadError">
|
||||||
<span class="usa-input__message">{{ "forms.task_order.upload_error" | translate }}</span>
|
<span class="usa-input__message">{{ "forms.task_order.upload_error" | translate }}</span>
|
||||||
|
@ -19,11 +19,11 @@
|
|||||||
{{ 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" action="{{ url_for('portfolios.create_portfolio') }}" method="POST">
|
<form id="portfolio-create" class="col form" 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 form-row--bordered">
|
||||||
<div class="form-col">
|
<div class="form-col">
|
||||||
{{ TextInput(form.name, validation="name", 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>
|
||||||
|
@ -28,7 +28,7 @@
|
|||||||
|
|
||||||
{% include "fragments/flash.html" %}
|
{% include "fragments/flash.html" %}
|
||||||
|
|
||||||
<div class="task-order">
|
<div class="task-order form">
|
||||||
{% block to_builder_form_field %}{% endblock %}
|
{% block to_builder_form_field %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from datetime import date, timedelta
|
from datetime import date, datetime, timedelta
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from atst.domain.exceptions import AlreadyExistsError
|
from atst.domain.exceptions import AlreadyExistsError
|
||||||
@ -178,3 +178,21 @@ def test_allows_alphanumeric_number():
|
|||||||
|
|
||||||
for number in valid_to_numbers:
|
for number in valid_to_numbers:
|
||||||
assert TaskOrders.create(portfolio.id, number, [], None)
|
assert TaskOrders.create(portfolio.id, number, [], None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_for_send_task_order_files():
|
||||||
|
new_to = TaskOrderFactory.create(create_clins=[{}])
|
||||||
|
updated_to = TaskOrderFactory.create(
|
||||||
|
create_clins=[{"last_sent_at": datetime(2020, 2, 1)}],
|
||||||
|
pdf_last_sent_at=datetime(2020, 1, 1),
|
||||||
|
)
|
||||||
|
sent_to = TaskOrderFactory.create(
|
||||||
|
create_clins=[{"last_sent_at": datetime(2020, 1, 1)}],
|
||||||
|
pdf_last_sent_at=datetime(2020, 1, 1),
|
||||||
|
)
|
||||||
|
|
||||||
|
updated_and_new_task_orders = TaskOrders.get_for_send_task_order_files()
|
||||||
|
assert len(updated_and_new_task_orders) == 2
|
||||||
|
assert sent_to not in updated_and_new_task_orders
|
||||||
|
assert updated_to in updated_and_new_task_orders
|
||||||
|
assert new_to in updated_and_new_task_orders
|
||||||
|
@ -322,6 +322,7 @@ class TaskOrderFactory(Base):
|
|||||||
number = factory.LazyFunction(random_task_order_number)
|
number = factory.LazyFunction(random_task_order_number)
|
||||||
signed_at = None
|
signed_at = None
|
||||||
_pdf = factory.SubFactory(AttachmentFactory)
|
_pdf = factory.SubFactory(AttachmentFactory)
|
||||||
|
pdf_last_sent_at = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _create(cls, model_class, *args, **kwargs):
|
def _create(cls, model_class, *args, **kwargs):
|
||||||
@ -347,6 +348,7 @@ class CLINFactory(Base):
|
|||||||
jedi_clin_type = factory.LazyFunction(
|
jedi_clin_type = factory.LazyFunction(
|
||||||
lambda *args: random.choice(list(clin.JEDICLINType))
|
lambda *args: random.choice(list(clin.JEDICLINType))
|
||||||
)
|
)
|
||||||
|
last_sent_at = None
|
||||||
|
|
||||||
|
|
||||||
class NotificationRecipientFactory(Base):
|
class NotificationRecipientFactory(Base):
|
||||||
|
@ -2,6 +2,8 @@ import pendulum
|
|||||||
import pytest
|
import pytest
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from unittest.mock import Mock, MagicMock
|
from unittest.mock import Mock, MagicMock
|
||||||
|
from smtplib import SMTPException
|
||||||
|
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 UserRoleCSPResult
|
||||||
@ -15,6 +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_environment,
|
create_environment,
|
||||||
do_create_user,
|
do_create_user,
|
||||||
do_provision_portfolio,
|
do_provision_portfolio,
|
||||||
@ -23,15 +26,17 @@ from atst.jobs import (
|
|||||||
do_create_application,
|
do_create_application,
|
||||||
)
|
)
|
||||||
from tests.factories import (
|
from tests.factories import (
|
||||||
|
ApplicationFactory,
|
||||||
|
ApplicationRoleFactory,
|
||||||
EnvironmentFactory,
|
EnvironmentFactory,
|
||||||
EnvironmentRoleFactory,
|
EnvironmentRoleFactory,
|
||||||
PortfolioFactory,
|
PortfolioFactory,
|
||||||
PortfolioStateMachineFactory,
|
PortfolioStateMachineFactory,
|
||||||
ApplicationFactory,
|
TaskOrderFactory,
|
||||||
ApplicationRoleFactory,
|
|
||||||
UserFactory,
|
UserFactory,
|
||||||
)
|
)
|
||||||
from atst.models import CSPRole, EnvironmentRole, ApplicationRoleStatus, JobFailure
|
from atst.models import CSPRole, EnvironmentRole, ApplicationRoleStatus, JobFailure
|
||||||
|
from atst.utils.localization import translate
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True, scope="function")
|
@pytest.fixture(autouse=True, scope="function")
|
||||||
@ -326,3 +331,84 @@ def test_create_environment_role():
|
|||||||
do_create_environment_role(csp, environment_role_id=env_role.id)
|
do_create_environment_role(csp, environment_role_id=env_role.id)
|
||||||
|
|
||||||
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
|
||||||
|
# and separate the success test into two tests
|
||||||
|
def test_dispatch_send_task_order_files(monkeypatch, app):
|
||||||
|
mock = Mock()
|
||||||
|
monkeypatch.setattr("atst.jobs.send_mail", mock)
|
||||||
|
|
||||||
|
def _download_task_order(MockFileService, object_name):
|
||||||
|
return {"name": object_name}
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"atst.domain.csp.files.MockFileService.download_task_order",
|
||||||
|
_download_task_order,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create 3 new Task Orders
|
||||||
|
for i in range(3):
|
||||||
|
TaskOrderFactory.create(create_clins=[{"number": "0001"}])
|
||||||
|
|
||||||
|
dispatch_send_task_order_files.run()
|
||||||
|
|
||||||
|
# Check that send_with_attachment was called once for each task order
|
||||||
|
assert mock.call_count == 3
|
||||||
|
mock.reset_mock()
|
||||||
|
|
||||||
|
# Create new TO
|
||||||
|
task_order = TaskOrderFactory.create(create_clins=[{"number": "0001"}])
|
||||||
|
assert not task_order.pdf_last_sent_at
|
||||||
|
|
||||||
|
dispatch_send_task_order_files.run()
|
||||||
|
|
||||||
|
# Check that send_with_attachment was called with correct kwargs
|
||||||
|
mock.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
|
||||||
|
|
||||||
|
|
||||||
|
def test_dispatch_send_task_order_files_send_failure(monkeypatch):
|
||||||
|
def _raise_smtp_exception(**kwargs):
|
||||||
|
raise SMTPException
|
||||||
|
|
||||||
|
monkeypatch.setattr("atst.jobs.send_mail", _raise_smtp_exception)
|
||||||
|
|
||||||
|
task_order = TaskOrderFactory.create(create_clins=[{"number": "0001"}])
|
||||||
|
dispatch_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_download_failure(monkeypatch):
|
||||||
|
mock = Mock()
|
||||||
|
monkeypatch.setattr("atst.jobs.send_mail", mock)
|
||||||
|
|
||||||
|
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"}])
|
||||||
|
dispatch_send_task_order_files.run()
|
||||||
|
|
||||||
|
# Check that pdf_last_sent_at has not been updated
|
||||||
|
assert not task_order.pdf_last_sent_at
|
||||||
|
Loading…
x
Reference in New Issue
Block a user