Merge pull request #1060 from dod-ccpo/app-settings-redesign

App settings redesign
This commit is contained in:
leigh-mil 2019-09-10 11:20:38 -04:00 committed by GitHub
commit dcb70ad925
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 566 additions and 1147 deletions

View File

@ -5,7 +5,6 @@ applications_bp = Blueprint("applications", __name__)
from . import index from . import index
from . import new from . import new
from . import settings from . import settings
from . import team
from . import invitations from . import invitations
from atst.domain.environment_roles import EnvironmentRoles from atst.domain.environment_roles import EnvironmentRoles
from atst.domain.exceptions import UnauthorizedError from atst.domain.exceptions import UnauthorizedError

View File

@ -1,17 +1,24 @@
from flask import redirect, render_template, request as http_request, url_for from flask import redirect, render_template, request as http_request, url_for, g
from . import applications_bp from . import applications_bp
from atst.domain.exceptions import AlreadyExistsError
from atst.domain.environments import Environments from atst.domain.environments import Environments
from atst.domain.applications import Applications from atst.domain.applications import Applications
from atst.domain.application_roles import ApplicationRoles
from atst.domain.audit_log import AuditLog from atst.domain.audit_log import AuditLog
from atst.domain.common import Paginator from atst.domain.common import Paginator
from atst.domain.environment_roles import EnvironmentRoles
from atst.forms.app_settings import AppEnvRolesForm from atst.forms.app_settings import AppEnvRolesForm
from atst.forms.application import ApplicationForm, EditEnvironmentForm from atst.forms.application import ApplicationForm, EditEnvironmentForm
from atst.forms.application_member import NewForm as NewMemberForm
from atst.forms.data import ENV_ROLE_NO_ACCESS as NO_ACCESS from atst.forms.data import ENV_ROLE_NO_ACCESS as NO_ACCESS
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.environment_role import CSPRole from atst.models.environment_role import CSPRole
from atst.models.permissions import Permissions from atst.models.permissions import Permissions
from atst.domain.permission_sets import PermissionSets
from atst.utils.flash import formatted_flash as flash from atst.utils.flash import formatted_flash as flash
from atst.utils.localization import translate
from atst.jobs import send_mail
def get_environments_obj_for_app(application): def get_environments_obj_for_app(application):
@ -79,12 +86,65 @@ def data_for_app_env_roles_form(application):
return {"envs": nested_data} return {"envs": nested_data}
def get_form_permission_value(member, edit_perm_set):
if member.has_permission_set(edit_perm_set):
return edit_perm_set
else:
return PermissionSets.VIEW_APPLICATION
def get_members_data(application):
members_data = []
for member in application.members:
permission_sets = {
"perms_team_mgmt": get_form_permission_value(
member, PermissionSets.EDIT_APPLICATION_TEAM
),
"perms_env_mgmt": get_form_permission_value(
member, PermissionSets.EDIT_APPLICATION_ENVIRONMENTS
),
"perms_del_env": get_form_permission_value(
member, PermissionSets.DELETE_APPLICATION_ENVIRONMENTS
),
}
roles = EnvironmentRoles.get_for_application_member(member.id)
environment_roles = [
{
"environment_id": str(role.environment.id),
"environment_name": role.environment.name,
"role": role.role,
}
for role in roles
]
members_data.append(
{
"role_id": member.id,
"user_name": member.user_name,
"permission_sets": permission_sets,
"environment_roles": environment_roles,
}
)
return members_data
def get_new_member_form(application):
env_roles = [
{"environment_id": e.id, "environment_name": e.name}
for e in application.environments
]
return NewMemberForm(data={"environment_roles": env_roles})
def render_settings_page(application, **kwargs): def render_settings_page(application, **kwargs):
environments_obj = get_environments_obj_for_app(application=application) environments_obj = get_environments_obj_for_app(application=application)
members_form = AppEnvRolesForm(data=data_for_app_env_roles_form(application)) members_form = AppEnvRolesForm(data=data_for_app_env_roles_form(application))
new_env_form = EditEnvironmentForm() new_env_form = EditEnvironmentForm()
pagination_opts = Paginator.get_pagination_opts(http_request) pagination_opts = Paginator.get_pagination_opts(http_request)
audit_events = AuditLog.get_application_events(application, pagination_opts) audit_events = AuditLog.get_application_events(application, pagination_opts)
new_member_form = get_new_member_form(application)
members = get_members_data(application)
if "application_form" not in kwargs: if "application_form" not in kwargs:
kwargs["application_form"] = ApplicationForm( kwargs["application_form"] = ApplicationForm(
@ -98,10 +158,23 @@ def render_settings_page(application, **kwargs):
members_form=members_form, members_form=members_form,
new_env_form=new_env_form, new_env_form=new_env_form,
audit_events=audit_events, audit_events=audit_events,
new_member_form=new_member_form,
members=members,
**kwargs, **kwargs,
) )
def send_application_invitation(invitee_email, inviter_name, token):
body = render_template(
"emails/application/invitation.txt", owner=inviter_name, token=token
)
send_mail.delay(
[invitee_email],
translate("email.application_invite", {"inviter_name": inviter_name}),
body,
)
@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):
@ -264,3 +337,72 @@ def delete_environment(environment_id):
fragment="application-environments", fragment="application-environments",
) )
) )
@applications_bp.route("/application/<application_id>/members/new", methods=["POST"])
@user_can(
Permissions.CREATE_APPLICATION_MEMBER, message="create new application member"
)
def create_member(application_id):
application = Applications.get(application_id)
form = NewMemberForm(http_request.form)
if form.validate():
try:
invite = Applications.invite(
application=application,
inviter=g.current_user,
user_data=form.user_data.data,
permission_sets_names=form.permission_sets.data,
environment_roles_data=form.environment_roles.data,
)
send_application_invitation(
invitee_email=invite.email,
inviter_name=g.current_user.full_name,
token=invite.token,
)
flash("new_application_member", user_name=invite.user_name)
except AlreadyExistsError:
return render_template(
"error.html", message="There was an error processing your request."
)
else:
pass
# TODO: flash error message
return redirect(
url_for(
"applications.settings",
application_id=application_id,
fragment="application-members",
_anchor="application-members",
)
)
@applications_bp.route(
"/applications/<application_id>/members/<application_role_id>/delete",
methods=["POST"],
)
@user_can(Permissions.DELETE_APPLICATION_MEMBER, message="remove application member")
def remove_member(application_id, application_role_id):
application_role = ApplicationRoles.get_by_id(application_role_id)
Applications.remove_member(application_role)
flash(
"application_member_removed",
user_name=application_role.user_name,
application_name=g.application.name,
)
return redirect(
url_for(
"applications.settings",
_anchor="application-members",
application_id=g.application.id,
fragment="application-members",
)
)

View File

