Merge branch 'staging' of github-DDS:dod-ccpo/atst into gi-updates-wo-20191216
This commit is contained in:
commit
007291da0a
@ -3,7 +3,7 @@
|
||||
"files": "^.secrets.baseline$|^.*pgsslrootcert.yml$",
|
||||
"lines": null
|
||||
},
|
||||
"generated_at": "2019-12-13T20:38:57Z",
|
||||
"generated_at": "2019-12-18T15:29:41Z",
|
||||
"plugins_used": [
|
||||
{
|
||||
"base64_limit": 4.5,
|
||||
@ -170,7 +170,7 @@
|
||||
"hashed_secret": "e4f14805dfd1e6af030359090c535e149e6b4207",
|
||||
"is_secret": false,
|
||||
"is_verified": false,
|
||||
"line_number": 659,
|
||||
"line_number": 665,
|
||||
"type": "Hex High Entropy String"
|
||||
}
|
||||
]
|
||||
|
@ -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 ###
|
@ -11,7 +11,7 @@ from atst.models import (
|
||||
ApplicationRoleStatus,
|
||||
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):
|
||||
@ -28,7 +28,7 @@ class Applications(BaseDomainClass):
|
||||
if 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
|
||||
|
||||
@classmethod
|
||||
@ -55,7 +55,7 @@ class Applications(BaseDomainClass):
|
||||
)
|
||||
|
||||
db.session.add(application)
|
||||
update_or_raise_already_exists_error(message="application")
|
||||
commit_or_raise_already_exists_error(message="application")
|
||||
return application
|
||||
|
||||
@classmethod
|
||||
|
@ -12,6 +12,7 @@ from atst.models import (
|
||||
CLIN,
|
||||
)
|
||||
from atst.domain.environment_roles import EnvironmentRoles
|
||||
from atst.utils import commit_or_raise_already_exists_error
|
||||
|
||||
from .exceptions import NotFoundError, DisabledError
|
||||
|
||||
@ -21,7 +22,7 @@ class Environments(object):
|
||||
def create(cls, user, application, name):
|
||||
environment = Environment(application=application, name=name, creator=user)
|
||||
db.session.add(environment)
|
||||
db.session.commit()
|
||||
commit_or_raise_already_exists_error(message="environment")
|
||||
return environment
|
||||
|
||||
@classmethod
|
||||
@ -39,7 +40,8 @@ class Environments(object):
|
||||
if name is not None:
|
||||
environment.name = name
|
||||
db.session.add(environment)
|
||||
db.session.commit()
|
||||
commit_or_raise_already_exists_error(message="environment")
|
||||
return environment
|
||||
|
||||
@classmethod
|
||||
def get(cls, environment_id):
|
||||
|
@ -4,7 +4,7 @@ from atst.database import db
|
||||
from atst.models.clin import CLIN
|
||||
from atst.models.task_order import TaskOrder, SORT_ORDERING
|
||||
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):
|
||||
@ -15,7 +15,7 @@ class TaskOrders(BaseDomainClass):
|
||||
def create(cls, portfolio_id, number, clins, pdf):
|
||||
task_order = TaskOrder(portfolio_id=portfolio_id, number=number, pdf=pdf)
|
||||
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)
|
||||
return task_order
|
||||
|
||||
@ -34,7 +34,7 @@ class TaskOrders(BaseDomainClass):
|
||||
task_order.number = number
|
||||
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
|
||||
|
||||
@classmethod
|
||||
|
@ -151,3 +151,6 @@ class SignatureForm(BaseForm):
|
||||
translate("task_orders.sign.digital_signature_description"),
|
||||
validators=[Required()],
|
||||
)
|
||||
confirm = BooleanField(
|
||||
translate("task_orders.sign.confirmation_description"), validators=[Required()],
|
||||
)
|
||||
|
@ -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.dialects.postgresql import JSONB
|
||||
from enum import Enum
|
||||
@ -38,6 +38,12 @@ class Environment(
|
||||
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):
|
||||
PENDING = "pending"
|
||||
COMPLETED = "completed"
|
||||
|
@ -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 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.domain.authz.decorator import user_can_access_decorator as user_can
|
||||
from atst.models.permissions import Permissions
|
||||
@ -13,6 +11,7 @@ from atst.routes.applications.settings import (
|
||||
get_new_member_form,
|
||||
handle_create_member,
|
||||
handle_update_member,
|
||||
handle_update_application,
|
||||
)
|
||||
|
||||
|
||||
@ -38,31 +37,6 @@ def render_new_application_form(
|
||||
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("/applications/<application_id>/new/step_1")
|
||||
@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(
|
||||
{**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:
|
||||
return redirect(
|
||||
|
@ -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 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.environment_roles import EnvironmentRoles
|
||||
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 import NameAndDescriptionForm, EditEnvironmentForm
|
||||
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
|
||||
|
||||
|
||||
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")
|
||||
@user_can(Permissions.VIEW_APPLICATION, message="view application edit form")
|
||||
def settings(application_id):
|
||||
application = Applications.get(application_id)
|
||||
|
||||
return render_settings_page(
|
||||
application=application,
|
||||
active_toggler=http_request.args.get("active_toggler"),
|
||||
active_toggler_section=http_request.args.get("active_toggler_section"),
|
||||
)
|
||||
return render_settings_page(application=application,)
|
||||
|
||||
|
||||
@applications_bp.route("/environments/<environment_id>/edit", methods=["POST"])
|
||||
@ -264,31 +314,21 @@ def update_environment(environment_id):
|
||||
application = environment.application
|
||||
|
||||
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():
|
||||
Environments.update(environment=environment, name=env_form.name.data)
|
||||
|
||||
flash("application_environments_updated")
|
||||
|
||||
if updated_environment:
|
||||
return redirect(
|
||||
url_for(
|
||||
"applications.settings",
|
||||
application_id=application.id,
|
||||
fragment="application-environments",
|
||||
_anchor="application-environments",
|
||||
active_toggler=environment.id,
|
||||
active_toggler_section="edit",
|
||||
)
|
||||
)
|
||||
else:
|
||||
return (
|
||||
render_settings_page(
|
||||
application=application,
|
||||
active_toggler=environment.id,
|
||||
active_toggler_section="edit",
|
||||
),
|
||||
400,
|
||||
)
|
||||
return (render_settings_page(application=application, show_flash=True), 400)
|
||||
|
||||
|
||||
@applications_bp.route(
|
||||
@ -298,14 +338,9 @@ def update_environment(environment_id):
|
||||
def new_environment(application_id):
|
||||
application = Applications.get(application_id)
|
||||
env_form = EditEnvironmentForm(formdata=http_request.form)
|
||||
environment = handle_update_environment(form=env_form, application=application)
|
||||
|
||||
if env_form.validate():
|
||||
Environments.create(
|
||||
g.current_user, application=application, name=env_form.name.data
|
||||
)
|
||||
|
||||
flash("environment_added", environment_name=env_form.data["name"])
|
||||
|
||||
if environment:
|
||||
return redirect(
|
||||
url_for(
|
||||
"applications.settings",
|
||||
@ -315,7 +350,7 @@ def new_environment(application_id):
|
||||
)
|
||||
)
|
||||
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"])
|
||||
@ -323,10 +358,9 @@ def new_environment(application_id):
|
||||
def update(application_id):
|
||||
application = Applications.get(application_id)
|
||||
form = NameAndDescriptionForm(http_request.form)
|
||||
if form.validate():
|
||||
application_data = form.data
|
||||
Applications.update(application, application_data)
|
||||
updated_application = handle_update_application(form, application_id)
|
||||
|
||||
if updated_application:
|
||||
return redirect(
|
||||
url_for(
|
||||
"applications.portfolio_applications",
|
||||
@ -334,22 +368,10 @@ def update(application_id):
|
||||
)
|
||||
)
|
||||
else:
|
||||
return render_settings_page(application=application, application_form=form)
|
||||
|
||||
|
||||
@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
|
||||
return (
|
||||
render_settings_page(application=application, show_flash=True),
|
||||
400,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@applications_bp.route("/environments/<environment_id>/delete", methods=["POST"])
|
||||
|
@ -56,13 +56,3 @@ def reports(portfolio_id):
|
||||
monthly_spending=Reports.monthly_spending(portfolio),
|
||||
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"))
|
||||
|
@ -30,7 +30,7 @@ def pick(keys, dct):
|
||||
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:
|
||||
db.session.commit()
|
||||
except IntegrityError:
|
||||
|
@ -29,6 +29,11 @@ MESSAGES = {
|
||||
""",
|
||||
"category": "success",
|
||||
},
|
||||
"application_environments_name_error": {
|
||||
"title_template": "",
|
||||
"message_template": """{{ 'flash.application.env_name_error.message' | translate({ 'name': name }) }}""",
|
||||
"category": "error",
|
||||
},
|
||||
"application_environments_updated": {
|
||||
"title_template": "Application environments updated",
|
||||
"message_template": "Application environments have been updated",
|
||||
|
@ -72,15 +72,18 @@ $CONTAINER_IMAGE \
|
||||
|
||||
# Use curl to wait for application container to become available
|
||||
docker pull curlimages/curl:latest
|
||||
echo "Waiting for application container to become available"
|
||||
docker run --network atat \
|
||||
curlimages/curl:latest \
|
||||
curl --connect-timeout 3 \
|
||||
curl \
|
||||
--silent \
|
||||
--connect-timeout 3 \
|
||||
--max-time 5 \
|
||||
--retry $CONTAINER_TIMEOUT \
|
||||
--retry-connrefused \
|
||||
--retry-delay 1 \
|
||||
--retry-max-time $CONTAINER_TIMEOUT \
|
||||
test-atat:8000
|
||||
test-atat:8000 >/dev/null
|
||||
|
||||
# Run Ghost Inspector tests
|
||||
docker pull ghostinspector/test-runner-standalone:latest
|
||||
|
@ -1,7 +1,8 @@
|
||||
// Form Grid
|
||||
.form-row {
|
||||
margin: ($gap * 4) 0;
|
||||
&--separated {
|
||||
|
||||
&--bordered {
|
||||
border-bottom: $color-gray-lighter 1px solid;
|
||||
}
|
||||
|
||||
|
@ -88,5 +88,9 @@ p {
|
||||
hr {
|
||||
border: 0;
|
||||
border-bottom: 1px solid $color-gray-light;
|
||||
margin: ($gap * 3) ($site-margins * -4);
|
||||
margin: ($gap * 3) 0;
|
||||
|
||||
&.full-width {
|
||||
margin: ($gap * 3) ($site-margins * -4);
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ $usa-banner-height: 2.8rem;
|
||||
$sidenav-expanded-width: 25rem;
|
||||
$sidenav-collapsed-width: 10rem;
|
||||
$max-panel-width: 80rem;
|
||||
$home-pg-icon-width: 6rem;
|
||||
|
||||
/*
|
||||
* USWDS Variables
|
||||
|
@ -46,7 +46,7 @@
|
||||
background: white;
|
||||
right: 0;
|
||||
padding-right: $gap * 4;
|
||||
border-top: 1px solid $color-gray-light;
|
||||
border-top: 1px solid $color-gray-lighter;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
@ -94,4 +94,19 @@
|
||||
&--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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -165,6 +165,15 @@
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
margin-left: 3rem;
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
left: -3rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
|
@ -1,8 +1,3 @@
|
||||
@mixin sidenav__header {
|
||||
padding: $gap ($gap * 2);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.sidenav-container {
|
||||
box-shadow: $box-shadow;
|
||||
overflow: hidden;
|
||||
@ -26,34 +21,58 @@
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
@include sidenav__header;
|
||||
&__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;
|
||||
width: 50%;
|
||||
color: $color-gray-dark;
|
||||
opacity: 0.54;
|
||||
white-space: nowrap;
|
||||
padding: $gap;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__toggle {
|
||||
@include sidenav__header;
|
||||
|
||||
font-size: $small-font-size;
|
||||
line-height: 2.8rem;
|
||||
float: right;
|
||||
color: $color-blue-darker;
|
||||
color: $color-blue;
|
||||
text-decoration: none;
|
||||
padding: $gap;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
.toggle-arrows {
|
||||
vertical-align: middle;
|
||||
@include icon-size(20);
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
&.sidenav__list--padded {
|
||||
margin-top: 4 * $gap;
|
||||
margin-top: 3 * $gap;
|
||||
margin-bottom: $footer-height;
|
||||
padding-bottom: $gap;
|
||||
padding-bottom: ($gap * 2);
|
||||
position: fixed;
|
||||
overflow-y: scroll;
|
||||
top: $topbar-height + $usa-banner-height + 4rem;
|
||||
@ -69,6 +88,7 @@
|
||||
li {
|
||||
margin: 0;
|
||||
display: block;
|
||||
color: $color-black-light;
|
||||
}
|
||||
}
|
||||
|
||||
@ -89,100 +109,19 @@
|
||||
&__link {
|
||||
display: block;
|
||||
padding: $gap ($gap * 2);
|
||||
color: $color-black;
|
||||
text-decoration: underline;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
color: $color-black-light;
|
||||
text-decoration: none;
|
||||
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 {
|
||||
@include h4;
|
||||
|
||||
color: $color-primary;
|
||||
background-color: $color-aqua-lightest;
|
||||
box-shadow: inset ($gap / 2) 0 0 0 $color-primary;
|
||||
|
||||
.sidenav__link-icon {
|
||||
@include icon-style-active;
|
||||
}
|
||||
|
||||
box-shadow: inset ($gap / 2) 0 0 0 $color-primary-darker;
|
||||
position: relative;
|
||||
|
||||
&_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;
|
||||
}
|
||||
}
|
||||
}
|
||||
color: $color-primary-darker;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
@ -1,49 +1,25 @@
|
||||
.home {
|
||||
margin: $gap * 3;
|
||||
.sticky-cta {
|
||||
margin: -1.6rem -1.6rem 0 -1.6rem;
|
||||
}
|
||||
}
|
||||
|
||||
.about-cloud {
|
||||
margin: 4rem auto;
|
||||
max-width: 900px;
|
||||
}
|
||||
&__content {
|
||||
margin: 4rem;
|
||||
max-width: 900px;
|
||||
|
||||
.your-project {
|
||||
margin-top: 3rem;
|
||||
padding: 3rem;
|
||||
background-color: $color-gray-lightest;
|
||||
&--descriptions {
|
||||
.col {
|
||||
margin-left: $home-pg-icon-width;
|
||||
padding: ($gap * 2) ($gap * 4);
|
||||
position: relative;
|
||||
|
||||
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;
|
||||
.icon--home-pg-badge {
|
||||
position: absolute;
|
||||
left: -$home-pg-icon-width;
|
||||
top: $gap * 3;
|
||||
}
|
||||
}
|
||||
|
||||
&.active:hover {
|
||||
color: $color-blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -112,8 +88,3 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#jedi-heirarchy {
|
||||
max-width: 65rem;
|
||||
margin-top: $gap * 8;
|
||||
}
|
||||
|
@ -20,10 +20,7 @@
|
||||
}
|
||||
|
||||
&__header {
|
||||
.h2,
|
||||
p {
|
||||
margin-bottom: $gap * 0.5;
|
||||
}
|
||||
margin-bottom: $gap * 6;
|
||||
}
|
||||
|
||||
.col {
|
||||
@ -155,6 +152,10 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__confirmation {
|
||||
margin-left: $gap * 8;
|
||||
}
|
||||
}
|
||||
|
||||
.task-order__modal-cancel {
|
||||
|
@ -100,7 +100,7 @@
|
||||
{{ 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) }}
|
||||
</div>
|
||||
<hr>
|
||||
<hr class="full-width">
|
||||
<div class="environment_roles environment-roles-new">
|
||||
<h2>{{ "portfolios.applications.members.form.env_access.title" | translate }}</h2>
|
||||
<p class='usa-input__help subtitle'>
|
||||
|
@ -40,7 +40,7 @@
|
||||
{% call Modal(modal_name, classes="form-content--app-mem") %}
|
||||
<div class="modal__form--header">
|
||||
<h1>{{ Icon('avatar') }} {{ "portfolios.applications.members.form.edit_access_header" | translate({ "user": member.user_name }) }}</h1>
|
||||
<hr>
|
||||
<hr class="full-width">
|
||||
</div>
|
||||
<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,) }}">
|
||||
@ -59,7 +59,7 @@
|
||||
{% call Modal(resend_invite_modal, classes="form-content--app-mem") %}
|
||||
<div class="modal__form--header">
|
||||
<h1>{{ "portfolios.applications.members.new.verify" | translate }}</h1>
|
||||
<hr>
|
||||
<hr class="full-width">
|
||||
</div>
|
||||
<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) }}">
|
||||
|
@ -2,7 +2,7 @@
|
||||
{% import "applications/fragments/member_form_fields.html" as member_fields %}
|
||||
|
||||
{% macro MemberFormTemplate(title=None, next_button=None, previous=True) %}
|
||||
<hr>
|
||||
<hr class="full-width">
|
||||
{% if title %} <h1>{{ title }}</h1> {% endif %}
|
||||
|
||||
{{ caller() }}
|
||||
|
@ -30,7 +30,7 @@
|
||||
{{ ('portfolios.applications.new.step_1_form_help_text.name' | translate | safe) }}
|
||||
</div>
|
||||
</div>
|
||||
<hr class="panel__break">
|
||||
<hr>
|
||||
<div class="form-row">
|
||||
<div class="form-col form-col--two-thirds">
|
||||
{{ TextInput(form.description, paragraph=True, optional=True) }}
|
||||
|
@ -19,7 +19,7 @@
|
||||
<p>
|
||||
{{ 'portfolios.applications.new.step_2_description' | translate }}
|
||||
</p>
|
||||
<hr class="panel__break">
|
||||
<hr>
|
||||
<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">
|
||||
<div class="subheading">{{ 'portfolios.applications.environments_heading' | translate }}</div>
|
||||
@ -58,9 +58,9 @@
|
||||
{{ Icon("plus") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="action-group-footer">
|
||||
{% block next_button %}
|
||||
{{ SaveButton(text=('portfolios.applications.new.step_2_button_text' | translate)) }}
|
||||
|
@ -15,7 +15,7 @@
|
||||
<p>
|
||||
{{ ('portfolios.applications.new.step_3_description' | translate) }}
|
||||
</p>
|
||||
<hr class="panel__break">
|
||||
<hr>
|
||||
|
||||
{{ MemberManagementTemplate(
|
||||
application,
|
||||
|
@ -13,6 +13,9 @@
|
||||
|
||||
{% block application_content %}
|
||||
|
||||
{% if show_flash -%}
|
||||
{% include "fragments/flash.html" %}
|
||||
{%- endif %}
|
||||
<h3>{{ 'portfolios.applications.settings.name_description' | translate }}</h3>
|
||||
|
||||
{% if user_can(permissions.EDIT_APPLICATION) %}
|
||||
@ -59,59 +62,8 @@
|
||||
environments_obj,
|
||||
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) %}
|
||||
<hr>
|
||||
{% include "fragments/audit_events_log.html" %}
|
||||
{{ Pagination(audit_events, url=url_for('applications.settings', application_id=application.id)) }}
|
||||
{% endif %}
|
||||
|
@ -21,9 +21,7 @@
|
||||
{% include 'navigation/topbar.html' %}
|
||||
|
||||
<div class='global-layout'>
|
||||
{% if portfolios %}
|
||||
{% include 'navigation/global_sidenav.html' %}
|
||||
{% endif %}
|
||||
{% include 'navigation/global_sidenav.html' %}
|
||||
|
||||
<div class='global-panel-container'>
|
||||
{% block sidenav %}{% endblock %}
|
||||
|
@ -1,35 +1,11 @@
|
||||
{% from "components/icon.html" import Icon %}
|
||||
|
||||
{% macro SidenavItem(label, href, active=False, icon=None, subnav=None) -%}
|
||||
{% macro SidenavItem(label, href, active=False) -%}
|
||||
<li>
|
||||
<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">
|
||||
{{label}}
|
||||
</span>
|
||||
{% if active %}
|
||||
<span class="sidenav__link-active_indicator">
|
||||
{{ Icon("caret_right") }}
|
||||
<a class="sidenav__link {% if active %}sidenav__link--active{% endif %}" href="{{href}}" title="{{label}}">
|
||||
<span class="sidenav__link-label">
|
||||
{{label}}
|
||||
</span>
|
||||
{% endif %}
|
||||
</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 %}
|
||||
</a>
|
||||
</li>
|
||||
{%- endmacro %}
|
||||
|
@ -1,8 +1,7 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% from "components/sticky_cta.html" import StickyCTA %}
|
||||
{% from "components/icon.html" import Icon %}
|
||||
{% from "components/semi_collapsible_text.html" import SemiCollapsibleText %}
|
||||
{% from "components/sticky_cta.html" import StickyCTA %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
@ -13,88 +12,56 @@
|
||||
{% set sticky_header = "home.get_started" | translate %}
|
||||
{% endif %}
|
||||
|
||||
{% call StickyCTA(sticky_header) %}
|
||||
<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">
|
||||
<div class="home__content">
|
||||
{% include "fragments/flash.html" %}
|
||||
|
||||
<h1>{{ "home.head" | translate }}</h1>
|
||||
<h3>Set up a Portfolio</h3>
|
||||
<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>
|
||||
<hr>
|
||||
|
||||
{{ SemiCollapsibleText(first_half=("home.about_cloud.part1"|translate), second_half=("home.about_cloud.part2"|translate)) }}
|
||||
|
||||
<div class="your-project">
|
||||
<h2 class="h3">{{ "home.your_project" | translate }}</h2>
|
||||
<p>{{ "home.your_project_descrip" | translate }}</p>
|
||||
|
||||
<hr>
|
||||
|
||||
{% macro Link(icon, text, section, default=False) %}
|
||||
{% if default %}
|
||||
<div v-bind:class='{"icon-link": true, active: selectedSection === "{{ section }}" || selectedSection === null}' v-on:click="toggleSection('{{ section }}')">
|
||||
{% else %}
|
||||
<div v-bind:class='{"icon-link": true, active: selectedSection === "{{ section }}"}' v-on:click="toggleSection('{{ section }}')">
|
||||
{% endif %}
|
||||
<div class="col">
|
||||
<div class='icon-link--icon'>{{ Icon(icon) }}</div>
|
||||
<div class='icon-link--name'>{{ text }}</div>
|
||||
</div>
|
||||
<div class="home__content--descriptions">
|
||||
<div class="row">
|
||||
<div class="col col--half col--pad">
|
||||
{{ Icon('funding', classes="icon--home-pg-badge") }}
|
||||
<h4>{{ "navigation.portfolio_navigation.breadcrumbs.funding" | translate }}</h4>
|
||||
<p>
|
||||
{{ "home.funding_descrip" | translate }}
|
||||
</p>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
<toggler inline-template v-bind:initial-selected-section="'funding'">
|
||||
<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>
|
||||
{% endmacro %}
|
||||
<div class="project-section-descriptions">
|
||||
{{ Description('funding', default=True) }}
|
||||
{{ Description('applications') }}
|
||||
{{ Description('reports') }}
|
||||
{{ Description('admin') }}
|
||||
</div>
|
||||
<div class="col col--half col--pad">
|
||||
{{ Icon('chart-pie', classes="icon--home-pg-badge") }}
|
||||
<h4>{{ "navigation.portfolio_navigation.breadcrumbs.reports" | translate }}</h4>
|
||||
<p>
|
||||
{{ "home.reports_descrip" | translate }}
|
||||
</p>
|
||||
</div>
|
||||
</toggler>
|
||||
|
||||
</div>
|
||||
<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>
|
||||
<img id='jedi-heirarchy' src="{{ url_for("static", filename="img/JEDIhierarchyDiagram.png")}}" alt="JEDI heirarchy diagram">
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
@ -7,29 +7,29 @@
|
||||
<div v-bind:class="{'sidenav-container': props.isVisible, 'sidenav-container--minimized': !props.isVisible}">
|
||||
<div class="sidenav-container__fixed">
|
||||
<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">
|
||||
{{ Icon('angle-double-left-solid', classes="toggle-arrows icon--blue") }}
|
||||
Hide
|
||||
<span class="sidenav__title col col--grow">My Portfolios</span>
|
||||
<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 v-else>
|
||||
Show
|
||||
{{ Icon('angle-double-right-solid', classes="toggle-arrows icon--blue") }}
|
||||
<a href="#" v-on:click="props.toggle" class="sidenav__toggle col">
|
||||
<span>Show</span>
|
||||
{{ Icon('angle-double-right-solid', classes="toggle-arrows icon--primary") }}
|
||||
</a>
|
||||
</template>
|
||||
</a>
|
||||
</div>
|
||||
<div v-if="props.isVisible">
|
||||
<div class="sidenav__title">Portfolios</div>
|
||||
<ul class="sidenav__list--padded">
|
||||
{% if portfolios %}
|
||||
{% for other_portfolio in portfolios|sort(attribute='name') %}
|
||||
{{ SidenavItem(other_portfolio.name,
|
||||
href=url_for("applications.portfolio_applications", portfolio_id=other_portfolio.id),
|
||||
active=(other_portfolio.id | string) == request.view_args.get('portfolio_id')
|
||||
) }}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<li><span class="sidenav__text">You have no portfolios yet</span></li>
|
||||
{% endif %}
|
||||
{% for other_portfolio in portfolios|sort(attribute='name') %}
|
||||
{{ SidenavItem(other_portfolio.name,
|
||||
href=url_for("applications.portfolio_applications", portfolio_id=other_portfolio.id),
|
||||
active=(other_portfolio.id | string) == request.view_args.get('portfolio_id')
|
||||
) }}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -56,14 +56,10 @@
|
||||
{% include "portfolios/fragments/primary_point_of_contact.html" %}
|
||||
{% endif %}
|
||||
|
||||
{% if user_can(permissions.ARCHIVE_PORTFOLIO) %}
|
||||
{% include "portfolios/fragments/delete_portfolio.html" %}
|
||||
{% endif %}
|
||||
|
||||
{% if user_can(permissions.VIEW_PORTFOLIO_USERS) %}
|
||||
{% include "portfolios/fragments/portfolio_members.html" %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if user_can(permissions.VIEW_PORTFOLIO_ACTIVITY_LOG) and config.get("USE_AUDIT_LOG", False) %}
|
||||
{% include "fragments/audit_events_log.html" %}
|
||||
{{ Pagination(audit_events, url_for('portfolios.admin', portfolio_id=portfolio.id)) }}
|
||||
|
@ -16,7 +16,7 @@
|
||||
{% endmacro %}
|
||||
|
||||
{% set step_one %}
|
||||
<hr>
|
||||
<hr class="full-width">
|
||||
<h1>Invite new portfolio member</h1>
|
||||
<div class='form-row'>
|
||||
<div class='form-col form-col--half'>
|
||||
@ -52,7 +52,7 @@
|
||||
</div>
|
||||
{% endset %}
|
||||
{% set step_two %}
|
||||
<hr>
|
||||
<hr class="full-width">
|
||||
<h1>Assign member permissions</h1>
|
||||
<a class='icon-link'>
|
||||
{{ Icon('info') }}
|
||||
|
@ -5,7 +5,7 @@
|
||||
{% from "components/options_input.html" import OptionsInput %}
|
||||
|
||||
{% set step_one %}
|
||||
<hr>
|
||||
<hr class="full-width">
|
||||
<h1>{{ "fragments.ppoc.update_ppoc_title" | translate }}</h1>
|
||||
|
||||
{{
|
||||
@ -42,7 +42,7 @@
|
||||
{% endset %}
|
||||
|
||||
{% set step_two %}
|
||||
<hr>
|
||||
<hr class="full-width">
|
||||
<h1>{{ "fragments.ppoc.update_ppoc_confirmation_title" | translate }}</h1>
|
||||
|
||||
{{
|
||||
|
@ -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 %}
|
@ -16,36 +16,36 @@
|
||||
</div>
|
||||
{{ StickyCTA(text="Create New Portfolio") }}
|
||||
<base-form inline-template>
|
||||
<form id="portfolio-create" action="{{ url_for('portfolios.create_portfolio') }}" method="POST">
|
||||
{{ form.csrf_token }}
|
||||
<div class="form-row form-row--separated">
|
||||
<div class="form-col">
|
||||
{{ TextInput(form.name, optional=False) }}
|
||||
{{"forms.portfolio.name.help_text" | translate | safe }}
|
||||
<div class="row">
|
||||
<form id="portfolio-create" class="col" action="{{ url_for('portfolios.create_portfolio') }}" method="POST">
|
||||
{{ form.csrf_token }}
|
||||
<div class="form-row form-row--bordered">
|
||||
<div class="form-col">
|
||||
{{ TextInput(form.name, optional=False, classes="form-col") }}
|
||||
{{"forms.portfolio.name.help_text" | translate | safe }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row form-row--separated">
|
||||
<div class="form-col">
|
||||
{{ TextInput(form.description, paragraph=True) }}
|
||||
{{"forms.portfolio.description.help_text" | translate | safe }}
|
||||
<div class="form-row form-row--bordered">
|
||||
<div class="form-col">
|
||||
{{ TextInput(form.description, paragraph=True) }}
|
||||
{{"forms.portfolio.description.help_text" | translate | safe }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-col">
|
||||
{{ MultiCheckboxInput(form.defense_component, optional=False) }}
|
||||
{{ "forms.portfolio.defense_component.help_text" | translate | safe }}
|
||||
<div class="form-row">
|
||||
<div class="form-col">
|
||||
{{ MultiCheckboxInput(form.defense_component, optional=False) }}
|
||||
{{ "forms.portfolio.defense_component.help_text" | translate | safe }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class='action-group'>
|
||||
{{
|
||||
SaveButton(
|
||||
text=('common.save' | translate),
|
||||
form="portfolio-create",
|
||||
element="input",
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
</form>
|
||||
<div class='action-group-footer'>
|
||||
{% block next_button %}
|
||||
{{ SaveButton(text=('common.save' | translate), form="portfolio-create", element="input") }}
|
||||
{% endblock %}
|
||||
<a href="{{ url_for('applications.portfolio_applications', portfolio_id=portfolio.id) }}">
|
||||
Cancel
|
||||
</a>
|
||||
</form>
|
||||
</div>
|
||||
</base-form>
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
@ -8,8 +8,26 @@
|
||||
<form id="to_form" action='{{ action }}' method="POST" autocomplete="off" enctype="multipart/form-data">
|
||||
{{ form.csrf_token }}
|
||||
|
||||
{% call StickyCTA(text=('task_orders.form.sticky_header_text' | translate({"step": step}) )) %}
|
||||
<span class="action-group">
|
||||
{{ StickyCTA(
|
||||
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 %}
|
||||
<input
|
||||
type="submit"
|
||||
@ -32,23 +50,6 @@
|
||||
{{ "common.cancel" | translate }}
|
||||
</a>
|
||||
</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>
|
||||
</to-form>
|
||||
|
@ -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="h2">
|
||||
{{ title }}
|
||||
</div>
|
||||
{% if title -%}
|
||||
<div class="h2">
|
||||
{{ title }}
|
||||
</div>
|
||||
{%- endif %}
|
||||
{% if to_number %}
|
||||
<p>
|
||||
<strong>Task Order Number:</strong> {{ to_number }}
|
||||
|
@ -1,7 +1,6 @@
|
||||
{% extends "task_orders/builder_base.html" %}
|
||||
|
||||
{% from 'components/icon.html' import Icon %}
|
||||
{% from "components/sticky_cta.html" import StickyCTA %}
|
||||
{% from "task_orders/form_header.html" import TOFormStepHeader %}
|
||||
{% from 'components/upload_input.html' import UploadInput %}
|
||||
|
||||
|
@ -10,11 +10,16 @@
|
||||
{% set step = "5" %}
|
||||
|
||||
{% block to_builder_form_field %}
|
||||
{{ TOFormStepHeader('task_orders.form.step_5.title' | translate, 'task_orders.form.step_5.description' | translate, task_order.number) }}
|
||||
|
||||
{% call Alert('',
|
||||
message="task_orders.form.step_5.alert_message" | translate
|
||||
) %}
|
||||
{{ TOFormStepHeader('task_orders.form.step_5.description' | translate, to_number=task_order.number) }}
|
||||
<div class="task-order__confirmation">
|
||||
{{ 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 %}
|
||||
|
31
terraform/modules/cdn/main.tf
Normal file
31
terraform/modules/cdn/main.tf
Normal 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
|
||||
}
|
||||
}
|
0
terraform/modules/cdn/outputs.tf
Normal file
0
terraform/modules/cdn/outputs.tf
Normal file
31
terraform/modules/cdn/variables.tf
Normal file
31
terraform/modules/cdn/variables.tf
Normal 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"
|
||||
}
|
||||
|
13
terraform/modules/container_registry/main.tf
Normal file
13
terraform/modules/container_registry/main.tf
Normal 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]
|
||||
}
|
0
terraform/modules/container_registry/outputs.tf
Normal file
0
terraform/modules/container_registry/outputs.tf
Normal file
37
terraform/modules/container_registry/variables.tf
Normal file
37
terraform/modules/container_registry/variables.tf
Normal 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
|
||||
|
||||
}
|
22
terraform/modules/lb/main.tf
Normal file
22
terraform/modules/lb/main.tf
Normal 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
|
||||
}
|
||||
}
|
0
terraform/modules/lb/outputs.tf
Normal file
0
terraform/modules/lb/outputs.tf
Normal file
19
terraform/modules/lb/variables.tf
Normal file
19
terraform/modules/lb/variables.tf
Normal 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"
|
||||
}
|
24
terraform/modules/redis/main.tf
Normal file
24
terraform/modules/redis/main.tf
Normal 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
|
||||
}
|
||||
}
|
0
terraform/modules/redis/outputs.tf
Normal file
0
terraform/modules/redis/outputs.tf
Normal file
60
terraform/modules/redis/variables.tf
Normal file
60
terraform/modules/redis/variables.tf
Normal 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)"
|
||||
}
|
@ -36,9 +36,9 @@ resource "azurerm_subnet" "subnet" {
|
||||
address_prefix = element(split(",", each.value), 0)
|
||||
|
||||
# See https://github.com/terraform-providers/terraform-provider-azurerm/issues/3471
|
||||
lifecycle {
|
||||
ignore_changes = [route_table_id]
|
||||
}
|
||||
lifecycle {
|
||||
ignore_changes = [route_table_id]
|
||||
}
|
||||
#delegation {
|
||||
# name = "acctestdelegation"
|
||||
#
|
||||
@ -57,16 +57,58 @@ resource "azurerm_route_table" "route_table" {
|
||||
}
|
||||
|
||||
resource "azurerm_subnet_route_table_association" "route_table" {
|
||||
for_each = var.networks
|
||||
subnet_id = azurerm_subnet.subnet[each.key].id
|
||||
for_each = var.networks
|
||||
subnet_id = azurerm_subnet.subnet[each.key].id
|
||||
route_table_id = azurerm_route_table.route_table[each.key].id
|
||||
}
|
||||
|
||||
resource "azurerm_route" "route" {
|
||||
for_each = var.route_tables
|
||||
name = "${var.name}-${var.environment}-default"
|
||||
for_each = var.route_tables
|
||||
name = "${var.name}-${var.environment}-default"
|
||||
resource_group_name = azurerm_resource_group.vpc.name
|
||||
route_table_name = azurerm_route_table.route_table[each.key].name
|
||||
route_table_name = azurerm_route_table.route_table[each.key].name
|
||||
address_prefix = "0.0.0.0/0"
|
||||
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"]
|
||||
}
|
||||
}
|
@ -41,3 +41,8 @@ variable "route_tables" {
|
||||
type = map
|
||||
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'"
|
||||
}
|
||||
|
8
terraform/providers/dev/cdn.tf
Normal file
8
terraform/providers/dev/cdn.tf
Normal 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
|
||||
}
|
8
terraform/providers/dev/container_registry.tf
Normal file
8
terraform/providers/dev/container_registry.tf
Normal 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
|
||||
}
|
BIN
terraform/providers/dev/diagram/USEAST Development Network.png
Normal file
BIN
terraform/providers/dev/diagram/USEAST Development Network.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 85 KiB |
BIN
terraform/providers/dev/diagram/USWEST Development Network.png
Normal file
BIN
terraform/providers/dev/diagram/USWEST Development Network.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 71 KiB |
50
terraform/providers/dev/diagram/useast.txt
Normal file
50
terraform/providers/dev/diagram/useast.txt
Normal 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
|
40
terraform/providers/dev/diagram/uswest.txt
Normal file
40
terraform/providers/dev/diagram/uswest.txt
Normal 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
|
@ -9,3 +9,10 @@ module "k8s" {
|
||||
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
|
||||
}
|
||||
|
7
terraform/providers/dev/redis.tf
Normal file
7
terraform/providers/dev/redis.tf
Normal file
@ -0,0 +1,7 @@
|
||||
module "redis" {
|
||||
source = "../../modules/redis"
|
||||
owner = var.owner
|
||||
environment = var.environment
|
||||
region = var.region
|
||||
name = var.name
|
||||
}
|
@ -7,6 +7,11 @@ variable "region" {
|
||||
|
||||
}
|
||||
|
||||
variable "backup_region" {
|
||||
default = "westus2"
|
||||
}
|
||||
|
||||
|
||||
variable "owner" {
|
||||
default = "dev"
|
||||
}
|
||||
@ -31,6 +36,12 @@ variable "networks" {
|
||||
}
|
||||
}
|
||||
|
||||
variable "gateway_subnet" {
|
||||
type = string
|
||||
default = "10.1.20.0/24"
|
||||
}
|
||||
|
||||
|
||||
variable "route_tables" {
|
||||
description = "Route tables and their default routes"
|
||||
type = map
|
||||
|
@ -4,6 +4,7 @@ module "vpc" {
|
||||
region = var.region
|
||||
virtual_network = var.virtual_network
|
||||
networks = var.networks
|
||||
gateway_subnet = var.gateway_subnet
|
||||
route_tables = var.route_tables
|
||||
owner = var.owner
|
||||
name = var.name
|
||||
|
@ -4,7 +4,7 @@ from uuid import uuid4
|
||||
|
||||
from atst.domain.environments import Environments
|
||||
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 tests.factories import (
|
||||
@ -100,6 +100,27 @@ def test_update_environment():
|
||||
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:
|
||||
@property
|
||||
def NOW(self):
|
||||
|
@ -52,8 +52,6 @@ def test_updating_application_environments_success(client, user_session):
|
||||
_external=True,
|
||||
fragment="application-environments",
|
||||
_anchor="application-environments",
|
||||
active_toggler=environment.id,
|
||||
active_toggler_section="edit",
|
||||
)
|
||||
assert environment.name == "new name a"
|
||||
|
||||
@ -78,6 +76,24 @@ def test_update_environment_failure(client, user_session):
|
||||
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):
|
||||
portfolio = PortfolioFactory.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!"
|
||||
|
||||
|
||||
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):
|
||||
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
|
||||
|
||||
|
||||
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):
|
||||
user = UserFactory.create()
|
||||
portfolio = PortfolioFactory(owner=user)
|
||||
|
@ -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))
|
||||
assert response.status_code == 200
|
||||
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
|
||||
|
@ -343,40 +343,6 @@ def test_portfolios_invite_member_access(post_url_assert_status):
|
||||
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
|
||||
def test_application_settings_access(get_url_assert_status):
|
||||
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]
|
||||
|
||||
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)
|
||||
post_url_assert_status(dev, url, 200)
|
||||
post_url_assert_status(ccpo, url, 200)
|
||||
post_url_assert_status(rando, url, 404)
|
||||
post_url_assert_status(dev, url, 302, data=_form_data())
|
||||
post_url_assert_status(ccpo, url, 302, data=_form_data())
|
||||
post_url_assert_status(rando, url, 404, data=_form_data())
|
||||
|
||||
|
||||
# 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(ccpo, url, 302, 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,
|
||||
)
|
||||
|
@ -22,13 +22,13 @@ home:
|
||||
add_portfolio_button_text: Add New Portfolio
|
||||
new_portfolio: New Portfolio
|
||||
get_started: Get Started
|
||||
head: About Cloud Services
|
||||
head: JEDI Cloud Services
|
||||
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.
|
||||
funding_descrip: is information about all approved task orders associated to your portfolio.
|
||||
applications_descrip: ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
|
||||
reports_descrip: enim ad minim veniam, quis nostrud exercitation ullamco
|
||||
admin_descrip: aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat
|
||||
funding_descrip: The Task Orders section allows you to enter, manage, and edit awarded TOs associated to a specific Portfolio.
|
||||
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: The Reports section allows you to view and monitor funding usage within a specific Portfolio.
|
||||
admin_descrip: Within the Settings section, you can manage your Portfolio name and description, as well as add, edit, and delete Portfolio managers.
|
||||
ccpo:
|
||||
users_title: CCPO Users
|
||||
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.'
|
||||
name_error:
|
||||
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.'
|
||||
deleted_member: Portfolio member deleted
|
||||
environment_added: 'The environment "{env_name}" has been added to the application.'
|
||||
@ -199,7 +201,7 @@ forms:
|
||||
</ul>
|
||||
</p>
|
||||
</div>
|
||||
defense_component:
|
||||
defense_component:
|
||||
label: "Select DoD component(s) funding your Portfolio:"
|
||||
choices:
|
||||
air_force: Air Force
|
||||
@ -211,7 +213,7 @@ forms:
|
||||
help_text: |
|
||||
<p>
|
||||
Select the DOD component(s) that will fund all Applications within this Portfolio.
|
||||
In JEDI, multiple DoD organizations can fund the same Portfolio.<br/>
|
||||
In JEDI, multiple DoD organizations can fund the same Portfolio.<br/>
|
||||
Select all that apply.<br/>
|
||||
</p>
|
||||
attachment:
|
||||
@ -523,10 +525,11 @@ task_orders:
|
||||
next_button: 'Next: Review Task Order'
|
||||
step_5:
|
||||
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 Agency’s 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.
|
||||
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:
|
||||
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.
|
||||
@ -539,7 +542,11 @@ task_orders:
|
||||
subtitle: Who will be involved in the work funded by this task order?
|
||||
team_title: Your team
|
||||
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_list_title: '{status} Task Orders'
|
||||
JEDICLINType:
|
||||
|
@ -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 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.
|
||||
- The script is, for the most part, a series of docker commands, so try running the commands individually and debugging that way.
|
||||
|
Loading…
x
Reference in New Issue
Block a user