Merge branch 'staging' of github-DDS:dod-ccpo/atst into gi-updates-wo-20191216

This commit is contained in:
Jay R. Newlin (PromptWorks)
2019-12-20 10:37:43 -05:00
72 changed files with 877 additions and 701 deletions

View File

@@ -3,7 +3,7 @@
"files": "^.secrets.baseline$|^.*pgsslrootcert.yml$", "files": "^.secrets.baseline$|^.*pgsslrootcert.yml$",
"lines": null "lines": null
}, },
"generated_at": "2019-12-13T20:38:57Z", "generated_at": "2019-12-18T15:29:41Z",
"plugins_used": [ "plugins_used": [
{ {
"base64_limit": 4.5, "base64_limit": 4.5,
@@ -170,7 +170,7 @@
"hashed_secret": "e4f14805dfd1e6af030359090c535e149e6b4207", "hashed_secret": "e4f14805dfd1e6af030359090c535e149e6b4207",
"is_secret": false, "is_secret": false,
"is_verified": false, "is_verified": false,
"line_number": 659, "line_number": 665,
"type": "Hex High Entropy String" "type": "Hex High Entropy String"
} }
] ]

View File

@@ -0,0 +1,26 @@
"""add uniqueness contraint to environment within an application
Revision ID: 08f2a640e9c2
Revises: c487d91f1a26
Create Date: 2019-12-16 10:43:12.331095
"""
from alembic import op
# revision identifiers, used by Alembic.
revision = '08f2a640e9c2' # pragma: allowlist secret
down_revision = 'c487d91f1a26' # pragma: allowlist secret
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_unique_constraint('environments_name_application_id_key', 'environments', ['name', 'application_id'])
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint('environments_name_application_id_key', 'environments', type_='unique')
# ### end Alembic commands ###

View File

@@ -11,7 +11,7 @@ from atst.models import (
ApplicationRoleStatus, ApplicationRoleStatus,
EnvironmentRole, EnvironmentRole,
) )
from atst.utils import first_or_none, update_or_raise_already_exists_error from atst.utils import first_or_none, commit_or_raise_already_exists_error
class Applications(BaseDomainClass): class Applications(BaseDomainClass):
@@ -28,7 +28,7 @@ class Applications(BaseDomainClass):
if environment_names: if environment_names:
Environments.create_many(user, application, environment_names) Environments.create_many(user, application, environment_names)
update_or_raise_already_exists_error(message="application") commit_or_raise_already_exists_error(message="application")
return application return application
@classmethod @classmethod
@@ -55,7 +55,7 @@ class Applications(BaseDomainClass):
) )
db.session.add(application) db.session.add(application)
update_or_raise_already_exists_error(message="application") commit_or_raise_already_exists_error(message="application")
return application return application
@classmethod @classmethod

View File

@@ -12,6 +12,7 @@ from atst.models import (
CLIN, CLIN,
) )
from atst.domain.environment_roles import EnvironmentRoles from atst.domain.environment_roles import EnvironmentRoles
from atst.utils import commit_or_raise_already_exists_error
from .exceptions import NotFoundError, DisabledError from .exceptions import NotFoundError, DisabledError
@@ -21,7 +22,7 @@ class Environments(object):
def create(cls, user, application, name): def create(cls, user, application, name):
environment = Environment(application=application, name=name, creator=user) environment = Environment(application=application, name=name, creator=user)
db.session.add(environment) db.session.add(environment)
db.session.commit() commit_or_raise_already_exists_error(message="environment")
return environment return environment
@classmethod @classmethod
@@ -39,7 +40,8 @@ class Environments(object):
if name is not None: if name is not None:
environment.name = name environment.name = name
db.session.add(environment) db.session.add(environment)
db.session.commit() commit_or_raise_already_exists_error(message="environment")
return environment
@classmethod @classmethod
def get(cls, environment_id): def get(cls, environment_id):

View File

@@ -4,7 +4,7 @@ from atst.database import db
from atst.models.clin import CLIN from atst.models.clin import CLIN
from atst.models.task_order import TaskOrder, SORT_ORDERING from atst.models.task_order import TaskOrder, SORT_ORDERING
from . import BaseDomainClass from . import BaseDomainClass
from atst.utils import update_or_raise_already_exists_error from atst.utils import commit_or_raise_already_exists_error
class TaskOrders(BaseDomainClass): class TaskOrders(BaseDomainClass):
@@ -15,7 +15,7 @@ class TaskOrders(BaseDomainClass):
def create(cls, portfolio_id, number, clins, pdf): def create(cls, portfolio_id, number, clins, pdf):
task_order = TaskOrder(portfolio_id=portfolio_id, number=number, pdf=pdf) task_order = TaskOrder(portfolio_id=portfolio_id, number=number, pdf=pdf)
db.session.add(task_order) db.session.add(task_order)
update_or_raise_already_exists_error(message="task_order") commit_or_raise_already_exists_error(message="task_order")
TaskOrders.create_clins(task_order.id, clins) TaskOrders.create_clins(task_order.id, clins)
return task_order return task_order
@@ -34,7 +34,7 @@ class TaskOrders(BaseDomainClass):
task_order.number = number task_order.number = number
db.session.add(task_order) db.session.add(task_order)
update_or_raise_already_exists_error(message="task_order") commit_or_raise_already_exists_error(message="task_order")
return task_order return task_order
@classmethod @classmethod

View File

@@ -151,3 +151,6 @@ class SignatureForm(BaseForm):
translate("task_orders.sign.digital_signature_description"), translate("task_orders.sign.digital_signature_description"),
validators=[Required()], validators=[Required()],
) )
confirm = BooleanField(
translate("task_orders.sign.confirmation_description"), validators=[Required()],
)

View File

@@ -1,4 +1,4 @@
from sqlalchemy import Column, ForeignKey, String, TIMESTAMP from sqlalchemy import Column, ForeignKey, String, TIMESTAMP, UniqueConstraint
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.dialects.postgresql import JSONB
from enum import Enum from enum import Enum
@@ -38,6 +38,12 @@ class Environment(
primaryjoin="and_(EnvironmentRole.environment_id == Environment.id, EnvironmentRole.deleted == False)", primaryjoin="and_(EnvironmentRole.environment_id == Environment.id, EnvironmentRole.deleted == False)",
) )
__table_args__ = (
UniqueConstraint(
"name", "application_id", name="environments_name_application_id_key"
),
)
class ProvisioningStatus(Enum): class ProvisioningStatus(Enum):
PENDING = "pending" PENDING = "pending"
COMPLETED = "completed" COMPLETED = "completed"

View File