@ -1,205 +0,0 @@
from flask import render_template, request as http_request, g, url_for, redirect
from . import applications_bp
from atst.domain.applications import Applications
from atst.domain.application_roles import ApplicationRoles
from atst.domain.authz.decorator import user_can_access_decorator as user_can
from atst.domain.environment_roles import EnvironmentRoles
from atst.domain.environments import Environments
from atst.domain.exceptions import AlreadyExistsError
from atst.domain.permission_sets import PermissionSets
from atst.forms.application_member import NewForm as NewMemberForm
from atst.forms.team import TeamForm
from atst.models import Permissions
from atst.utils.flash import formatted_flash as flash
from atst.utils.localization import translate
from atst.jobs import send_mail
def get_form_permission_value(member, edit_perm_set):
if member.has_permission_set(edit_perm_set):
return edit_perm_set
else:
return PermissionSets.VIEW_APPLICATION
def get_team_form(application):
team_data = []
for member in application.members:
permission_sets = {
"perms_team_mgmt": get_form_permission_value(
member, PermissionSets.EDIT_APPLICATION_TEAM
),
"perms_env_mgmt": get_form_permission_value(
member, PermissionSets.EDIT_APPLICATION_ENVIRONMENTS
),
"perms_del_env": get_form_permission_value(
member, PermissionSets.DELETE_APPLICATION_ENVIRONMENTS
),
}
roles = EnvironmentRoles.get_for_application_member(member.id)
environment_roles = [
{
"environment_id": str(role.environment.id),
"environment_name": role.environment.name,
"role": role.role,
}
for role in roles
]
team_data.append(
{
"role_id": member.id,
"user_name": member.user_name,
"permission_sets": permission_sets,
"environment_roles": environment_roles,
}
)
return TeamForm(data={"members": team_data})
def get_new_member_form(application):
env_roles = [
{"environment_id": e.id, "environment_name": e.name}
for e in application.environments
]
return NewMemberForm(data={"environment_roles": env_roles})
def render_team_page(application):
team_form = get_team_form(application)
new_member_form = get_new_member_form(application)
return render_template(
"portfolios/applications/team.html",
application=application,
team_form=team_form,
new_member_form=new_member_form,
)
@applications_bp.route("/applications/<application_id>/team")
@user_can(Permissions.VIEW_APPLICATION, message="view portfolio applications")
def team(application_id):
application = Applications.get(resource_id=application_id)
return render_team_page(application)
@applications_bp.route("/application/<application_id>/team", methods=["POST"])
@user_can(Permissions.EDIT_APPLICATION_MEMBER, message="update application member")
def update_team(application_id):
application = Applications.get(application_id)
form = TeamForm(http_request.form)
if form.validate():
for member_form in form.members:
app_role = ApplicationRoles.get_by_id(member_form.role_id.data)
new_perms = [
perm
for perm in member_form.data["permission_sets"]
if perm != PermissionSets.VIEW_APPLICATION
]
ApplicationRoles.update_permission_sets(app_role, new_perms)
for environment_role_form in member_form.environment_roles:
environment = Environments.get(
environment_role_form.environment_id.data
)
Environments.update_env_role(
environment, app_role, environment_role_form.data.get("role")
)
flash("updated_application_team_settings", application_name=application.name)
return redirect(
url_for(
"applications.team",
application_id=application_id,
fragment="application-members",
_anchor="application-members",
)
)
else:
return (render_team_page(application), 400)
def send_application_invitation(invitee_email, inviter_name, token):
body = render_template(
"emails/application/invitation.txt", owner=inviter_name, token=token
)
send_mail.delay(
[invitee_email],
translate("email.application_invite", {"inviter_name": inviter_name}),
body,
)
@applications_bp.route("/application/<application_id>/members/new", methods=["POST"])
@user_can(
Permissions.CREATE_APPLICATION_MEMBER, message="create new application member"
)
def create_member(application_id):
application = Applications.get(application_id)
form = NewMemberForm(http_request.form)
if form.validate():
try:
invite = Applications.invite(
application=application,
inviter=g.current_user,
user_data=form.user_data.data,
permission_sets_names=form.permission_sets.data,
environment_roles_data=form.environment_roles.data,
)
send_application_invitation(
invitee_email=invite.email,
inviter_name=g.current_user.full_name,
token=invite.token,
)
flash("new_application_member", user_name=invite.user_name)
except AlreadyExistsError:
return render_template(
"error.html", message="There was an error processing your request."
)
else:
pass
# TODO: flash error message
return redirect(
url_for(
"applications.team",
application_id=application_id,
fragment="application-members",
_anchor="application-members",
)
)
@applications_bp.route(
"/applications/<application_id>/members/<application_role_id>/delete",
methods=["POST"],
)
@user_can(Permissions.DELETE_APPLICATION_MEMBER, message="remove application member")
def remove_member(application_id, application_role_id):
application_role = ApplicationRoles.get_by_id(application_role_id)
Applications.remove_member(application_role)
flash(
"application_member_removed",
user_name=application_role.user_name,
application_name=g.application.name,
)
return redirect(
url_for(
"applications.team",
_anchor="application-members",
application_id=g.application.id,
fragment="application-members",
)
)

View File

@ -139,6 +139,9 @@
} }
table { table {
margin: 0;
width: 100%;
thead { thead {
th:first-child { th:first-child {
padding-left: 3 * $gap; padding-left: 3 * $gap;
@ -282,6 +285,13 @@
.application-content { .application-content {
.subheading { .subheading {
@include subheading; @include subheading;
position: relative;
.icon-link__add {
position: absolute;
right: 0;
top: 0;
}
} }
.panel { .panel {
@ -312,6 +322,34 @@
input#delete-application { input#delete-application {
margin-top: $gap * 3; margin-top: $gap * 3;
} }
.accordion-table__item-content.form-row {
margin-bottom: 0;
margin-top: 0;
padding-bottom: 0;
}
li.accordion-table__item__expanded {
height: auto;
}
.environment-list__item {
position: relative;
height: 7rem;
}
span.accordion-table__item__toggler.icon-link {
font-size: $small-font-size;
font-weight: $font-normal;
position: absolute;
left: -$gap * 1.25;
bottom: 0;
}
a.application-list-item__environment__csp_link.icon-link {
font-size: $small-font-size;
font-weight: $font-normal;
}
} }
.activity-log { .activity-log {

View File

@ -4,7 +4,6 @@
z-index: 10; z-index: 10;
@include media($medium-screen) { @include media($medium-screen) {
margin-left: -$gap * 5;
margin-right: -$gap * 5; margin-right: -$gap * 5;
} }
@ -53,4 +52,10 @@
} }
} }
} }
&-return-link {
padding-top: 1.6rem;
font-size: $small-font-size;
font-weight: $font-bold;
}
} }

View File