@@ -1,9 +1,7 @@
from flask import redirect, render_template, request as http_request, url_for, g from flask import redirect, render_template, request as http_request, url_for
from .blueprint import applications_bp from .blueprint import applications_bp
from atst.domain.applications import Applications from atst.domain.applications import Applications
from atst.domain.exceptions import AlreadyExistsError
from atst.domain.portfolios import Portfolios
from atst.forms.application import NameAndDescriptionForm, EnvironmentsForm from atst.forms.application import NameAndDescriptionForm, EnvironmentsForm
from atst.domain.authz.decorator import user_can_access_decorator as user_can from atst.domain.authz.decorator import user_can_access_decorator as user_can
from atst.models.permissions import Permissions from atst.models.permissions import Permissions
@@ -13,6 +11,7 @@ from atst.routes.applications.settings import (
get_new_member_form, get_new_member_form,
handle_create_member, handle_create_member,
handle_update_member, handle_update_member,
handle_update_application,
) )
@@ -38,31 +37,6 @@ def render_new_application_form(
return render_template(template, **render_args) return render_template(template, **render_args)
def update_application(form, application_id=None, portfolio_id=None):
if form.validate():
application = None
try:
if application_id:
application = Applications.get(application_id)
application = Applications.update(application, form.data)
flash("application_updated", application_name=application.name)
else:
portfolio = Portfolios.get_for_update(portfolio_id)
application = Applications.create(
g.current_user, portfolio, **form.data
)
flash("application_created", application_name=application.name)
return application
except AlreadyExistsError:
flash("application_name_error", name=form.data["name"])
return False
else:
return False
@applications_bp.route("/portfolios/<portfolio_id>/applications/new") @applications_bp.route("/portfolios/<portfolio_id>/applications/new")
@applications_bp.route("/applications/<application_id>/new/step_1") @applications_bp.route("/applications/<application_id>/new/step_1")
@user_can(Permissions.CREATE_APPLICATION, message="view create new application form") @user_can(Permissions.CREATE_APPLICATION, message="view create new application form")
@@ -90,7 +64,7 @@ def create_or_update_new_application_step_1(portfolio_id=None, application_id=No
form = get_new_application_form( form = get_new_application_form(
{**http_request.form}, NameAndDescriptionForm, application_id {**http_request.form}, NameAndDescriptionForm, application_id
) )
application = update_application(form, application_id, portfolio_id) application = handle_update_application(form, application_id, portfolio_id)
if application: if application:
return redirect( return redirect(

View File

@@ -1,4 +1,10 @@
from flask import redirect, render_template, request as http_request, url_for, g from flask import (
redirect,
render_template,
request as http_request,
url_for,
g,
)
from .blueprint import applications_bp from .blueprint import applications_bp
from atst.domain.exceptions import AlreadyExistsError from atst.domain.exceptions import AlreadyExistsError
@@ -10,6 +16,7 @@ from atst.domain.csp.cloud import GeneralCSPException
from atst.domain.common import Paginator from atst.domain.common import Paginator
from atst.domain.environment_roles import EnvironmentRoles from atst.domain.environment_roles import EnvironmentRoles
from atst.domain.invitations import ApplicationInvitations from atst.domain.invitations import ApplicationInvitations
from atst.domain.portfolios import Portfolios
from atst.forms.application_member import NewForm as NewMemberForm, UpdateMemberForm from atst.forms.application_member import NewForm as NewMemberForm, UpdateMemberForm
from atst.forms.application import NameAndDescriptionForm, EditEnvironmentForm from atst.forms.application import NameAndDescriptionForm, EditEnvironmentForm
from atst.forms.data import ENV_ROLE_NO_ACCESS as NO_ACCESS from atst.forms.data import ENV_ROLE_NO_ACCESS as NO_ACCESS
@@ -245,16 +252,59 @@ def handle_update_member(application_id, application_role_id, form_data):
# TODO: flash error message # TODO: flash error message
def handle_update_environment(form, application=None, environment=None):
if form.validate():
try:
if environment:
environment = Environments.update(
environment=environment, name=form.name.data
)
flash("application_environments_updated")
else:
environment = Environments.create(
g.current_user, application=application, name=form.name.data
)
flash("environment_added", environment_name=form.name.data)
return environment
except AlreadyExistsError:
flash("application_environments_name_error", name=form.name.data)
return False
else:
return False
def handle_update_application(form, application_id=None, portfolio_id=None):
if form.validate():
application = None
try:
if application_id:
application = Applications.get(application_id)
application = Applications.update(application, form.data)
flash("application_updated", application_name=application.name)
else:
portfolio = Portfolios.get_for_update(portfolio_id)
application = Applications.create(
g.current_user, portfolio, **form.data
)
flash("application_created", application_name=application.name)
return application
except AlreadyExistsError:
flash("application_name_error", name=form.data["name"])
return False
@applications_bp.route("/applications/<application_id>/settings") @applications_bp.route("/applications/<application_id>/settings")
@user_can(Permissions.VIEW_APPLICATION, message="view application edit form") @user_can(Permissions.VIEW_APPLICATION, message="view application edit form")
def settings(application_id): def settings(application_id):
application = Applications.get(application_id) application = Applications.get(application_id)
return render_settings_page( return render_settings_page(application=application,)
application=application,
active_toggler=http_request.args.get("active_toggler"),
active_toggler_section=http_request.args.get("active_toggler_section"),
)
@applications_bp.route("/environments/<environment_id>/edit", methods=["POST"]) @applications_bp.route("/environments/<environment_id>/edit", methods=["POST"])
@@ -264,31 +314,21 @@ def update_environment(environment_id):
application = environment.application application = environment.application
env_form = EditEnvironmentForm(obj=environment, formdata=http_request.form) env_form = EditEnvironmentForm(obj=environment, formdata=http_request.form)
updated_environment = handle_update_environment(
form=env_form, application=application, environment=environment
)
if env_form.validate(): if updated_environment:
Environments.update(environment=environment, name=env_form.name.data)
flash("application_environments_updated")
return redirect( return redirect(
url_for( url_for(
"applications.settings", "applications.settings",
application_id=application.id, application_id=application.id,
fragment="application-environments", fragment="application-environments",
_anchor="application-environments", _anchor="application-environments",
active_toggler=environment.id,
active_toggler_section="edit",
) )
) )
else: else:
return ( return (render_settings_page(application=application, show_flash=True), 400)
render_settings_page(
application=application,
active_toggler=environment.id,
active_toggler_section="edit",
),
400,
)
@applications_bp.route( @applications_bp.route(
@@ -298,14 +338,9 @@ def update_environment(environment_id):
def new_environment(application_id): def new_environment(application_id):
application = Applications.get(application_id) application = Applications.get(application_id)
env_form = EditEnvironmentForm(formdata=http_request.form) env_form = EditEnvironmentForm(formdata=http_request.form)
environment = handle_update_environment(form=env_form, application=application)
if env_form.validate(): if environment:
Environments.create(
g.current_user, application=application, name=env_form.name.data
)
flash("environment_added", environment_name=env_form.data["name"])
return redirect( return redirect(
url_for( url_for(
"applications.settings", "applications.settings",
@@ -315,7 +350,7 @@ def new_environment(application_id):
) )
) )
else: else:
return (render_settings_page(application=application), 400) return (render_settings_page(application=application, show_flash=True), 400)
@applications_bp.route("/applications/<application_id>/edit", methods=["POST"]) @applications_bp.route("/applications/<application_id>/edit", methods=["POST"])
@@ -323,10 +358,9 @@ def new_environment(application_id):
def update(application_id): def update(application_id):
application = Applications.get(application_id) application = Applications.get(application_id)
form = NameAndDescriptionForm(http_request.form) form = NameAndDescriptionForm(http_request.form)
if form.validate(): updated_application = handle_update_application(form, application_id)
application_data = form.data
Applications.update(application, application_data)
if updated_application:
return redirect( return redirect(
url_for( url_for(
"applications.portfolio_applications", "applications.portfolio_applications",
@@ -334,21 +368,9 @@ def update(application_id):
) )
) )
else: else:
return render_settings_page(application=application, application_form=form) return (
render_settings_page(application=application, show_flash=True),
400,
@applications_bp.route("/applications/<application_id>/delete", methods=["POST"])
@user_can(Permissions.DELETE_APPLICATION, message="delete application")
def delete(application_id):
application = Applications.get(application_id)
Applications.delete(application)
flash("application_deleted", application_name=application.name)
return redirect(
url_for(
"applications.portfolio_applications", portfolio_id=application.portfolio_id
)
) )

View File

@@ -56,13 +56,3 @@ def reports(portfolio_id):
monthly_spending=Reports.monthly_spending(portfolio), monthly_spending=Reports.monthly_spending(portfolio),
retrieved=datetime.now(), # mocked datetime of reporting data retrival retrieved=datetime.now(), # mocked datetime of reporting data retrival
) )
@portfolios_bp.route("/portfolios/<portfolio_id>/destroy", methods=["POST"])
@user_can(Permissions.ARCHIVE_PORTFOLIO, message="archive portfolio")
def delete_portfolio(portfolio_id):
Portfolios.delete(portfolio=g.portfolio)
flash("portfolio_deleted", portfolio_name=g.portfolio.name)
return redirect(url_for("atst.home"))

View File

@@ -30,7 +30,7 @@ def pick(keys, dct):
return {k: v for (k, v) in dct.items() if k in _keys} return {k: v for (k, v) in dct.items() if k in _keys}
def update_or_raise_already_exists_error(message): def commit_or_raise_already_exists_error(message):
try: try:
db.session.commit() db.session.commit()
except IntegrityError: except IntegrityError:

View File

@@ -29,6 +29,11 @@ MESSAGES = {
""", """,
"category": "success", "category": "success",
}, },
"application_environments_name_error": {
"title_template": "",
"message_template": """{{ 'flash.application.env_name_error.message' | translate({ 'name': name }) }}""",
"category": "error",
},
"application_environments_updated": { "application_environments_updated": {
"title_template": "Application environments updated", "title_template": "Application environments updated",
"message_template": "Application environments have been updated", "message_template": "Application environments have been updated",

View File

@@ -72,15 +72,18 @@ $CONTAINER_IMAGE \
# Use curl to wait for application container to become available # Use curl to wait for application container to become available
docker pull curlimages/curl:latest docker pull curlimages/curl:latest
echo "Waiting for application container to become available"
docker run --network atat \ docker run --network atat \
curlimages/curl:latest \ curlimages/curl:latest \
curl --connect-timeout 3 \ curl \
--silent \
--connect-timeout 3 \
--max-time 5 \ --max-time 5 \
--retry $CONTAINER_TIMEOUT \ --retry $CONTAINER_TIMEOUT \
--retry-connrefused \ --retry-connrefused \
--retry-delay 1 \ --retry-delay 1 \
--retry-max-time $CONTAINER_TIMEOUT \ --retry-max-time $CONTAINER_TIMEOUT \
test-atat:8000 test-atat:8000 >/dev/null
# Run Ghost Inspector tests # Run Ghost Inspector tests
docker pull ghostinspector/test-runner-standalone:latest docker pull ghostinspector/test-runner-standalone:latest

View File

@@ -1,7 +1,8 @@
// Form Grid // Form Grid
.form-row { .form-row {
margin: ($gap * 4) 0; margin: ($gap * 4) 0;
&--separated {
&--bordered {
border-bottom: $color-gray-lighter 1px solid; border-bottom: $color-gray-lighter 1px solid;
} }

View File

@@ -88,5 +88,9 @@ p {
hr { hr {
border: 0; border: 0;
border-bottom: 1px solid $color-gray-light; border-bottom: 1px solid $color-gray-light;
margin: ($gap * 3) 0;
&.full-width {
margin: ($gap * 3) ($site-margins * -4); margin: ($gap * 3) ($site-margins * -4);
} }
}

View File

@@ -17,6 +17,7 @@ $usa-banner-height: 2.8rem;
$sidenav-expanded-width: 25rem; $sidenav-expanded-width: 25rem;
$sidenav-collapsed-width: 10rem; $sidenav-collapsed-width: 10rem;
$max-panel-width: 80rem; $max-panel-width: 80rem;
$home-pg-icon-width: 6rem;
/* /*
* USWDS Variables * USWDS Variables

View File

@@ -46,7 +46,7 @@
background: white; background: white;
right: 0; right: 0;
padding-right: $gap * 4; padding-right: $gap * 4;
border-top: 1px solid $color-gray-light; border-top: 1px solid $color-gray-lighter;
width: 100%; width: 100%;
z-index: 1; z-index: 1;
} }

View File

@@ -94,4 +94,19 @@
&--primary { &--primary {
@include icon-color($color-primary); @include icon-color($color-primary);
} }
&--home-pg-badge {
@include icon-size(27);
@include icon-color($color-white);
background-color: $color-primary;
height: $home-pg-icon-width;
width: $home-pg-icon-width;
border-radius: 100%;
display: inline-flex;
svg {
margin: auto;
}
}
} }

View File

@@ -165,6 +165,15 @@
margin-top: 0; margin-top: 0;
margin-bottom: 0; margin-bottom: 0;
} }
label {
margin-left: 3rem;
&:before {
position: absolute;
left: -3rem;
}
}
} }
select { select {

View File

@@ -1,8 +1,3 @@
@mixin sidenav__header {
padding: $gap ($gap * 2);
font-weight: bold;
}
.sidenav-container { .sidenav-container {
box-shadow: $box-shadow; box-shadow: $box-shadow;
overflow: hidden; overflow: hidden;
@@ -26,34 +21,58 @@
margin: 0px; margin: 0px;
} }
&__title { &__header {
@include sidenav__header; padding: $gap ($gap * 2);
font-weight: bold;
border-bottom: 1px solid $color-gray-lighter;
font-size: $h3-font-size; &--minimized {
@extend .sidenav__header;
padding: $gap;
width: $sidenav-collapsed-width;
}
}
&__title {
font-size: $h6-font-size;
text-transform: uppercase; text-transform: uppercase;
width: 50%; width: 50%;
color: $color-gray-dark; color: $color-gray-dark;
opacity: 0.54; opacity: 0.54;
white-space: nowrap;
padding: $gap;
display: inline-flex;
align-items: center;
} }
&__toggle { &__toggle {
@include sidenav__header;
font-size: $small-font-size; font-size: $small-font-size;
line-height: 2.8rem; color: $color-blue;
float: right; text-decoration: none;
color: $color-blue-darker; padding: $gap;
display: inline-flex;
align-items: center;
.toggle-arrows { .toggle-arrows {
vertical-align: middle; vertical-align: middle;
@include icon-size(20);
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
} }
} }
ul { ul {
&.sidenav__list--padded { &.sidenav__list--padded {
margin-top: 4 * $gap; margin-top: 3 * $gap;
margin-bottom: $footer-height; margin-bottom: $footer-height;
padding-bottom: $gap; padding-bottom: ($gap * 2);
position: fixed; position: fixed;
overflow-y: scroll; overflow-y: scroll;
top: $topbar-height + $usa-banner-height + 4rem; top: $topbar-height + $usa-banner-height + 4rem;
@@ -69,6 +88,7 @@
li { li {
margin: 0; margin: 0;
display: block; display: block;
color: $color-black-light;
} }
} }
@@ -89,100 +109,19 @@
&__link { &__link {
display: block; display: block;
padding: $gap ($gap * 2); padding: $gap ($gap * 2);
color: $color-black;
text-decoration: underline;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
color: $color-black-light;
text-decoration: none;
text-overflow: ellipsis; text-overflow: ellipsis;
&-icon {
margin-left: -($gap * 0.5);
}
&--disabled {
color: $color-shadow;
pointer-events: none;
}
&--add {
color: $color-blue;
font-size: $small-font-size;
.icon {
@include icon-color($color-blue);
@include icon-size(14);
}
}
&--active { &--active {
@include h4; @include h4;
color: $color-primary;
background-color: $color-aqua-lightest; background-color: $color-aqua-lightest;
box-shadow: inset ($gap / 2) 0 0 0 $color-primary; box-shadow: inset ($gap / 2) 0 0 0 $color-primary-darker;
.sidenav__link-icon {
@include icon-style-active;
}
position: relative; position: relative;
color: $color-primary-darker;
&_indicator .icon {
@include icon-color($color-primary);
position: absolute;
right: 0;
}
+ ul {
background-color: $color-primary;
.sidenav__link {
color: $color-white;
background-color: $color-primary;
&:hover {
background-color: $color-blue-darker;
}
&--active {
@include h5;
color: $color-white;
background-color: $color-primary;
box-shadow: none;
}
.icon {
@include icon-color($color-white);
}
}
}
}
+ ul {
li {
.sidenav__link {
@include h5;
padding: $gap * 0.75;
padding-left: 4.5rem;
border: 0;
font-weight: normal;
.sidenav__link-icon {
@include icon-size(12);
flex-shrink: 0;
margin-right: 1.5rem;
margin-left: -3rem;
}
.sidenav__link-label {
padding-left: 0;
}
}
}
} }
&:hover { &:hover {

View File

@@ -1,48 +1,24 @@
.home { .home {
margin: $gap * 3;
.sticky-cta { .sticky-cta {
margin: -1.6rem -1.6rem 0 -1.6rem; margin: -1.6rem -1.6rem 0 -1.6rem;
} }
}
.about-cloud { &__content {
margin: 4rem auto; margin: 4rem;
max-width: 900px; max-width: 900px;
&--descriptions {
.col {
margin-left: $home-pg-icon-width;
padding: ($gap * 2) ($gap * 4);
position: relative;
.icon--home-pg-badge {
position: absolute;
left: -$home-pg-icon-width;
top: $gap * 3;
} }
.your-project {
margin-top: 3rem;
padding: 3rem;
background-color: $color-gray-lightest;
h2 {
margin-top: 0;
}
.links {
justify-content: flex-start;
.icon-link {
padding: $gap ($gap * 4);
&:first-child {
padding-left: 0;
}
&:last-child {
padding-right: 0;
}
&:hover {
background-color: transparent;
color: $color-gray-dark;
.svg * {
fill: $color-gray-dark;
}
}
&.active:hover {
color: $color-blue;
} }
} }
} }
@@ -112,8 +88,3 @@
} }
} }
} }
#jedi-heirarchy {
max-width: 65rem;
margin-top: $gap * 8;
}

View File

@@ -20,10 +20,7 @@
} }
&__header { &__header {
.h2, margin-bottom: $gap * 6;
p {
margin-bottom: $gap * 0.5;
}
} }
.col { .col {
@@ -155,6 +152,10 @@
} }
} }
} }
&__confirmation {
margin-left: $gap * 8;
}
} }
.task-order__modal-cancel { .task-order__modal-cancel {

View File

@@ -100,7 +100,7 @@
{{ CheckboxInput(form.perms_env_mgmt, classes="input__inline-fields", key=env_mgmt, id=env_mgmt, optional=True) }} {{ CheckboxInput(form.perms_env_mgmt, classes="input__inline-fields", key=env_mgmt, id=env_mgmt, optional=True) }}
{{ CheckboxInput(form.perms_del_env, classes="input__inline-fields", key=del_env, id=del_env, optional=True) }} {{ CheckboxInput(form.perms_del_env, classes="input__inline-fields", key=del_env, id=del_env, optional=True) }}
</div> </div>
<hr> <hr class="full-width">
<div class="environment_roles environment-roles-new"> <div class="environment_roles environment-roles-new">
<h2>{{ "portfolios.applications.members.form.env_access.title" | translate }}</h2> <h2>{{ "portfolios.applications.members.form.env_access.title" | translate }}</h2>
<p class='usa-input__help subtitle'> <p class='usa-input__help subtitle'>

View File

@@ -40,7 +40,7 @@
{% call Modal(modal_name, classes="form-content--app-mem") %} {% call Modal(modal_name, classes="form-content--app-mem") %}
<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>
<hr> <hr class="full-width">
</div> </div>
<base-form inline-template> <base-form inline-template>
<form id='{{ modal_name }}' method="POST" action="{{ url_for(action_update, application_id=application.id, application_role_id=member.role_id,) }}"> <form id='{{ modal_name }}' method="POST" action="{{ url_for(action_update, application_id=application.id, application_role_id=member.role_id,) }}">
@@ -59,7 +59,7 @@
{% call Modal(resend_invite_modal, classes="form-content--app-mem") %} {% call Modal(resend_invite_modal, classes="form-content--app-mem") %}
<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>
<hr> <hr class="full-width">
</div> </div>
<base-form inline-template :enable-save="true"> <base-form inline-template :enable-save="true">
<form id='{{ resend_invite_modal }}' method="POST" action="{{ url_for('applications.resend_invite', application_id=application.id, application_role_id=member.role_id) }}"> <form id='{{ resend_invite_modal }}' method="POST" action="{{ url_for('applications.resend_invite', application_id=application.id, application_role_id=member.role_id) }}">

View File

@@ -2,7 +2,7 @@
{% import "applications/fragments/member_form_fields.html" as member_fields %} {% import "applications/fragments/member_form_fields.html" as member_fields %}
{% macro MemberFormTemplate(title=None, next_button=None, previous=True) %} {% macro MemberFormTemplate(title=None, next_button=None, previous=True) %}
<hr> <hr class="full-width">
{% if title %} <h1>{{ title }}</h1> {% endif %} {% if title %} <h1>{{ title }}</h1> {% endif %}
{{ caller() }} {{ caller() }}

View File

@@ -30,7 +30,7 @@
{{ ('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>
<hr class="panel__break"> <hr>
<div class="form-row"> <div class="form-row">
<div class="form-col form-col--two-thirds"> <div class="form-col form-col--two-thirds">
{{ TextInput(form.description, paragraph=True, optional=True) }} {{ TextInput(form.description, paragraph=True, optional=True) }}

View File

@@ -19,7 +19,7 @@
<p> <p>
{{ 'portfolios.applications.new.step_2_description' | translate }} {{ 'portfolios.applications.new.step_2_description' | translate }}
</p> </p>
<hr class="panel__break"> <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">
<div class="subheading">{{ 'portfolios.applications.environments_heading' | translate }}</div> <div class="subheading">{{ 'portfolios.applications.environments_heading' | translate }}</div>

View File

@@ -15,7 +15,7 @@
<p> <p>
{{ ('portfolios.applications.new.step_3_description' | translate) }} {{ ('portfolios.applications.new.step_3_description' | translate) }}
</p> </p>
<hr class="panel__break"> <hr>
{{ MemberManagementTemplate( {{ MemberManagementTemplate(
application, application,

View File

@@ -13,6 +13,9 @@
{% block application_content %} {% block application_content %}
{% if show_flash -%}
{% include "fragments/flash.html" %}
{%- endif %}
<h3>{{ 'portfolios.applications.settings.name_description' | translate }}</h3> <h3>{{ 'portfolios.applications.settings.name_description' | translate }}</h3>
{% if user_can(permissions.EDIT_APPLICATION) %} {% if user_can(permissions.EDIT_APPLICATION) %}
@@ -59,59 +62,8 @@
environments_obj, environments_obj,
new_env_form) }} new_env_form) }}
{% if user_can(permissions.DELETE_APPLICATION) %}
{% set env_count = application.environments | length %}
{% if env_count == 1 %}
{% set pluralized_env = "environment" %}
{% else %}
{% set pluralized_env = "environments" %}
{% endif %}
<h3>
{{ "portfolios.applications.delete.subheading" | translate }}
</h3>
<div class="form-row">
<div class="form-col form-col--two-thirds">
{{ "portfolios.applications.delete.text" | translate({"application_name": application.name}) | safe }}
</div>
<div class="form-col form-col--third">
<div class="usa-input">
<input
id="delete-application"
type="button"
v-on:click="openModal('delete-application')"
class='usa-button--outline button-danger-outline'
value="{{ 'portfolios.applications.delete.button' | translate }}"
>
</div>
</div>
</div>
{% call Modal(name="delete-application") %}
<h1>{{ "portfolios.applications.delete.header" | translate }}</h1>
<hr>
{{
Alert(
title=("components.modal.destructive_title" | translate),
message=("portfolios.applications.delete.alert.message" | translate),
level="warning"
)
}}
{{
DeleteConfirmation(
modal_id="delete_application",
delete_text=('portfolios.applications.delete.button' | translate),
delete_action= url_for('applications.delete', application_id=application.id),
form=application_form
)
}}
{% endcall %}
{% endif %}
<hr>
{% if user_can(permissions.VIEW_APPLICATION_ACTIVITY_LOG) and config.get("USE_AUDIT_LOG", False) %} {% if user_can(permissions.VIEW_APPLICATION_ACTIVITY_LOG) and config.get("USE_AUDIT_LOG", False) %}
<hr>
{% include "fragments/audit_events_log.html" %} {% include "fragments/audit_events_log.html" %}
{{ Pagination(audit_events, url=url_for('applications.settings', application_id=application.id)) }} {{ Pagination(audit_events, url=url_for('applications.settings', application_id=application.id)) }}
{% endif %} {% endif %}

View File

@@ -21,9 +21,7 @@
{% include 'navigation/topbar.html' %} {% include 'navigation/topbar.html' %}
<div class='global-layout'> <div class='global-layout'>
{% if portfolios %}
{% include 'navigation/global_sidenav.html' %} {% include 'navigation/global_sidenav.html' %}
{% endif %}
<div class='global-panel-container'> <div class='global-panel-container'>
{% block sidenav %}{% endblock %} {% block sidenav %}{% endblock %}

View File

@@ -1,35 +1,11 @@
{% from "components/icon.html" import Icon %} {% from "components/icon.html" import Icon %}
{% macro SidenavItem(label, href, active=False, icon=None, subnav=None) -%} {% macro SidenavItem(label, href, active=False) -%}
<li> <li>
<a class="sidenav__link {% if active %}sidenav__link--active{% endif %}" href="{{href}}" title="{{label}}"> <a class="sidenav__link {% if active %}sidenav__link--active{% endif %}" href="{{href}}" title="{{label}}">
{% if icon %}
{{ Icon(icon, classes="sidenav__link-icon") }}
{% endif %}
<span class="sidenav__link-label"> <span class="sidenav__link-label">
{{label}} {{label}}
</span> </span>
{% if active %}
<span class="sidenav__link-active_indicator">
{{ Icon("caret_right") }}
</span>
{% endif %}
</a> </a>
{% if subnav and active %}
<ul>
{% for item in subnav %}
<li>
<a class="sidenav__link {% if item["active"] %}sidenav__link--active{% endif %}" href="{{item["href"]}}" title="{{item["label"]}}">
{% if "icon" in item %}
{{ Icon(item["icon"], classes="sidenav__link-icon") }}
{% endif %}
<span class="sidenav__link-label">{{item["label"]}}</span>
</a>
</li>
{% endfor %}
</ul>
{% endif %}
</li> </li>
{%- endmacro %} {%- endmacro %}

View File

@@ -1,8 +1,7 @@
{% extends "base.html" %} {% extends "base.html" %}
{% from "components/sticky_cta.html" import StickyCTA %}
{% from "components/icon.html" import Icon %} {% from "components/icon.html" import Icon %}
{% from "components/semi_collapsible_text.html" import SemiCollapsibleText %} {% from "components/sticky_cta.html" import StickyCTA %}
{% block content %} {% block content %}
@@ -13,88 +12,56 @@
{% set sticky_header = "home.get_started" | translate %} {% set sticky_header = "home.get_started" | translate %}
{% endif %} {% endif %}
{% call StickyCTA(sticky_header) %} <div class="home__content">
<a href="{{ url_for("portfolios.new_portfolio_step_1") }}" class="usa-button-primary">
{{ "home.add_portfolio_button_text" | translate }}
</a>
{% endcall %}
<div class="about-cloud">
{% include "fragments/flash.html" %} {% include "fragments/flash.html" %}
<h1>{{ "home.head" | translate }}</h1> <h1>{{ "home.head" | translate }}</h1>
<h3>Set up a Portfolio</h3>
{{ SemiCollapsibleText(first_half=("home.about_cloud.part1"|translate), second_half=("home.about_cloud.part2"|translate)) }} <h4>New Portfolios will be visible in the left side bar of this page. </h4>
<p>All TOs associated to a specific Application or set of related Applications will be entered at the Portfolio level. Funding is applied and managed at the Portfolio level as well.</p>
<div class="your-project">
<h2 class="h3">{{ "home.your_project" | translate }}</h2>
<p>{{ "home.your_project_descrip" | translate }}</p>
<hr> <hr>
{% macro Link(icon, text, section, default=False) %} <div class="home__content--descriptions">
{% if default %} <div class="row">
<div v-bind:class='{"icon-link": true, active: selectedSection === "{{ section }}" || selectedSection === null}' v-on:click="toggleSection('{{ section }}')"> <div class="col col--half col--pad">
{% else %} {{ Icon('funding', classes="icon--home-pg-badge") }}
<div v-bind:class='{"icon-link": true, active: selectedSection === "{{ section }}"}' v-on:click="toggleSection('{{ section }}')"> <h4>{{ "navigation.portfolio_navigation.breadcrumbs.funding" | translate }}</h4>
{% endif %} <p>
<div class="col"> {{ "home.funding_descrip" | translate }}
<div class='icon-link--icon'>{{ Icon(icon) }}</div> </p>
<div class='icon-link--name'>{{ text }}</div> </div>
</div> <div class="col col--half col--pad">
</div> {{ Icon('chart-pie', classes="icon--home-pg-badge") }}
{% endmacro %} <h4>{{ "navigation.portfolio_navigation.breadcrumbs.reports" | translate }}</h4>
<p>
<toggler inline-template v-bind:initial-selected-section="'funding'"> {{ "home.reports_descrip" | translate }}
<div>
<div class="portfolio-header">
<div class="links row">
{{ Link(
icon='funding',
section='funding',
text='navigation.portfolio_navigation.breadcrumbs.funding' | translate,
default=True
) }}
{{ Link(
icon='applications',
section='applications',
text='navigation.portfolio_navigation.breadcrumbs.applications' | translate,
) }}
{{ Link(
icon='chart-pie',
section='reports',
text='navigation.portfolio_navigation.breadcrumbs.reports' | translate,
) }}
{{ Link(
icon='cog',
section='admin',
text='navigation.portfolio_navigation.breadcrumbs.admin' | translate,
) }}
</div>
</div>
{% macro Description(section, default=False) %}
{% if default %}
<p v-show="selectedSection === '{{ section }}' || selectedSection === null">
{% else %}
<p v-show="selectedSection === '{{ section }}'">
{% endif %}
<strong>
{{ "navigation.portfolio_navigation.breadcrumbs.%s" | format(section) | translate }}
</strong>
{{ "home.%s_descrip" | format(section) | translate }}
</p> </p>
{% endmacro %}
<div class="project-section-descriptions">
{{ Description('funding', default=True) }}
{{ Description('applications') }}
{{ Description('reports') }}
{{ Description('admin') }}
</div> </div>
</div> </div>
</toggler> <div class="row">
<div class="col col--half col--pad">
{{ Icon('applications', classes="icon--home-pg-badge") }}
<h4>{{ "navigation.portfolio_navigation.breadcrumbs.applications" | translate }}</h4>
<p>
{{ "home.applications_descrip" | translate }}
</p>
</div>
<div class="col col--half col--pad">
{{ Icon('cog', classes="icon--home-pg-badge") }}
<h4>{{ "navigation.portfolio_navigation.breadcrumbs.admin" | translate }}</h4>
<p>
{{ "home.admin_descrip" | translate }}
</p>
</div>
</div>
</div>
<div class="empty-state">
<div class="empty-state__footer">
<a href="{{ url_for("portfolios.new_portfolio_step_1") }}" class="usa-button usa-button-primary">
{{ "home.add_portfolio_button_text" | translate }}
</a>
</div>
</div> </div>
<img id='jedi-heirarchy' src="{{ url_for("static", filename="img/JEDIhierarchyDiagram.png")}}" alt="JEDI heirarchy diagram">
</div> </div>
</main> </main>

View File

@@ -7,29 +7,29 @@
<div v-bind:class="{'sidenav-container': props.isVisible, 'sidenav-container--minimized': !props.isVisible}"> <div v-bind:class="{'sidenav-container': props.isVisible, 'sidenav-container--minimized': !props.isVisible}">
<div class="sidenav-container__fixed"> <div class="sidenav-container__fixed">
<div v-bind:class="{'sidenav': props.isVisible, 'sidenav--minimized': !props.isVisible}"> <div v-bind:class="{'sidenav': props.isVisible, 'sidenav--minimized': !props.isVisible}">
<a href="#" v-on:click="props.toggle" class="sidenav__toggle"> <div v-bind:class="{'sidenav__header': props.isVisible, 'sidenav__header--minimized': !props.isVisible}" class="row">
<template v-if="props.isVisible"> <template v-if="props.isVisible">
{{ Icon('angle-double-left-solid', classes="toggle-arrows icon--blue") }} <span class="sidenav__title col col--grow">My Portfolios</span>
Hide <a href="#" v-on:click="props.toggle" class="sidenav__toggle col">
{{ Icon('angle-double-left-solid', classes="toggle-arrows icon--primary") }}
<span>Hide</span>
</a>
</template> </template>
<template v-else> <template v-else>
Show <a href="#" v-on:click="props.toggle" class="sidenav__toggle col">
{{ Icon('angle-double-right-solid', classes="toggle-arrows icon--blue") }} <span>Show</span>
</template> {{ Icon('angle-double-right-solid', classes="toggle-arrows icon--primary") }}
</a> </a>
</template>
</div>
<div v-if="props.isVisible"> <div v-if="props.isVisible">
<div class="sidenav__title">Portfolios</div>
<ul class="sidenav__list--padded"> <ul class="sidenav__list--padded">
{% if portfolios %}
{% for other_portfolio in portfolios|sort(attribute='name') %} {% for other_portfolio in portfolios|sort(attribute='name') %}
{{ SidenavItem(other_portfolio.name, {{ SidenavItem(other_portfolio.name,
href=url_for("applications.portfolio_applications", portfolio_id=other_portfolio.id), href=url_for("applications.portfolio_applications", portfolio_id=other_portfolio.id),
active=(other_portfolio.id | string) == request.view_args.get('portfolio_id') active=(other_portfolio.id | string) == request.view_args.get('portfolio_id')
) }} ) }}
{% endfor %} {% endfor %}
{% else %}
<li><span class="sidenav__text">You have no portfolios yet</span></li>
{% endif %}
</ul> </ul>
</div> </div>
</div> </div>

View File

@@ -56,10 +56,6 @@
{% include "portfolios/fragments/primary_point_of_contact.html" %} {% include "portfolios/fragments/primary_point_of_contact.html" %}
{% endif %} {% endif %}
{% if user_can(permissions.ARCHIVE_PORTFOLIO) %}
{% include "portfolios/fragments/delete_portfolio.html" %}
{% endif %}
{% if user_can(permissions.VIEW_PORTFOLIO_USERS) %} {% if user_can(permissions.VIEW_PORTFOLIO_USERS) %}
{% include "portfolios/fragments/portfolio_members.html" %} {% include "portfolios/fragments/portfolio_members.html" %}
{% endif %} {% endif %}

View File

@@ -16,7 +16,7 @@
{% endmacro %} {% endmacro %}
{% set step_one %} {% set step_one %}
<hr> <hr class="full-width">
<h1>Invite new portfolio member</h1> <h1>Invite new portfolio member</h1>
<div class='form-row'> <div class='form-row'>
<div class='form-col form-col--half'> <div class='form-col form-col--half'>
@@ -52,7 +52,7 @@
</div> </div>
{% endset %} {% endset %}
{% set step_two %} {% set step_two %}
<hr> <hr class="full-width">
<h1>Assign member permissions</h1> <h1>Assign member permissions</h1>
<a class='icon-link'> <a class='icon-link'>
{{ Icon('info') }} {{ Icon('info') }}

View File

@@ -5,7 +5,7 @@
{% from "components/options_input.html" import OptionsInput %} {% from "components/options_input.html" import OptionsInput %}
{% set step_one %} {% set step_one %}
<hr> <hr class="full-width">
<h1>{{ "fragments.ppoc.update_ppoc_title" | translate }}</h1> <h1>{{ "fragments.ppoc.update_ppoc_title" | translate }}</h1>
{{ {{
@@ -42,7 +42,7 @@
{% endset %} {% endset %}
{% set step_two %} {% set step_two %}
<hr> <hr class="full-width">
<h1>{{ "fragments.ppoc.update_ppoc_confirmation_title" | translate }}</h1> <h1>{{ "fragments.ppoc.update_ppoc_confirmation_title" | translate }}</h1>
{{ {{

View File

@@ -1,42 +0,0 @@
{% from "components/delete_confirmation.html" import DeleteConfirmation %}
{% from "components/alert.html" import Alert %}
{% from "components/modal.html" import Modal %}
<section id="primary-point-of-contact" class="panel">
<div class="panel__content">
<h2>{{ "fragments.delete_portfolio.title" | translate }}</h2>
<p>{{ "fragments.delete_portfolio.subtitle" | translate }}</p>
<div
class="usa-button-primary {% if applications_count == 0 %}button-danger{% else %}usa-button-disabled{% endif %}"
{% if applications_count == 0 %}v-on:click="openModal('delete_portfolio')"{% endif %}
>
{{ "common.deactivate" | translate }}
</div>
</div>
</section>
{% call Modal(name="delete_portfolio") %}
<h1>
{{ 'fragments.delete_portfolio.title' | translate }}
</h1>
<hr>
{{
Alert(
level="warning",
title=('components.modal.destructive_title' | translate),
message=('components.modal.destructive_message' | translate({"resource": "portfolio"})),
)
}}
{{
DeleteConfirmation(
modal_id='delete_portfolio',
delete_text='Deactivate',
delete_action=url_for('portfolios.delete_portfolio', portfolio_id=portfolio.id),
form=portfolio_form,
confirmation_text="deactivate",
)
}}
{% endcall %}

View File

@@ -16,15 +16,16 @@
</div> </div>
{{ StickyCTA(text="Create New Portfolio") }} {{ StickyCTA(text="Create New Portfolio") }}
<base-form inline-template> <base-form inline-template>
<form id="portfolio-create" action="{{ url_for('portfolios.create_portfolio') }}" method="POST"> <div class="row">
<form id="portfolio-create" class="col" action="{{ url_for('portfolios.create_portfolio') }}" method="POST">
{{ form.csrf_token }} {{ form.csrf_token }}
<div class="form-row form-row--separated"> <div class="form-row form-row--bordered">
<div class="form-col"> <div class="form-col">
{{ TextInput(form.name, optional=False) }} {{ TextInput(form.name, 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--separated"> <div class="form-row form-row--bordered">
<div class="form-col"> <div class="form-col">
{{ TextInput(form.description, paragraph=True) }} {{ TextInput(form.description, paragraph=True) }}
{{"forms.portfolio.description.help_text" | translate | safe }} {{"forms.portfolio.description.help_text" | translate | safe }}
@@ -36,16 +37,15 @@
{{ "forms.portfolio.defense_component.help_text" | translate | safe }} {{ "forms.portfolio.defense_component.help_text" | translate | safe }}
</div> </div>
</div> </div>
<div class='action-group'> <div class='action-group-footer'>
{{ {% block next_button %}
SaveButton( {{ SaveButton(text=('common.save' | translate), form="portfolio-create", element="input") }}
text=('common.save' | translate), {% endblock %}
form="portfolio-create", <a href="{{ url_for('applications.portfolio_applications', portfolio_id=portfolio.id) }}">
element="input", Cancel
) </a>
}}
</div>
</form> </form>
</div>
</base-form> </base-form>
</main> </main>
{% endblock %} {% endblock %}

View File

@@ -8,8 +8,26 @@
<form id="to_form" action='{{ action }}' method="POST" autocomplete="off" enctype="multipart/form-data"> <form id="to_form" action='{{ action }}' method="POST" autocomplete="off" enctype="multipart/form-data">
{{ form.csrf_token }} {{ form.csrf_token }}
{% call StickyCTA(text=('task_orders.form.sticky_header_text' | translate({"step": step}) )) %} {{ StickyCTA(
<span class="action-group"> text='task_orders.form.sticky_header_text' | translate,
context=('task_orders.form.sticky_header_context' | translate({"step": step}) )) }}
{% call Modal(name='cancel', dismissable=True) %}
<div class="task-order__modal-cancel">
<h1>Do you want to save this draft?</h1>
<div class="action-group">
<button formaction="{{ cancel_discard_url }}" class="usa-button usa-button-primary" type="submit">No, delete it</button>
<button formaction="{{ cancel_save_url }}" class="usa-button usa-button-primary" type="submit">Yes, save for later</button>
</div>
</div>
{% endcall %}
{% include "fragments/flash.html" %}
<div class="task-order">
{% block to_builder_form_field %}{% endblock %}
</div>
<span class="action-group-footer">
{% block next_button %} {% block next_button %}
<input <input
type="submit" type="submit"
@@ -32,23 +50,6 @@
{{ "common.cancel" | translate }} {{ "common.cancel" | translate }}
</a> </a>
</span> </span>
{% endcall %}
{% call Modal(name='cancel', dismissable=True) %}
<div class="task-order__modal-cancel">
<h1>Do you want to save this draft?</h1>
<div class="action-group">
<button formaction="{{ cancel_discard_url }}" class="usa-button usa-button-primary" type="submit">No, delete it</button>
<button formaction="{{ cancel_save_url }}" class="usa-button usa-button-primary" type="submit">Yes, save for later</button>
</div>
</div>
{% endcall %}
{% include "fragments/flash.html" %}
<div class="task-order">
{% block to_builder_form_field %}{% endblock %}
</div>
</form> </form>
</to-form> </to-form>

View File

@@ -1,8 +1,10 @@
{% macro TOFormStepHeader(title, description, to_number=None) %} {% macro TOFormStepHeader(description, title=None, to_number=None) %}
<div class="task-order__header"> <div class="task-order__header">
{% if title -%}
<div class="h2"> <div class="h2">
{{ title }} {{ title }}
</div> </div>
{%- endif %}
{% if to_number %} {% if to_number %}
<p> <p>
<strong>Task Order Number:</strong> {{ to_number }} <strong>Task Order Number:</strong> {{ to_number }}

View File

@@ -1,7 +1,6 @@
{% extends "task_orders/builder_base.html" %} {% extends "task_orders/builder_base.html" %}
{% from 'components/icon.html' import Icon %} {% from 'components/icon.html' import Icon %}
{% from "components/sticky_cta.html" import StickyCTA %}
{% from "task_orders/form_header.html" import TOFormStepHeader %} {% from "task_orders/form_header.html" import TOFormStepHeader %}
{% from 'components/upload_input.html' import UploadInput %} {% from 'components/upload_input.html' import UploadInput %}

View File

@@ -10,11 +10,16 @@
{% set step = "5" %} {% set step = "5" %}
{% block to_builder_form_field %} {% block to_builder_form_field %}
{{ TOFormStepHeader('task_orders.form.step_5.title' | translate, 'task_orders.form.step_5.description' | translate, task_order.number) }} {{ TOFormStepHeader('task_orders.form.step_5.description' | translate, to_number=task_order.number) }}
<div class="task-order__confirmation">
{% call Alert('',
message="task_orders.form.step_5.alert_message" | translate
) %}
{{ CheckboxInput(form.signature) }} {{ CheckboxInput(form.signature) }}
{% endcall %} {{ CheckboxInput(form.confirm) }}
</div>
<hr>
<h5>
{{ "task_orders.sign.acknowledge.title" | translate }}
</h5>
<p>
{{ "task_orders.sign.acknowledge.text" | translate }}
</p>
{% endblock %} {% endblock %}

View File

@@ -0,0 +1,31 @@
resource "random_id" "server" {
keepers = {
azi_id = 1
}
byte_length = 8
}
resource "azurerm_resource_group" "cdn" {
name = "${var.name}-${var.environment}-cdn"
location = var.region
}
resource "azurerm_cdn_profile" "cdn" {
name = "${var.name}-${var.environment}-profile"
location = azurerm_resource_group.cdn.location
resource_group_name = azurerm_resource_group.cdn.name
sku = var.sku
}
resource "azurerm_cdn_endpoint" "cdn" {
name = "${var.name}-${var.environment}-${random_id.server.hex}"
profile_name = azurerm_cdn_profile.cdn.name
location = azurerm_resource_group.cdn.location
resource_group_name = azurerm_resource_group.cdn.name
origin {
name = "${var.name}-${var.environment}-origin"
host_name = var.origin_host_name
}
}

View File

View File

@@ -0,0 +1,31 @@
variable "region" {
type = string
description = "Region this module and resources will be created in"
}
variable "name" {
type = string
description = "Unique name for the services in this module"
}
variable "environment" {
type = string
description = "Environment these resources reside (prod, dev, staging, etc)"
}
variable "owner" {
type = string
description = "Owner of the environment and resources created in this module"
}
variable "sku" {
type = string
description = "SKU of which CDN to use"
default = "Standard_Verizon"
}
variable "origin_host_name" {
type = string
description = "Subdomain to use for the origin in requests to the CDN"
}

View File

@@ -0,0 +1,13 @@
resource "azurerm_resource_group" "acr" {
name = "${var.name}-${var.environment}-acr"
location = var.region
}
resource "azurerm_container_registry" "acr" {
name = "${var.name}${var.environment}registry" # Alpha Numeric Only
resource_group_name = azurerm_resource_group.acr.name
location = azurerm_resource_group.acr.location
sku = var.sku
admin_enabled = var.admin_enabled
#georeplication_locations = [azurerm_resource_group.acr.location, var.backup_region]
}

View File

@@ -0,0 +1,37 @@
variable "region" {
type = string
description = "Region this module and resources will be created in"
}
variable "name" {
type = string
description = "Unique name for the services in this module"
}
variable "environment" {
type = string
description = "Environment these resources reside (prod, dev, staging, etc)"
}
variable "owner" {
type = string
description = "Owner of the environment and resources created in this module"
}
variable "backup_region" {
type = string
description = "Backup region for georeplicating the container registry"
}
variable "sku" {
type = string
description = "SKU to use for the container registry service"
default = "Premium"
}
variable "admin_enabled" {
type = string
description = "Admin enabled? (true/false default: false)"
default = false
}

View File

@@ -0,0 +1,22 @@
resource "azurerm_resource_group" "lb" {
name = "${var.name}-${var.environment}-lb"
location = var.region
}
resource "azurerm_public_ip" "lb" {
name = "${var.name}-${var.environment}-ip"
location = var.region
resource_group_name = azurerm_resource_group.lb.name
allocation_method = "Static"
}
resource "azurerm_lb" "lb" {
name = "${var.name}-${var.environment}-lb"
location = var.region
resource_group_name = azurerm_resource_group.lb.name
frontend_ip_configuration {
name = "${var.name}-${var.environment}-ip"
public_ip_address_id = azurerm_public_ip.lb.id
}
}

View File

View File

@@ -0,0 +1,19 @@
variable "region" {
type = string
description = "Region this module and resources will be created in"
}
variable "name" {
type = string
description = "Unique name for the services in this module"
}
variable "environment" {
type = string
description = "Environment these resources reside (prod, dev, staging, etc)"
}
variable "owner" {
type = string
description = "Owner of the environment and resources created in this module"
}

View File

@@ -0,0 +1,24 @@
resource "azurerm_resource_group" "redis" {
name = "${var.name}-${var.environment}-redis"
location = var.region
}
# NOTE: the Name used for Redis needs to be globally unique
resource "azurerm_redis_cache" "redis" {
name = "${var.name}-${var.environment}-redis"
location = azurerm_resource_group.redis.location
resource_group_name = azurerm_resource_group.redis.name
capacity = var.capacity
family = var.family
sku_name = var.sku_name
enable_non_ssl_port = var.enable_non_ssl_port
minimum_tls_version = var.minimum_tls_version
redis_configuration {
enable_authentication = var.enable_authentication
}
tags = {
environment = var.environment
owner = var.owner
}
}

View File

View File

@@ -0,0 +1,60 @@
variable "region" {
type = string
description = "Region this module and resources will be created in"
}
variable "name" {
type = string
description = "Unique name for the services in this module"
}
variable "environment" {
type = string
description = "Environment these resources reside (prod, dev, staging, etc)"
}
variable "owner" {
type = string
description = "Owner of the environment and resources created in this module"
}
variable "capacity" {
type = string
default = 2
description = "The capacity of the redis cache"
}
variable "family" {
type = string
default = "C"
description = "The subscription family for redis"
}
variable "sku_name" {
type = string
default = "Standard"
description = "The sku to use"
}
variable "enable_non_ssl_port" {
type = bool
default = false
description = "Enable non TLS port (default: false)"
}
variable "minimum_tls_version" {
type = string
default = "1.2"
description = "Minimum TLS version to use"
}
variable "enable_authentication" {
type = bool
default = true
description = "Enable or disable authentication (default: true)"
}

View File

@@ -70,3 +70,45 @@ resource "azurerm_route" "route" {
address_prefix = "0.0.0.0/0" address_prefix = "0.0.0.0/0"
next_hop_type = each.value next_hop_type = each.value
} }
# Required for the gateway
resource "azurerm_subnet" "gateway" {
name = "GatewaySubnet"
resource_group_name = azurerm_resource_group.vpc.name
virtual_network_name = azurerm_virtual_network.vpc.name
address_prefix = var.gateway_subnet
}
resource "azurerm_public_ip" "vpn_ip" {
name = "test"
location = azurerm_resource_group.vpc.location
resource_group_name = azurerm_resource_group.vpc.name
allocation_method = "Dynamic"
}
resource "azurerm_virtual_network_gateway" "vnet_gateway" {
name = "test"
location = azurerm_resource_group.vpc.location
resource_group_name = azurerm_resource_group.vpc.name
type = "Vpn"
vpn_type = "RouteBased"
active_active = false
enable_bgp = false
sku = "Standard"
ip_configuration {
name = "vnetGatewayConfig"
public_ip_address_id = azurerm_public_ip.vpn_ip.id
private_ip_address_allocation = "Dynamic"
subnet_id = azurerm_subnet.gateway.id
}
vpn_client_configuration {
address_space = ["172.16.1.0/24"]
vpn_client_protocols = ["OpenVPN"]
}
}

View File

@@ -41,3 +41,8 @@ variable "route_tables" {
type = map type = map
description = "A map with the route tables to create" description = "A map with the route tables to create"
} }
variable "gateway_subnet" {
type = string
description = "The Subnet CIDR that we'll use for the virtual_network_gateway 'GatewaySubnet'"
}

View File

@@ -0,0 +1,8 @@
module "cdn" {
source = "../../modules/cdn"
origin_host_name = "staging.atat.code.mil"
owner = var.owner
environment = var.environment
name = var.name
region = var.region
}

View File

@@ -0,0 +1,8 @@
module "container_registry" {
source = "../../modules/container_registry"
name = var.name
region = var.region
environment = var.environment
owner = var.owner
backup_region = var.backup_region
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

View File

@@ -0,0 +1,50 @@
@startuml USEAST Development Network
title USEAST Development Network
cloud Internet
cloud Azure {
[Azure Storage] as storage
[Azure CDN] as cdn
cdn --> storage : "HTTPS/443"
note as cdn_note
CDN and Azure storage are
managed by Azure and configured
for geographic failover
end note
}
frame "USEAST Virtual Network" as vnet {
frame "Public Route Table" as public_rt{
frame "Public Subnet" as public_subnet {
[ALB]
[Internet] --> ALB
note as public_useast
10.1.1.0/24
end note
}
}
frame "Private Route Table" as private_rt{
frame "Private Subnet" as private_subnet {
[AKS]
[Redis]
[Postgres]
[AzurePrivateStorage]
AKS --> Redis : "TLS:6379"
AKS --> Postgres : "TLS:5432"
AKS --> AzurePrivateStorage : "HTTPS/443"
[ALB] --> AKS : "HTTPS:443"
note as private_useast
10.1.2.0/24
end note
}
}
}
frame "US West Backup Region" as backupregion {
component "Backup Postgres" as pgbackup
[Postgres] --> pgbackup : "Private Peering / TLS:5432"
}
note right of [ALB] : Azure Load Balancer restricted to AKS only
@enduml

View File

@@ -0,0 +1,40 @@
@startuml USWEST Development Network
title USWEST Development Network
cloud Internet
frame "USEAST Virtual Network" as vnet {
frame "Public Route Table" as public_rt{
frame "Public Subnet" as public_subnet {
[ALB]
[Internet] --> ALB
note as public_useast
10.2.1.0/24
end note
}
}
frame "Private Route Table" as private_rt{
frame "Private Subnet" as private_subnet {
[AKS]
[Redis]
[Postgres]
[AzurePrivateStorage]
AKS --> Redis : "TLS:6379"
AKS --> Postgres : "TLS:5432"
AKS --> AzurePrivateStorage : "HTTPS/443"
[ALB] --> AKS : "HTTPS:443"
note as private_useast
10.2.2.0/24
end note
}
}
}
frame "USEAST Primary Region " as primary_region{
component "Postgres" as pgbackup
[Postgres] --> pgbackup : "Private Peering / TLS:5432"
}
note right of [ALB] : Azure Load Balancer restricted to AKS only
@enduml

View File

@@ -9,3 +9,10 @@ module "k8s" {
vnet_subnet_id = module.vpc.subnets #FIXME - output from module.vpc.subnets should be map vnet_subnet_id = module.vpc.subnets #FIXME - output from module.vpc.subnets should be map
} }
module "lb" {
source = "../../modules/lb"
region = var.region
name = var.name
environment = var.environment
owner = var.owner
}

View File

@@ -0,0 +1,7 @@
module "redis" {
source = "../../modules/redis"
owner = var.owner
environment = var.environment
region = var.region
name = var.name
}

View File

@@ -7,6 +7,11 @@ variable "region" {
} }
variable "backup_region" {
default = "westus2"
}
variable "owner" { variable "owner" {
default = "dev" default = "dev"
} }
@@ -31,6 +36,12 @@ variable "networks" {
} }
} }
variable "gateway_subnet" {
type = string
default = "10.1.20.0/24"
}
variable "route_tables" { variable "route_tables" {
description = "Route tables and their default routes" description = "Route tables and their default routes"
type = map type = map

View File

@@ -4,6 +4,7 @@ module "vpc" {
region = var.region region = var.region
virtual_network = var.virtual_network virtual_network = var.virtual_network
networks = var.networks networks = var.networks
gateway_subnet = var.gateway_subnet
route_tables = var.route_tables route_tables = var.route_tables
owner = var.owner owner = var.owner
name = var.name name = var.name

View File

@@ -4,7 +4,7 @@ from uuid import uuid4
from atst.domain.environments import Environments from atst.domain.environments import Environments
from atst.domain.environment_roles import EnvironmentRoles from atst.domain.environment_roles import EnvironmentRoles
from atst.domain.exceptions import NotFoundError, DisabledError from atst.domain.exceptions import AlreadyExistsError, DisabledError, NotFoundError
from atst.models.environment_role import CSPRole, EnvironmentRole from atst.models.environment_role import CSPRole, EnvironmentRole
from tests.factories import ( from tests.factories import (
@@ -100,6 +100,27 @@ def test_update_environment():
assert environment.name == "name 2" assert environment.name == "name 2"
def test_create_does_not_duplicate_names_within_application():
application = ApplicationFactory.create()
name = "Your Environment"
user = application.portfolio.owner
assert Environments.create(user, application, name)
with pytest.raises(AlreadyExistsError):
Environments.create(user, application, name)
def test_update_does_not_duplicate_names_within_application():
application = ApplicationFactory.create()
name = "Your Environment"
environment = EnvironmentFactory.create(application=application, name=name)
dupe_env = EnvironmentFactory.create(application=application)
user = application.portfolio.owner
with pytest.raises(AlreadyExistsError):
Environments.update(dupe_env, name)
class EnvQueryTest: class EnvQueryTest:
@property @property
def NOW(self): def NOW(self):

View File

@@ -52,8 +52,6 @@ def test_updating_application_environments_success(client, user_session):
_external=True, _external=True,
fragment="application-environments", fragment="application-environments",
_anchor="application-environments", _anchor="application-environments",
active_toggler=environment.id,
active_toggler_section="edit",
) )
assert environment.name == "new name a" assert environment.name == "new name a"
@@ -78,6 +76,24 @@ def test_update_environment_failure(client, user_session):
assert environment.name == "original name" assert environment.name == "original name"
def test_enforces_unique_env_name(client, user_session, session):
application = ApplicationFactory.create()
user = application.portfolio.owner
name = "New Environment"
environment = EnvironmentFactory.create(application=application, name=name)
form_data = {"name": name}
user_session(user)
session.begin_nested()
response = client.post(
url_for("applications.new_environment", application_id=application.id),
data=form_data,
)
session.rollback()
assert response.status_code == 400
def test_application_settings(client, user_session): def test_application_settings(client, user_session):
portfolio = PortfolioFactory.create() portfolio = PortfolioFactory.create()
application = Applications.create( application = Applications.create(
@@ -258,6 +274,23 @@ def test_user_without_permission_cannot_update_application(client, user_session)
assert application.description == "Cool stuff happening here!" assert application.description == "Cool stuff happening here!"
def test_update_application_enforces_unique_name(client, user_session, session):
portfolio = PortfolioFactory.create()
name = "Test Application"
application = ApplicationFactory.create(portfolio=portfolio, name=name)
dupe_application = ApplicationFactory.create(portfolio=portfolio)
user_session(portfolio.owner)
session.begin_nested()
response = client.post(
url_for("applications.update", application_id=dupe_application.id),
data={"name": name, "description": dupe_application.description},
)
session.rollback()
assert response.status_code == 400
def test_user_can_only_access_apps_in_their_portfolio(client, user_session): def test_user_can_only_access_apps_in_their_portfolio(client, user_session):
portfolio = PortfolioFactory.create() portfolio = PortfolioFactory.create()
other_portfolio = PortfolioFactory.create( other_portfolio = PortfolioFactory.create(
@@ -288,41 +321,6 @@ def test_user_can_only_access_apps_in_their_portfolio(client, user_session):
assert time_updated == other_application.time_updated assert time_updated == other_application.time_updated
def test_delete_application(client, user_session):
user = UserFactory.create()
port = PortfolioFactory.create(
owner=user,
applications=[
{
"name": "mos eisley",
"environments": [
{"name": "bar"},
{"name": "booth"},
{"name": "band stage"},
],
}
],
)
application = port.applications[0]
user_session(user)
response = client.post(
url_for("applications.delete", application_id=application.id)
)
# appropriate response and redirect
assert response.status_code == 302
assert response.location == url_for(
"applications.portfolio_applications", portfolio_id=port.id, _external=True
)
# appropriate flash message
message = get_flashed_messages()[0]
assert "deleted" in message["message"]
assert application.name in message["message"]
# app and envs are soft deleted
assert len(port.applications) == 0
assert len(application.environments) == 0
def test_new_environment(client, user_session): def test_new_environment(client, user_session):
user = UserFactory.create() user = UserFactory.create()
portfolio = PortfolioFactory(owner=user) portfolio = PortfolioFactory(owner=user)

View File

@@ -84,35 +84,3 @@ def test_portfolio_reports_with_mock_portfolio(client, user_session):
response = client.get(url_for("portfolios.reports", portfolio_id=portfolio.id)) response = client.get(url_for("portfolios.reports", portfolio_id=portfolio.id))
assert response.status_code == 200 assert response.status_code == 200
assert portfolio.name in response.data.decode() assert portfolio.name in response.data.decode()
def test_delete_portfolio_success(client, user_session):
portfolio = PortfolioFactory.create()
owner = portfolio.owner
user_session(owner)
assert len(Portfolios.for_user(user=owner)) == 1
response = client.post(
url_for("portfolios.delete_portfolio", portfolio_id=portfolio.id)
)
assert response.status_code == 302
assert url_for("atst.home") in response.location
assert len(Portfolios.for_user(user=owner)) == 0
def test_delete_portfolio_failure(no_debug_client, user_session):
portfolio = PortfolioFactory.create()
application = ApplicationFactory.create(portfolio=portfolio)
owner = portfolio.owner
user_session(owner)
assert len(Portfolios.for_user(user=owner)) == 1
response = no_debug_client.post(
url_for("portfolios.delete_portfolio", portfolio_id=portfolio.id)
)
assert response.status_code == 500
assert len(Portfolios.for_user(user=owner)) == 1

View File

@@ -343,40 +343,6 @@ def test_portfolios_invite_member_access(post_url_assert_status):
post_url_assert_status(rando, url, 404) post_url_assert_status(rando, url, 404)
# applications.delete
def test_applications_delete_access(post_url_assert_status, monkeypatch):
ccpo = UserFactory.create_ccpo()
owner = user_with()
app_admin = user_with()
rando = user_with()
portfolio = PortfolioFactory.create(
owner=owner, applications=[{"name": "mos eisley"}]
)
application = portfolio.applications[0]
ApplicationRoleFactory.create(
user=app_admin,
application=application,
permission_sets=PermissionSets.get_many(
[
PermissionSets.VIEW_APPLICATION,
PermissionSets.EDIT_APPLICATION_ENVIRONMENTS,
PermissionSets.EDIT_APPLICATION_TEAM,
PermissionSets.DELETE_APPLICATION_ENVIRONMENTS,
]
),
)
monkeypatch.setattr("atst.domain.applications.Applications.delete", lambda *a: True)
url = url_for("applications.delete", application_id=application.id)
post_url_assert_status(app_admin, url, 404)
post_url_assert_status(rando, url, 404)
post_url_assert_status(owner, url, 302)
post_url_assert_status(ccpo, url, 302)
# applications.settings # applications.settings
def test_application_settings_access(get_url_assert_status): def test_application_settings_access(get_url_assert_status):
ccpo = user_with(PermissionSets.VIEW_PORTFOLIO_APPLICATION_MANAGEMENT) ccpo = user_with(PermissionSets.VIEW_PORTFOLIO_APPLICATION_MANAGEMENT)
@@ -538,10 +504,16 @@ def test_applications_update_access(post_url_assert_status):
) )
app = portfolio.applications[0] app = portfolio.applications[0]
def _form_data():
return {
"name": "Test Application %s" % (random.randrange(1, 1000)),
"description": "This is only a test",
}
url = url_for("applications.update", application_id=app.id) url = url_for("applications.update", application_id=app.id)
post_url_assert_status(dev, url, 200) post_url_assert_status(dev, url, 302, data=_form_data())
post_url_assert_status(ccpo, url, 200) post_url_assert_status(ccpo, url, 302, data=_form_data())
post_url_assert_status(rando, url, 404) post_url_assert_status(rando, url, 404, data=_form_data())
# applications.update_environments # applications.update_environments
@@ -699,34 +671,3 @@ def test_task_orders_new_post_routes(post_url_assert_status):
post_url_assert_status(owner, url, 302, data=data) post_url_assert_status(owner, url, 302, data=data)
post_url_assert_status(ccpo, url, 302, data=data) post_url_assert_status(ccpo, url, 302, data=data)
post_url_assert_status(rando, url, 404, data=data) post_url_assert_status(rando, url, 404, data=data)
def test_portfolio_delete_access(post_url_assert_status):
rando = UserFactory.create()
owner = UserFactory.create()
ccpo = UserFactory.create_ccpo()
post_url_assert_status(
ccpo,
url_for(
"portfolios.delete_portfolio", portfolio_id=PortfolioFactory.create().id
),
302,
)
post_url_assert_status(
owner,
url_for(
"portfolios.delete_portfolio",
portfolio_id=PortfolioFactory.create(owner=owner).id,
),
302,
)
post_url_assert_status(
rando,
url_for(
"portfolios.delete_portfolio", portfolio_id=PortfolioFactory.create().id
),
404,
)

View File

@@ -22,13 +22,13 @@ home:
add_portfolio_button_text: Add New Portfolio add_portfolio_button_text: Add New Portfolio
new_portfolio: New Portfolio new_portfolio: New Portfolio
get_started: Get Started get_started: Get Started
head: About Cloud Services head: JEDI Cloud Services
your_project: Your Project your_project: Your Project
your_project_descrip: Your portfolio is where all task orders pertaining to a specific project or set of related projects live. In JEDI, every task order in your portfolio has four components. your_project_descrip: Your portfolio is where all task orders pertaining to a specific project or set of related projects live. In JEDI, every task order in your portfolio has four components.
funding_descrip: is information about all approved task orders associated to your portfolio. funding_descrip: The Task Orders section allows you to enter, manage, and edit awarded TOs associated to a specific Portfolio.
applications_descrip: ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod applications_descrip: The Applications section allows you to easily create and define new Applications within a Portfolio, as well as manage user permissions and Environments.
reports_descrip: enim ad minim veniam, quis nostrud exercitation ullamco reports_descrip: The Reports section allows you to view and monitor funding usage within a specific Portfolio.
admin_descrip: aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat admin_descrip: Within the Settings section, you can manage your Portfolio name and description, as well as add, edit, and delete Portfolio managers.
ccpo: ccpo:
users_title: CCPO Users users_title: CCPO Users
add_user: Add new CCPO user add_user: Add new CCPO user
@@ -116,6 +116,8 @@ flash:
deleted: 'You have successfully deleted the {application_name} application. To view the retained activity log, visit the portfolio administration page.' deleted: 'You have successfully deleted the {application_name} application. To view the retained activity log, visit the portfolio administration page.'
name_error: name_error:
message: 'The application name {name} has already been used in this portfolio. Please enter a unique name.' message: 'The application name {name} has already been used in this portfolio. Please enter a unique name.'
env_name_error:
message: 'The environment name {name} has already been used in this application. Please enter a unique name.'
delete_member_success: 'You have successfully deleted {member_name} from the portfolio.' delete_member_success: 'You have successfully deleted {member_name} from the portfolio.'
deleted_member: Portfolio member deleted deleted_member: Portfolio member deleted
environment_added: 'The environment "{env_name}" has been added to the application.' environment_added: 'The environment "{env_name}" has been added to the application.'
@@ -523,10 +525,11 @@ task_orders:
next_button: 'Next: Review Task Order' next_button: 'Next: Review Task Order'
step_5: step_5:
title: Confirm Signature title: Confirm Signature
description: Finally, plase confirm that your uploaded document representing the information you've entered contains the required signature from your Contracting Officer. You will be informed as soon as CCPO completes their review. description: Prior to submitting the Task Order, you must acknowledge, by marking the appropriate box below, that the uploaded Task Order is signed by an appropriate, duly warranted Contracting Officer who has the authority to execute the uploaded Task Order on your Agencys behalf and has authorized you to upload the Task Order in accordance with Agency policy and procedures. You must further acknowledge, by marking the appropriate box below, that all information entered herein matches that of the submitted Task Order.
alert_message: All task orders require a Contracting Officer signature. alert_message: All task orders require a Contracting Officer signature.
next_button: 'Confirm & Submit' next_button: 'Confirm & Submit'
sticky_header_text: 'Add Task Order (step {step} of 5)' sticky_header_text: 'Add Task Order'
sticky_header_context: 'Step {step} of 5'
empty_state: empty_state:
header: Add approved task orders header: Add approved task orders
message: Upload your approved Task Order here. You are required to confirm you have the appropriate signature. You will have the ability to add additional approved Task Orders with more funding to this Portfolio in the future. message: Upload your approved Task Order here. You are required to confirm you have the appropriate signature. You will have the ability to add additional approved Task Orders with more funding to this Portfolio in the future.
@@ -539,7 +542,11 @@ task_orders:
subtitle: Who will be involved in the work funded by this task order? subtitle: Who will be involved in the work funded by this task order?
team_title: Your team team_title: Your team
sign: sign:
digital_signature_description: I acknowledge that the uploaded task order contains the required KO signature. digital_signature_description: I confirm the uploaded Task Order is signed by the appropriate, duly warranted Agency Contracting Officer who authorized me to upload the Task Order.
confirmation_description: I confirm that the information entered here in matches that of the submitted Task Order.
acknowledge:
title: Acknowledge Statement
text: I acknowledge, by executing the confirmation above and submitting this verification, that I am subject to potential penalties that may include fines, imprisonment, or both, under the U.S. law and regulations for any false statement or misrepresentation in association with this Task Order submission or on any accompanying documentation.
status_empty_state: 'This Portfolio has no {status} Task Orders.' status_empty_state: 'This Portfolio has no {status} Task Orders.'
status_list_title: '{status} Task Orders' status_list_title: '{status} Task Orders'
JEDICLINType: JEDICLINType:

View File

@@ -58,6 +58,5 @@ NGROK_TOKEN=<token> GI_API_KEY=<api key> GI_SUITE=<suite> CONTAINER_IMAGE=atat:b
- If you get errors regarding ports being in use, make sure you don't have instances of the Flask app, Postgres, or Redis running locally using those ports. - If you get errors regarding ports being in use, make sure you don't have instances of the Flask app, Postgres, or Redis running locally using those ports.
- If the curl command used to wait for the application container times out and fails, you can increase the timeout by setting a CONTAINER_TIMEOUT environment variable. It defaults to 200 in the script. - If the curl command used to wait for the application container times out and fails, you can increase the timeout by setting a CONTAINER_TIMEOUT environment variable. It defaults to 200 in the script.
- The curl command will print errors until it successfully connects to the application container. These are normal and expected. When it finally connects, it will print the ATAT home page HTML to STDOUT.
- You may see errors like "No such container". The script attempts to clean up any previous incarnations of the containers before it starts, and it may print errors when it doesn't find them. This is fine. - You may see errors like "No such container". The script attempts to clean up any previous incarnations of the containers before it starts, and it may print errors when it doesn't find them. This is fine.
- The script is, for the most part, a series of docker commands, so try running the commands individually and debugging that way. - The script is, for the most part, a series of docker commands, so try running the commands individually and debugging that way.