@ -58,6 +58,7 @@
.usa-input { .usa-input {
margin: ($gap * 4) ($gap * 2) ($gap * 4) 0; margin: ($gap * 4) ($gap * 2) ($gap * 4) 0;
max-width: 75rem;
@include media($medium-screen) { @include media($medium-screen) {
margin: ($gap * 4) 0; margin: ($gap * 4) 0;

View File

@ -60,11 +60,6 @@
} }
} }
.app-team-settings-link {
font-size: $small-font-size;
font-weight: $font-normal;
}
.environment-roles { .environment-roles {
padding: 0 ($gap * 3) ($gap * 3); padding: 0 ($gap * 3) ($gap * 3);

View File

@ -1,12 +1,23 @@
{% macro StickyCTA(text) -%} {% from 'components/icon.html' import Icon %}
{% macro StickyCTA(text, return_link_url=None, return_link_text=None) -%}
<div class="sticky-cta" v-sticky='{ "stickyBitStickyOffset": 76 }'> <div class="sticky-cta" v-sticky='{ "stickyBitStickyOffset": 76 }'>
<div class="sticky-cta-container"> <div class="sticky-cta-container">
<div class="sticky-cta-text"> <div class="sticky-cta-text">
{% if return_link_url and return_link_text %}
<div class="sticky-cta-return-link">
<a href="{{ return_link_url }}">
{{ Icon('caret_left', classes="icon--tiny icon--blue") }} {{ return_link_text}}
</a>
</div>
{% endif %}
<h3>{{ text }}</h3> <h3>{{ text }}</h3>
</div> </div>
<div class="sticky-cta-buttons"> {% if caller %}
{{ caller() }} <div class="sticky-cta-buttons">
</div> {{ caller() }}
</div>
{% endif %}
</div> </div>
</div> </div>
{%- endmacro %} {%- endmacro %}

View File

@ -18,25 +18,17 @@
</div> </div>
<div class="panel__footer"> <div class="panel__footer">
<div class="action-group"> <div class="action-group">
<div class='action-group-cancel'> {{ SaveButton(text=('common.save' | translate), element="input", form="add-new-env") }}
<a class='action-group-cancel__action icon-link icon-link--default' v-on:click="toggle"> <a class='action-group__action icon-link icon-link--default' v-on:click="toggle">
{{ "common.cancel" | translate }} {{ "common.cancel" | translate }}
</a> </a>
{{ SaveButton(text=('common.save' | translate), element="input", form="add-new-env") }}
</div>
</div> </div>
</div> </div>
</form> </form>
</div> </div>
<a class='icon-link icon-link__add' v-on:click="toggle">
<div v-else class="panel__footer"> {{ Icon('plus') }}
<div class="action-group"> {{ "portfolios.applications.add_environment" | translate }}
<a class='icon-link' v-on:click="toggle"> </a>
{{ "portfolios.applications.add_environment" | translate }}
{{ Icon('plus') }}
</a>
</div>
</div>
</div> </div>
</new-environment> </new-environment>

View File

@ -1,97 +0,0 @@
{% from "components/icon.html" import Icon %}
{% from "components/save_button.html" import SaveButton %}
{% for env_form in members_form.envs %}
{% if env_form.env_id.data == env['id'] %}
<div class='app-team-settings-link'>
{{ 'fragments.edit_environment_team_form.add_new_member_text' | translate }}
<a href='{{ url_for("applications.team", application_id=application.id) }}'>
{{ 'fragments.edit_environment_team_form.add_new_member_link' | translate }}
</a>
</div>
<form
action="{{ url_for('applications.update_env_roles', environment_id=env['id']) }}"
method="post">
{{ members_form.csrf_token }}
{{ env_form.env_id() }}
<edit-environment-role
inline-template
v-bind:initial-role-categories='{{ env_form.team_roles.data | tojson }}'>
<div>
<div v-for='(roleCategory, roleindex) in roleCategories' class='environment-role'>
<h4 v-if='checkNoAccess(roleCategory.role)'>
{{ 'fragments.edit_environment_team_form.unassigned_title' | translate }}
</h4>
<h4 v-else v-html='roleCategory.role'></h4>
<ul class='environment-role__users'>
<div
v-if="roleCategory.members && !roleCategory.members.length"
class='environment-role__no-user'>
{{ 'fragments.edit_environment_team_form.no_members' | translate }}
</div>
<li
v-for='(member, memberindex) in roleCategory.members'
class="environment-role__user"
v-bind:class="{'unassigned': checkNoAccess(member.role_name)}">
<span v-html='member.user_name'>
</span>
<span v-on:click="toggleSection(member.application_role_id)" class="icon-link right">
{{ Icon('edit', classes="icon--medium") }}
</span>
<div
v-show="selectedSection === member.application_role_id"
class='environment-role__user-field'>
<div class="usa-input">
<fieldset
data-ally-disabled="true"
class="usa-input__choices"
v-on:change="onInput">
<ul
v-for='(roleCategory, roleinputindex) in roleCategories'
v-bind:id="'envs-{{ loop.index0 }}-team_roles-' + roleindex + '-members-' + memberindex + '-role_name'">
<li>
<input
v-bind:checked="member.role_name === roleCategory.role"
v-bind:name="'envs-{{ loop.index0 }}-team_roles-' + roleindex + '-members-' + memberindex + '-role_name'"
v-bind:id="'envs-{{ loop.index0 }}-team_roles-' + roleindex + '-members-' + memberindex + '-role_name-' + roleinputindex"
type="radio"
v-bind:user-id='member.application_role_id'
v-bind:value='roleCategory.role'>
<label
v-bind:for="'envs-{{ loop.index0 }}-team_roles-' + roleindex + '-members-' + memberindex + '-role_name-' + roleinputindex">
<span v-if='checkNoAccess(roleCategory.role)'>
{{ 'fragments.edit_environment_team_form.no_access' | translate }}
</span>
<span v-else v-html='roleCategory.role'></span>
</label>
</li>
</ul>
</fieldset>
</div>
</div>
<input
v-bind:id="'envs-{{ loop.index0 }}-team_roles-' + roleindex + '-members-' + memberindex + '-application_role_id'"
v-bind:name="'envs-{{ loop.index0 }}-team_roles-' + roleindex + '-members-' + memberindex + '-application_role_id'"
type="hidden"
v-bind:value='member.application_role_id'>
</li>
</ul>
</div>
<div class='action-group'>
{{
SaveButton(
text=("common.save" | translate)
)
}}
</div>
</div>
</edit-environment-role>
<div class='action-group-cancel'>
<a class='action-group-cancel__action icon-link icon-link--default' v-on:click="toggleSection('members')">
{{ "common.cancel" | translate }}
</a>
</div>
</form>
{% endif %}
{% endfor %}

View File

@ -1,128 +0,0 @@
{% from "components/delete_confirmation.html" import DeleteConfirmation %}
{% from "components/icon.html" import Icon %}
{% from "components/modal.html" import Modal %}
{% from "components/options_input.html" import OptionsInput %}
{% from "components/save_button.html" import SaveButton %}
{% from "components/text_input.html" import TextInput %}
{% from "components/toggle_list.html" import ToggleButton, ToggleSection %}
<div class="application-list-item application-list">
<header>
<div class="responsive-table-wrapper__header">
<div class='responsive-table-wrapper__title'>
<div class='h3'>{{ 'portfolios.applications.environments_heading' | translate }}</div>
</div>
<a class='icon-link'>
{{ Icon('info') }}
{{ "portfolios.admin.settings_info" | translate }}
</a>
</div>
</header>
<div class="accordion-table accordion-table-list">
<div class="accordion-table__head row">
<div class="col col--grow">{{ "portfolios.applications.environments.name" | translate }}</div>
<div class="col col--grow">{{ "portfolios.applications.environments.edit_name" | translate }}</div>
<div class="col col--grow">{{ "common.delete" | translate }}</div>
<div class="col col--grow">{{ "common.members" | translate }}</div>
</div>
<ul class="accordion-table__items">
{% for env in environments_obj %}
{% set delete_environment_modal_id = "delete_modal_environment{}".format(env['id']) %}
{% set edit_form = env['edit_form'] %}
<toggler inline-template {% if active_toggler == (env['id'] | safe) %}initial-selected-section="{{ active_toggler_section }}"{% endif %}>
<li class="accordion-table__item">
<div class="accordion-table__item-content row">
<div class="col col--grow">
{{ env['name'] }}
</div>
<div class="col col--grow">
<span class="icon-link">
{% set edit_environment_button %}
{{ Icon('edit') }}
{% endset %}
{{
ToggleButton(
open_html=edit_environment_button,
close_html=edit_environment_button,
section_name="edit"
)
}}
</span>
</div>
<div class="col col--grow">
<span class="icon-link icon-link--danger" alt="Delete environment" v-on:click="openModal('{{ delete_environment_modal_id }}')">
{{ Icon('trash') }}
</span>
</div>
<div class="col col--grow icon-link icon-link--large accordion-table__item__toggler">
{% set open_members_button %}
{{ "common.members" | translate }} ({{ env['member_count'] }}) {{ Icon('caret_down') }}
{% endset %}
{% set close_members_button %}
{{ "common.members" | translate }} ({{ env['member_count'] }}) {{ Icon('caret_up') }}
{% endset %}
{{
ToggleButton(
open_html=open_members_button,
close_html=close_members_button,
section_name="members"
)
}}
</div>
</div>
{% call ToggleSection(section_name="members", classes="environment-roles") %}
{% include 'fragments/applications/edit_environment_team_form.html' %}
{% endcall %}
{% call ToggleSection(section_name="edit") %}
<ul>
<li class="accordion-table__item__expanded">
<form action="{{ url_for('applications.update_environment', environment_id=env['id']) }}" method="post" v-on:submit="handleSubmit">
{{ edit_form.csrf_token }}
{{ TextInput(edit_form.name, validation='requiredField') }}
{{
SaveButton(
text=("common.save" | translate)
)
}}
</form>
</li>
</ul>
{% endcall %}
</li>
</toggler>
{% call Modal(name=delete_environment_modal_id) %}
<h1>
{{ 'fragments.edit_environment_team_form.delete_environment_title' | translate }}
</h1>
{{
Alert(
level="warning",
title=('components.modal.destructive_title' | translate),
message=('components.modal.destructive_message' | translate({"resource": "environment"})),
)
}}
{{
DeleteConfirmation(
modal_id=delete_environment_modal_id,
delete_text=('portfolios.applications.environments.delete.button' | translate),
delete_action= url_for('applications.delete_environment', environment_id=env['id']),
form=edit_form
)
}}
{% endcall %}
{% endfor %}
</ul>
</div>
</div>

View File

@ -1,88 +0,0 @@
{% from "components/options_input.html" import OptionsInput %}
{% from "components/toggle_list.html" import ToggleButton, ToggleSection %}
{{ team_form.csrf_token }}
{% for member_form in team_form.members %}
{% set delete_modal_id = "delete-user-{}".format(member_form.id) %}
{% set environment_roles_form = member_form.environment_roles %}
{% set permissions_form = member_form.permission_sets %}
<toggler inline-template>
<li class="accordion-table__item">
<div class="accordion-table__item-content row">
<div class="col col--grow">
<div class="member-list__name">
{{ member_form.user_name.data }}
</div>
</div>
<div class="col col--grow">{{ OptionsInput(permissions_form.perms_team_mgmt, label=False, watch=True) }}</div>
<div class="col col--grow">{{ OptionsInput(permissions_form.perms_env_mgmt, label=False, watch=True) }}</div>
<div class="col col--grow">{{ OptionsInput(permissions_form.perms_del_env, label=False, watch=True) }}</div>
<div class="col col--grow icon-link icon-link--large accordion-table__item__toggler">
{% set open_html %}
{{ "portfolios.applications.team_settings.environments" | translate }} ({{ environment_roles_form | length }}) {{ Icon('caret_down') }}
{% endset %}
{% set close_html %}
{{ "portfolios.applications.team_settings.environments" | translate }} ({{ environment_roles_form | length }}) {{ Icon('caret_up') }}
{% endset %}
{{
ToggleButton(
open_html=open_html,
close_html=close_html,
section_name="environments"
)
}}
</div>
</div>
{% call ToggleSection(section_name="environments") %}
<ul>
{% for environment_form in environment_roles_form %}
<li class="accordion-table__item__expanded">
<environment-role inline-template v-bind:initial-role="'{{ environment_form.role.data }}'">
<div>
<div class="row">
<div class="col col--grow">
{{ environment_form.environment_name.data }}
</div>
<div class="accordion-table__item__expanded-role col col--grow">
<div class="right">
<span v-html="role">
</span>
<div class="icon-link" v-on:click="toggle">
{{ Icon("edit") }}
</div>
</div>
</div>
</div>
<div class="member-list__role-select" v-show="expanded">
{{ environment_form.role.label }}
{{ environment_form.role(**{"v-on:change": "radioChange", "class": "member-list____role-select__radio"}) }}
<button
class="usa-button"
type="button"
v-on:click="toggle"
>
{{ "common.close" | translate }}
</button>
{{ environment_form.environment_id() }}
</div>
</div>
</environment-role>
</li>
{% endfor %}
</ul>
<div class="accordion-table__item__action-group">
{% if user_can(permissions.DELETE_APPLICATION_MEMBER) %}
<a class="usa-button button-danger" v-on:click="openModal('{{ delete_modal_id }}')">
{{ "portfolios.applications.remove_member.button" | translate }}
</a>
{% endif %}
</div>
{% endcall %}
{{ member_form.role_id() }}
</li>
</toggler>
{% endfor %}

View File

@ -1,76 +0,0 @@
{% from "components/icon.html" import Icon %}
{% from "components/toggle_list.html" import ToggleButton, ToggleSection %}
<div class="application-list-item">
<header>
<div class="responsive-table-wrapper__header">
<div class='responsive-table-wrapper__title'>
<div class='h3'>{{ 'portfolios.applications.environments_heading' | translate }}</div>
</div>
</div>
</header>
<div class="accordion-table accordion-table-list">
<div class="accordion-table__head">
<span>{{ "portfolios.applications.environments.name" | translate }}</span>
</div>
<ul class="accordion-table__items">
{% for env in environments_obj %}
<toggler inline-template>
<li class="accordion-table__item">
<div class="accordion-table__item-content">
<span>
{{ env['name'] }}
</span>
<span class="icon-link icon-link--large accordion-table__item__toggler">
{% set open_members_button %}
{{ "common.members" | translate }} ({{ env['member_count'] }}) {{ Icon('caret_down') }}
{% endset %}
{% set close_members_button %}
{{ "common.members" | translate }} ({{ env['member_count'] }}) {{ Icon('caret_up') }}
{% endset %}
{{
ToggleButton(
open_html=open_members_button,
close_html=close_members_button,
section_name="members"
)
}}
</span>
</div>
{% call ToggleSection(section_name="members") %}
<ul>
{% for member in env['members'] %}
<li class="accordion-table__item__expanded">
<div class="accordion-table__item__expanded_first">{{ member }}</div>
</li>
{% endfor %}
</ul>
{% endcall %}
{% call ToggleSection(section_name="edit") %}
<ul>
<li class="accordion-table__item__expanded">
<div>
<form>
<div class="form-row">
<div class="form-col form-col--half">
Row here
</div>
</div>
</form>
</div>
</li>
</ul>
{% endcall %}
</li>
</toggler>
{% endfor %}
</ul>
</div>
</div>

View File

@ -1,47 +0,0 @@
{% from "components/toggle_list.html" import ToggleButton, ToggleSection %}
{% for member in team_form.members %}
{% set user_permissions = [member.permission_sets.perms_team_mgmt, member.permission_sets.perms_env_mgmt, member.permission_sets.perms_del_env] %}
{% macro PermissionField(value) %}
<div class="col col--grow user-permission{% if "Edit" in value or "Yes" in value %} green{% endif %}">{{ value }}</div>
{% endmacro %}
<toggler inline-template>
<li class="accordion-table__item">
<div class="accordion-table__item-content row">
<div class="col col--grow">{{ member.user_name.data }}</div>
{% for permission in user_permissions %}
{% set perm = dict(permission.choices).get(permission.data) %}
{{ PermissionField(perm) }}
{% endfor %}
<div class="col col--grow icon-link icon-link--large accordion-table__item__toggler">
{% set open_html %}
{{ "portfolios.applications.team_settings.environments" | translate }} ({{ member.environment_roles | length }}) {{ Icon('caret_down') }}
{% endset %}
{% set close_html %}
{{ "portfolios.applications.team_settings.environments" | translate }} ({{ member.environment_roles | length }}) {{ Icon('caret_up') }}
{% endset %}
{{
ToggleButton(
open_html=open_html,
close_html=close_html,
section_name="environments"
)
}}
</div>
</div>
{% call ToggleSection(section_name="environments") %}
<ul>
{% for environment in member.environment_roles %}
<li class="accordion-table__item__expanded">
{{ environment.environment_name.data }}
</li>
{% endfor %}
</ul>
{% endcall %}
</li>
</toggler>
{% endfor %}

View File

@ -1,11 +1,11 @@
{% extends "portfolios/base.html" %} {% extends "portfolios/base.html" %}
{% from "components/sticky_cta.html" import StickyCTA %}
{% block portfolio_header %} {% block portfolio_header %}
<div class='portfolio-header'> {% if application %}
<div class='portfolio-header__name'> {{ StickyCTA(text=application.name, return_link_url=url_for('applications.portfolio_applications', portfolio_id=application.portfolio_id), return_link_text="BACK TO APPLICATIONS") }}
{{ secondary_breadcrumb }} {% endif %}
</div>
</div>
{% endblock %} {% endblock %}
{% block portfolio_content %} {% block portfolio_content %}

View File

@ -51,12 +51,6 @@
<span>{{ "portfolios.applications.app_settings_text" | translate }}</span> <span>{{ "portfolios.applications.app_settings_text" | translate }}</span>
</a> </a>
<div class='separator'></div> <div class='separator'></div>
<a
href="{{ url_for('applications.team', application_id=application.id) }}"
class='icon-link'>
<span>{{ "portfolios.applications.team_text" | translate }} ({{ application.members | length }})</span>
</a>
<div class='separator'></div>
{% set has_environments = 0 < (application.environments|length) %} {% set has_environments = 0 < (application.environments|length) %}
<a class='icon-link triangle-box' v-on:click="toggleSection('{{ section_name }}')" disabled="{{ not has_environments }}"> <a class='icon-link triangle-box' v-on:click="toggleSection('{{ section_name }}')" disabled="{{ not has_environments }}">
<span>Environments ({{ application.environments|length }})</span> <span>Environments ({{ application.environments|length }})</span>

View File

@ -3,16 +3,19 @@
{% from "components/alert.html" import Alert %} {% from "components/alert.html" import Alert %}
{% from "components/delete_confirmation.html" import DeleteConfirmation %} {% from "components/delete_confirmation.html" import DeleteConfirmation %}
{% from "components/icon.html" import Icon %} {% from "components/icon.html" import Icon %}
{% import "fragments/applications/new_member_modal_content.html" as member_steps %}
{% from "components/modal.html" import Modal %} {% from "components/modal.html" import Modal %}
{% from "components/multi_step_modal_form.html" import MultiStepModalForm %}
{% from "components/pagination.html" import Pagination %} {% from "components/pagination.html" import Pagination %}
{% from "components/save_button.html" import SaveButton %} {% from "components/save_button.html" import SaveButton %}
{% from "components/text_input.html" import TextInput %} {% from "components/text_input.html" import TextInput %}
{% from "components/toggle_list.html" import ToggleButton, ToggleSection %}
{% set secondary_breadcrumb = 'portfolios.applications.existing_application_title' | translate({ "application_name": application.name }) %} {% set secondary_breadcrumb = 'portfolios.applications.existing_application_title' | translate({ "application_name": application.name }) %}
{% block application_content %} {% block application_content %}
<div class='subheading'>{{ 'portfolios.applications.settings_heading' | translate }}</div> <div class='subheading'>{{ 'portfolios.applications.settings.name_description' | translate }}</div>
{% if user_can(permissions.EDIT_APPLICATION) %} {% if user_can(permissions.EDIT_APPLICATION) %}
<base-form inline-template> <base-form inline-template>
@ -20,32 +23,16 @@
<div class="panel"> <div class="panel">
<div class="panel__content"> <div class="panel__content">
{{ application_form.csrf_token }} {{ application_form.csrf_token }}
<p>
{{ "fragments.edit_application_form.explain" | translate }}
</p>
<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(application_form.name, optional=False) }} {{ TextInput(application_form.name, optional=False) }}
{{ TextInput(application_form.description, paragraph=True, optional=False) }} {{ TextInput(application_form.description, paragraph=True, optional=False) }}
</div> </div>
<div class="form-col form-col--third">
{% if user_can(permissions.DELETE_APPLICATION) %}
<div class="usa-input">
<input
id="delete-application"
type="button"
v-on:click="openModal('delete-application')"
class='usa-button button-danger-outline'
value="{{ 'portfolios.applications.delete.button' | translate }}"
>
</div>
{% endif %}
</div>
</div> </div>
</div> </div>
<div class="panel__footer"> <div class="panel__footer">
<div class="action-group"> <div class="action-group">
{{ SaveButton('common.save'|translate) }} {{ SaveButton('common.save_changes'|translate) }}
</div> </div>
</div> </div>
</div> </div>
@ -81,26 +68,221 @@
</div> </div>
{% endif %} {% endif %}
<div id="application-environments"> {% if not application.members %}
<div class="accordion-table responsive-table-wrapper panel"> {% set user_can_invite = user_can(permissions.CREATE_APPLICATION_MEMBER) %}
{% if g.matchesPath("application-environments") %}
{% include "fragments/flash.html" %} <div class='empty-state'>
<p class='empty-state__message'>{{ ("portfolios.applications.team_settings.blank_slate.title" | translate) }}</p>
{{ Icon('avatar') }}
{% if not user_can_invite %}
<p class='empty-state__sub-message'>{{ ("portfolios.applications.team_settings.blank_slate.sub_message" | translate) }}</p>
{% endif %} {% endif %}
{% if user_can(permissions.EDIT_ENVIRONMENT) %} {% if user_can_invite %}
{% include "fragments/applications/edit_environments.html" %} {% set new_member_modal_name = "add-app-mem" %}
<a class="usa-button usa-button-big" v-on:click="openModal('{{ new_member_modal_name }}')">
{{ "portfolios.applications.team_settings.blank_slate.action_label" | translate }}
</a>
{{ MultiStepModalForm(
name=new_member_modal_name,
form=new_member_form,
form_action=url_for("applications.create_member", application_id=application.id),
steps=[
member_steps.MemberStepOne(new_member_form),
member_steps.MemberStepTwo(new_member_form, application)
],
) }}
{% endif %}
</div>
{% if user_can(permissions.CREATE_ENVIRONMENT) %} {% else %}
{% include "fragments/applications/add_new_environment.html" %} <div class='subheading'>
{{ 'portfolios.applications.settings.team_members' | translate }}
{% set new_member_modal_name = "add-app-mem" %}
{% if user_can(permissions.CREATE_APPLICATION_MEMBER) %}
<a class="icon-link modal-link icon-link__add" v-on:click="openModal('{{ new_member_modal_name }}')">
{{ Icon("plus") }}
{{ "portfolios.applications.add_member" | translate }}
</a>
{% endif %}
</div>
<section class="member-list application-list" id="application-members">
<div class='responsive-table-wrapper panel'>
{% if g.matchesPath("application-members") %}
{% include "fragments/flash.html" %}
{% endif %} {% endif %}
<table>
<thead>
<tr>
<th>Member</th>
<th>Project Permissions</th>
<th>Environment Access</th>
</tr>
</thead>
<tbody>
{% for member in members %}
<tr>
<td>{{ member.user_name }}</td>
<td>
{% for perm, value in member.permission_sets.items() %}
{{ ("portfolios.applications.members.{}.{}".format(perm, value)) | translate }}<br>
{% endfor %}
</td>
<td>
{% for env in member.environment_roles %}
{{ env.environment_name }}{% if not env == member.environment_roles[-1]%},{% endif %}
{% endfor %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% elif user_can(permissions.VIEW_ENVIRONMENT) %} {% if user_can(permissions.CREATE_APPLICATION_MEMBER) %}
{% include "fragments/applications/read_only_environments.html" %} {% import "fragments/applications/new_member_modal_content.html" as member_steps %}
{{ MultiStepModalForm(
name=new_member_modal_name,
form=new_member_form,
form_action=url_for("applications.create_member", application_id=application.id),
steps=[
member_steps.MemberStepOne(new_member_form),
member_steps.MemberStepTwo(new_member_form, application)
],
) }}
{% endif %} {% endif %}
</section>
{% endif %}
<div class='subheading'>
{{ 'common.resource_names.environments' | translate }}
{% if user_can(permissions.CREATE_ENVIRONMENT) %}
{% include "fragments/applications/add_new_environment.html" %}
{% endif %}
</div>
<div class="panel">
{% if g.matchesPath("application-environments") %}
{% include "fragments/flash.html" %}
{% endif %}
<div class="panel__content">
<div class="accordion-table accordion-table-list">
<ul class="accordion-table__items">
{% for env in environments_obj %}
{% set edit_form = env['edit_form'] %}
<toggler inline-template>
<li class="accordion-table__item">
<div class="accordion-table__item-content form-row">
<div class="form-col form-col--two-thirds">
<div class="environment-list__item">
<span>
{{ env['name'] }}
</span>
<span class="icon-link">
{% set edit_environment_button %}
{{ Icon('edit') }}
{% endset %}
{{
ToggleButton(
open_html=edit_environment_button,
close_html=edit_environment_button,
section_name="edit"
)
}}
</span>
<span class="accordion-table__item__toggler icon-link">
{% set members_button = "portfolios.applications.member_count" | translate({'count': env['member_count']}) %}
{{
ToggleButton(
open_html=members_button,
close_html=members_button,
section_name="members"
)
}}
</span>
</div>
</div>
<div class="form-col form-col--third">
<a href='{{ url_for("applications.access_environment", environment_id=env.id)}}' target='_blank' rel='noopener noreferrer' class='application-list-item__environment__csp_link icon-link'>
<span>{{ "portfolios.applications.csp_link" | translate }} {{ Icon('link', classes="icon--tiny") }}</span>
</a>
</div>
</div>
{% call ToggleSection(section_name="members") %}
<ul>
{% for member in env['members'] %}
<li class="accordion-table__item__expanded">
{{ member }}
</li>
{% endfor %}
</ul>
{% endcall %}
{% call ToggleSection(section_name="edit") %}
<ul>
<li class="accordion-table__item__expanded">
<form action="{{ url_for('applications.update_environment', environment_id=env['id']) }}" method="post" v-on:submit="handleSubmit">
{{ edit_form.csrf_token }}
{{ TextInput(edit_form.name, validation='requiredField') }}
{{
SaveButton(
text=("common.save" | translate)
)
}}
</form>
</li>
</ul>
{% endcall %}
</li>
</toggler>
{% endfor %}
</ul>
</div>
</div> </div>
</div> </div>
<hr>
{% if user_can(permissions.DELETE_APPLICATION) %} {% 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 %}
<div class='subheading'>
{{ "portfolios.applications.delete.subheading" | translate }}
</div>
<div class="panel">
<div class="panel__content">
<div class="form-row">
<div class="form-col form-col--two-thirds">
{{ "portfolios.applications.delete.panel_text" | translate({"name": application.name, "env_count": env_count , "pluralized_env": pluralized_env}) | 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 button-danger-outline'
value="{{ 'portfolios.applications.delete.button' | translate }}"
>
</div>
</div>
</div>
</div>
</div>
{% call Modal(name="delete-application") %} {% call Modal(name="delete-application") %}
<h1>{{ "portfolios.applications.delete.header" | translate }}</h1> <h1>{{ "portfolios.applications.delete.header" | translate }}</h1>

View File

@ -1,154 +0,0 @@
{% extends "portfolios/applications/base.html" %}
{% from "components/icon.html" import Icon %}
{% from "components/multi_step_modal_form.html" import MultiStepModalForm %}
{% from 'components/save_button.html' import SaveButton %}
{% import "fragments/applications/new_member_modal_content.html" as member_steps %}
{% from "components/alert.html" import Alert %}
{% from "components/delete_confirmation.html" import DeleteConfirmation %}
{% from "components/modal.html" import Modal %}
{% set secondary_breadcrumb = 'portfolios.applications.team_settings.title' | translate({ "application_name": application.name }) %}
{% block application_content %}
{% if not application.members %}
{% set user_can_invite = user_can(permissions.CREATE_APPLICATION_MEMBER) %}
<div class='empty-state'>
<p class='empty-state__message'>{{ ("portfolios.applications.team_settings.blank_slate.title" | translate) }}</p>
{{ Icon('avatar') }}
{% if not user_can_invite %}
<p class='empty-state__sub-message'>{{ ("portfolios.applications.team_settings.blank_slate.sub_message" | translate) }}</p>
{% endif %}
{% if user_can_invite %}
{% set new_member_modal_name = "add-app-mem" %}
<a class="usa-button usa-button-big" v-on:click="openModal('{{ new_member_modal_name }}')">
{{ "portfolios.applications.team_settings.blank_slate.action_label" | translate }}
</a>
{{ MultiStepModalForm(
name=new_member_modal_name,
form=new_member_form,
form_action=url_for("applications.create_member", application_id=application.id),
steps=[
member_steps.MemberStepOne(new_member_form),
member_steps.MemberStepTwo(new_member_form, application)
],
) }}
{% endif %}
</div>
{% else %}
<div class='subheading'>
{{ 'portfolios.applications.team_settings.subheading' | translate }}
</div>
<section class="member-list application-list" id="application-members">
<base-form inline-template>
<form method='POST' id="team" action='{{ url_for("applications.update_team", application_id=application.id) }}' autocomplete="off" enctype="multipart/form-data">
<div class='responsive-table-wrapper panel'>
{% if g.matchesPath("application-members") %}
{% include "fragments/flash.html" %}
{% endif %}
<header>
<div class="responsive-table-wrapper__header">
<div class="responsive-table-wrapper__title row">
<div class="h3">
{{ "portfolios.applications.team_settings.section.title" | translate({ "application_name": application.name }) }}
<p class="member-list__subhead">Members ({{ team_form.members | length }})</p>
</div>
</div>
</div>
</header>
<div class="accordion-table accordion-table-list">
<div class="accordion-table__head row">
<div class="col col--grow">
{{ "common.name" | translate }}
</div>
<div class="col col--grow">
{{ "portfolios.applications.team_settings.section.table.team_management" | translate }}
</div>
<div class="col col--grow">
{{ "portfolios.applications.team_settings.section.table.environment_management" | translate }}
</div>
<div class="col col--grow">
{{ "portfolios.applications.team_settings.section.table.delete_access" | translate }}
</div>
<div class="col col--grow">
&nbsp;
</div>
</div>
<ul class="accordion-table__items">
{% if user_can(permissions.EDIT_APPLICATION_MEMBER) %}
{% include "fragments/applications/edit_team.html" %}
{% elif user_can(permissions.VIEW_APPLICATION_MEMBER) %}
{% include "fragments/applications/read_only_team.html" %}
{% endif %}
</ul>
</div>
<div class="panel__footer">
<div class="action-group save">
{% if user_can(permissions.EDIT_APPLICATION_MEMBER) %}
{{ SaveButton(text=('common.save' | translate), element="input", form="team") }}
{% endif %}
{% set new_member_modal_name = "add-app-mem" %}
{% if user_can(permissions.CREATE_APPLICATION_MEMBER) %}
<a class="icon-link modal-link" v-on:click="openModal('{{ new_member_modal_name }}')">
{{ "portfolios.admin.add_new_member" | translate }}
{{ Icon("plus") }}
</a>
{% endif %}
</div>
</div>
</div>
</form>
</base-form>
{% if user_can(permissions.DELETE_APPLICATION_MEMBER) %}
{% for member_form in team_form.members %}
{% set delete_modal_id = "delete-user-{}".format(member_form.id) %}
{% call Modal(name=delete_modal_id) %}
<h1>
{{ "portfolios.applications.remove_member.header" | translate }}
</h1>
{{
Alert(
title=("components.modal.destructive_title" | translate),
message=("portfolios.applications.remove_member.alert.message" | translate({"user_name": member_form.user_name.data})),
level="warning"
)
}}
{{
DeleteConfirmation(
modal_id=delete_modal_id,
delete_text=('portfolios.applications.remove_member.button' | translate),
delete_action=url_for('applications.remove_member', application_id=application.id, application_role_id=member_form.data.role_id),
form=member_form
)
}}
{% endcall %}
{% endfor %}
{% endif %}
{% if user_can(permissions.CREATE_APPLICATION_MEMBER) %}
{% import "fragments/applications/new_member_modal_content.html" as member_steps %}
{{ MultiStepModalForm(
name=new_member_modal_name,
form=new_member_form,
form_action=url_for("applications.create_member", application_id=application.id),
steps=[
member_steps.MemberStepOne(new_member_form),
member_steps.MemberStepTwo(new_member_form, application)
],
) }}
{% endif %}
</section>
{% endif %}
{% endblock %}

View File

@ -1,5 +1,7 @@
import pytest import pytest
import uuid
from flask import url_for, get_flashed_messages from flask import url_for, get_flashed_messages
from unittest.mock import Mock
from tests.factories import * from tests.factories import *
@ -424,3 +426,122 @@ def test_delete_environment(client, user_session):
assert environment.name in message["message"] assert environment.name in message["message"]
# deletes environment # deletes environment
assert len(application.environments) == 0 assert len(application.environments) == 0
def test_create_member(monkeypatch, client, user_session, session):
job_mock = Mock()
monkeypatch.setattr("atst.jobs.send_mail.delay", job_mock)
user = UserFactory.create()
application = ApplicationFactory.create(
environments=[{"name": "Naboo"}, {"name": "Endor"}]
)
env = application.environments[0]
env_1 = application.environments[1]
user_session(application.portfolio.owner)
response = client.post(
url_for("applications.create_member", application_id=application.id),
data={
"user_data-first_name": user.first_name,
"user_data-last_name": user.last_name,
"user_data-dod_id": user.dod_id,
"user_data-email": user.email,
"environment_roles-0-environment_id": env.id,
"environment_roles-0-role": "Basic Access",
"environment_roles-0-environment_name": env.name,
"environment_roles-1-environment_id": env_1.id,
"environment_roles-1-role": NO_ACCESS,
"environment_roles-1-environment_name": env_1.name,
"permission_sets-perms_env_mgmt": True,
"permission_sets-perms_team_mgmt": True,
"permission_sets-perms_del_env": True,
},
)
assert response.status_code == 302
expected_url = url_for(
"applications.settings",
application_id=application.id,
fragment="application-members",
_anchor="application-members",
_external=True,
)
assert response.location == expected_url
assert len(application.roles) == 1
environment_roles = application.roles[0].environment_roles
assert len(environment_roles) == 1
assert environment_roles[0].environment == env
invitation = (
session.query(ApplicationInvitation).filter_by(dod_id=user.dod_id).one()
)
assert invitation.role.application == application
assert job_mock.called
def test_remove_member_success(client, user_session):
user = UserFactory.create()
application = ApplicationFactory.create()
application_role = ApplicationRoleFactory.create(application=application, user=user)
user_session(application.portfolio.owner)
response = client.post(
url_for(
"applications.remove_member",
application_id=application.id,
application_role_id=application_role.id,
)
)
assert response.status_code == 302
assert response.location == url_for(
"applications.settings",
_anchor="application-members",
_external=True,
application_id=application.id,
fragment="application-members",
)
def test_remove_new_member_success(client, user_session):
application = ApplicationFactory.create()
application_role = ApplicationRoleFactory.create(application=application, user=None)
user_session(application.portfolio.owner)
response = client.post(
url_for(
"applications.remove_member",
application_id=application.id,
application_role_id=application_role.id,
)
)
assert response.status_code == 302
assert response.location == url_for(
"applications.settings",
_anchor="application-members",
_external=True,
application_id=application.id,
fragment="application-members",
)
def test_remove_member_failure(client, user_session):
user = UserFactory.create()
application = ApplicationFactory.create()
user_session(application.portfolio.owner)
response = client.post(
url_for(
"applications.remove_member",
application_id=application.id,
application_role_id=uuid.uuid4(),
)
)
assert response.status_code == 404

View File

@ -1,269 +0,0 @@
import uuid
from unittest.mock import Mock
from flask import url_for
from atst.domain.permission_sets import PermissionSets
from atst.models import CSPRole
from atst.forms.data import ENV_ROLE_NO_ACCESS as NO_ACCESS
from tests.factories import *
def test_application_team(client, user_session):
portfolio = PortfolioFactory.create()
application = ApplicationFactory.create(portfolio=portfolio)
user_session(portfolio.owner)
response = client.get(url_for("applications.team", application_id=application.id))
assert response.status_code == 200
def test_update_team_permissions(client, user_session):
application = ApplicationFactory.create()
owner = application.portfolio.owner
app_role = ApplicationRoleFactory.create(
application=application, permission_sets=[]
)
user_session(owner)
response = client.post(
url_for("applications.update_team", application_id=application.id),
data={
"members-0-role_id": app_role.id,
"members-0-permission_sets-perms_team_mgmt": PermissionSets.EDIT_APPLICATION_TEAM,
"members-0-permission_sets-perms_env_mgmt": PermissionSets.EDIT_APPLICATION_ENVIRONMENTS,
"members-0-permission_sets-perms_del_env": PermissionSets.DELETE_APPLICATION_ENVIRONMENTS,
},
)
assert response.status_code == 302
actual_perms_names = [perm.name for perm in app_role.permission_sets]
expected_perms_names = [
PermissionSets.VIEW_APPLICATION,
PermissionSets.EDIT_APPLICATION_TEAM,
PermissionSets.EDIT_APPLICATION_ENVIRONMENTS,
PermissionSets.DELETE_APPLICATION_ENVIRONMENTS,
]
assert expected_perms_names == actual_perms_names
def test_update_team_with_bad_permission_sets(client, user_session):
application = ApplicationFactory.create()
owner = application.portfolio.owner
app_role = ApplicationRoleFactory.create(
application=application, permission_sets=[]
)
permission_sets = app_role.permission_sets
user_session(owner)
response = client.post(
url_for("applications.update_team", application_id=application.id),
data={
"members-0-role_id": app_role.id,
"members-0-permission_sets-perms_team_mgmt": PermissionSets.EDIT_APPLICATION_TEAM,
"members-0-permission_sets-perms_env_mgmt": "some random string",
},
)
assert response.status_code == 400
assert app_role.permission_sets == permission_sets
def test_update_team_with_non_app_user(client, user_session):
application = ApplicationFactory.create()
owner = application.portfolio.owner
user_session(owner)
response = client.post(
url_for("applications.update_team", application_id=application.id),
data={
"members-0-role_id": str(uuid.uuid4()),
"members-0-permission_sets-perms_team_mgmt": PermissionSets.EDIT_APPLICATION_TEAM,
"members-0-permission_sets-perms_env_mgmt": PermissionSets.EDIT_APPLICATION_ENVIRONMENTS,
"members-0-permission_sets-perms_del_env": PermissionSets.DELETE_APPLICATION_ENVIRONMENTS,
},
)
assert response.status_code == 404
def test_update_team_environment_roles(client, user_session):
application = ApplicationFactory.create()
owner = application.portfolio.owner
app_role = ApplicationRoleFactory.create(
application=application, permission_sets=[]
)
environment = EnvironmentFactory.create(application=application)
env_role = EnvironmentRoleFactory.create(
application_role=app_role,
environment=environment,
role=CSPRole.NETWORK_ADMIN.value,
)
user_session(owner)
response = client.post(
url_for("applications.update_team", application_id=application.id),
data={
"members-0-role_id": app_role.id,
"members-0-permission_sets-perms_team_mgmt": PermissionSets.EDIT_APPLICATION_TEAM,
"members-0-permission_sets-perms_env_mgmt": PermissionSets.EDIT_APPLICATION_ENVIRONMENTS,
"members-0-permission_sets-perms_del_env": PermissionSets.DELETE_APPLICATION_ENVIRONMENTS,
"members-0-environment_roles-0-environment_id": environment.id,
"members-0-environment_roles-0-role": CSPRole.TECHNICAL_READ.value,
},
)
assert response.status_code == 302
assert env_role.role == CSPRole.TECHNICAL_READ.value
def test_update_team_revoke_environment_access(client, user_session, db, session):
application = ApplicationFactory.create()
owner = application.portfolio.owner
user = UserFactory.create()
app_role = ApplicationRoleFactory.create(
application=application, user=user, permission_sets=[]
)
environment = EnvironmentFactory.create(application=application)
env_role = EnvironmentRoleFactory.create(
application_role=app_role,
environment=environment,
role=CSPRole.BASIC_ACCESS.value,
)
assert user in environment.users
user_session(owner)
response = client.post(
url_for("applications.update_team", application_id=application.id),
data={
"members-0-role_id": app_role.id,
"members-0-permission_sets-perms_team_mgmt": PermissionSets.EDIT_APPLICATION_TEAM,
"members-0-permission_sets-perms_env_mgmt": PermissionSets.EDIT_APPLICATION_ENVIRONMENTS,
"members-0-permission_sets-perms_del_env": PermissionSets.DELETE_APPLICATION_ENVIRONMENTS,
"members-0-environment_roles-0-environment_id": environment.id,
"members-0-environment_roles-0-role": NO_ACCESS,
},
)
assert response.status_code == 302
env_role_exists = db.exists().where(EnvironmentRole.id == env_role.id)
assert not session.query(env_role_exists).scalar()
assert user not in environment.users
def test_create_member(monkeypatch, client, user_session, session):
job_mock = Mock()
monkeypatch.setattr("atst.jobs.send_mail.delay", job_mock)
user = UserFactory.create()
application = ApplicationFactory.create(
environments=[{"name": "Naboo"}, {"name": "Endor"}]
)
env = application.environments[0]
env_1 = application.environments[1]
user_session(application.portfolio.owner)
response = client.post(
url_for("applications.create_member", application_id=application.id),
data={
"user_data-first_name": user.first_name,
"user_data-last_name": user.last_name,
"user_data-dod_id": user.dod_id,
"user_data-email": user.email,
"environment_roles-0-environment_id": env.id,
"environment_roles-0-role": "Basic Access",
"environment_roles-0-environment_name": env.name,
"environment_roles-1-environment_id": env_1.id,
"environment_roles-1-role": NO_ACCESS,
"environment_roles-1-environment_name": env_1.name,
"permission_sets-perms_env_mgmt": True,
"permission_sets-perms_team_mgmt": True,
"permission_sets-perms_del_env": True,
},
)
assert response.status_code == 302
expected_url = url_for(
"applications.team",
application_id=application.id,
fragment="application-members",
_anchor="application-members",
_external=True,
)
assert response.location == expected_url
assert len(application.roles) == 1
environment_roles = application.roles[0].environment_roles
assert len(environment_roles) == 1
assert environment_roles[0].environment == env
invitation = (
session.query(ApplicationInvitation).filter_by(dod_id=user.dod_id).one()
)
assert invitation.role.application == application
assert job_mock.called
def test_remove_member_success(client, user_session):
user = UserFactory.create()
application = ApplicationFactory.create()
application_role = ApplicationRoleFactory.create(application=application, user=user)
user_session(application.portfolio.owner)
response = client.post(
url_for(
"applications.remove_member",
application_id=application.id,
application_role_id=application_role.id,
)
)
assert response.status_code == 302
assert response.location == url_for(
"applications.team",
_anchor="application-members",
_external=True,
application_id=application.id,
fragment="application-members",
)
def test_remove_new_member_success(client, user_session):
application = ApplicationFactory.create()
application_role = ApplicationRoleFactory.create(application=application, user=None)
user_session(application.portfolio.owner)
response = client.post(
url_for(
"applications.remove_member",
application_id=application.id,
application_role_id=application_role.id,
)
)
assert response.status_code == 302
assert response.location == url_for(
"applications.team",
_anchor="application-members",
_external=True,
application_id=application.id,
fragment="application-members",
)
def test_remove_member_failure(client, user_session):
user = UserFactory.create()
application = ApplicationFactory.create()
user_session(application.portfolio.owner)
response = client.post(
url_for(
"applications.remove_member",
application_id=application.id,
application_role_id=uuid.uuid4(),
)
)
assert response.status_code == 404

View File

@ -585,20 +585,6 @@ def test_task_orders_new_post_routes(post_url_assert_status):
post_url_assert_status(rando, url, 404, data=data) post_url_assert_status(rando, url, 404, data=data)
def test_applications_application_team_access(get_url_assert_status):
ccpo = UserFactory.create_ccpo()
rando = UserFactory.create()
portfolio = PortfolioFactory.create()
application = ApplicationFactory.create(portfolio=portfolio)
url = url_for("applications.team", application_id=application.id)
get_url_assert_status(ccpo, url, 200)
get_url_assert_status(portfolio.owner, url, 200)
get_url_assert_status(rando, url, 404)
def test_portfolio_delete_access(post_url_assert_status): def test_portfolio_delete_access(post_url_assert_status):
rando = UserFactory.create() rando = UserFactory.create()
owner = UserFactory.create() owner = UserFactory.create()

View File

@ -59,6 +59,7 @@ common:
'no': 'No' 'no': 'No'
response_label: Response required response_label: Response required
save: Save save: Save
save_changes: Save Changes
undo: Undo undo: Undo
view: View view: View
resource_names: resource_names:
@ -283,13 +284,15 @@ portfolios:
portfolio_name: Portfolio name portfolio_name: Portfolio name
applications: applications:
add_application_text: Add a new application add_application_text: Add a new application
add_environment: Add new environment add_environment: Create an Environment
add_member: Add a New Team Member
add_another_environment: Add another environment add_another_environment: Add another environment
app_settings_text: App settings app_settings_text: App settings
create_button_text: Create create_button_text: Create
create_new_env: Create a new environment. create_new_env: Create a new environment.
create_new_env_info: Creating an environment gives you access to the Cloud Service Provider. This environment will function within the constraints of the task order, and any costs will be billed against the portfolio. create_new_env_info: Creating an environment gives you access to the Cloud Service Provider. This environment will function within the constraints of the task order, and any costs will be billed against the portfolio.
csp_console_text: CSP console csp_console_text: CSP console
csp_link: Cloud Service Provider Link
remove_member: remove_member:
alert: alert:
message: '{user_name} will no longer be able to access this application or any of its environments' message: '{user_name} will no longer be able to access this application or any of its environments'
@ -300,6 +303,8 @@ portfolios:
message: You will lose access to this application and all environments will be removed from the CSP. Your reporting and activity will still be accessible. message: You will lose access to this application and all environments will be removed from the CSP. Your reporting and activity will still be accessible.
button: Delete application button: Delete application
header: Are you sure you want to delete this application? header: Are you sure you want to delete this application?
panel_text: 'Deleting {name} will delete this application along with all {env_count} {pluralized_env}. <strong>This cannot be undone.</strong>'
subheading: Delete Application
enter_env_name: "Enter environment name:" enter_env_name: "Enter environment name:"
environments: environments:
name: Name name: Name
@ -309,8 +314,12 @@ portfolios:
environments_description: Each environment created within an application is logically separated from one another for easier management and security. environments_description: Each environment created within an application is logically separated from one another for easier management and security.
environments_heading: Application environments environments_heading: Application environments
existing_application_title: '{application_name} Application Settings' existing_application_title: '{application_name} Application Settings'
member_count: '{count} members'
new_application_title: New Application new_application_title: New Application
settings_heading: Application Settings settings_heading: Application Settings
settings:
name_description: Name and Description
team_members: Team Members
team_settings: team_settings:
blank_slate: blank_slate:
action_label: Invite a new team member action_label: Invite a new team member
@ -323,7 +332,6 @@ portfolios:
environment_management: Environment Management environment_management: Environment Management
team_management: Team Management team_management: Team Management
title: '{application_name} Team' title: '{application_name} Team'
subheading: Team Settings
title: '{application_name} Team Settings' title: '{application_name} Team Settings'
add_to_environment: Add to existing environment add_to_environment: Add to existing environment
team_text: Team team_text: Team
@ -335,6 +343,15 @@ portfolios:
manage_envs: 'Allow member to <strong>add</strong> and <strong>rename environments</strong> within the application.' manage_envs: 'Allow member to <strong>add</strong> and <strong>rename environments</strong> within the application.'
delete_envs: 'Allow member to <strong>delete environments</strong> within the application.' delete_envs: 'Allow member to <strong>delete environments</strong> within the application.'
manage_team: 'Allow member to <strong>add, update,</strong> and <strong>remove members</strong> from the application team.' manage_team: 'Allow member to <strong>add, update,</strong> and <strong>remove members</strong> from the application team.'
perms_team_mgmt:
view_application: View Team
edit_application_team: Edit Team
perms_env_mgmt:
view_application: View Environments
edit_application_environments: Edit Environments
perms_del_env:
view_application: ""
delete_application_environments: Delete Application
index: index:
empty: empty:
start_button: Start a new JEDI portfolio start_button: Start a new JEDI portfolio