Merge pull request #822 from dod-ccpo/app-members-edit

App members edit
This commit is contained in:
dandds 2019-05-17 14:08:06 -04:00 committed by GitHub
commit 01a935f257
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 363 additions and 117 deletions

View File

@ -2,18 +2,18 @@ from flask_wtf import FlaskForm
from wtforms.fields import FieldList, FormField, HiddenField, RadioField, StringField from wtforms.fields import FieldList, FormField, HiddenField, RadioField, StringField
from .forms import BaseForm from .forms import BaseForm
from .data import ENV_ROLES from .data import ENV_ROLES, ENV_ROLE_NO_ACCESS as NO_ACCESS
class MemberForm(FlaskForm): class MemberForm(FlaskForm):
user_id = HiddenField() user_id = HiddenField()
user_name = StringField() user_name = StringField()
role_name = RadioField(choices=ENV_ROLES, default="no_access") role_name = RadioField(choices=ENV_ROLES, default=NO_ACCESS)
@property @property
def data(self): def data(self):
_data = super().data _data = super().data
if "role_name" in _data and _data["role_name"] == "no_access": if "role_name" in _data and _data["role_name"] == NO_ACCESS:
_data["role_name"] = None _data["role_name"] = None
return _data return _data

View File

@ -217,6 +217,7 @@ REQUIRED_DISTRIBUTIONS = [
("other", "Other as necessary"), ("other", "Other as necessary"),
] ]
ENV_ROLE_NO_ACCESS = "No Access"
ENV_ROLES = [(role.value, role.value) for role in CSPRole] + [ ENV_ROLES = [(role.value, role.value) for role in CSPRole] + [
("no_access", "No access") (ENV_ROLE_NO_ACCESS, "No access")
] ]

View File

@ -1,14 +1,31 @@
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms.fields import FormField, FieldList, HiddenField, StringField from wtforms.fields import FormField, FieldList, HiddenField, RadioField, StringField
from wtforms.validators import Required from wtforms.validators import Required
from .application_member import EnvironmentForm from .application_member import EnvironmentForm as BaseEnvironmentForm
from .data import ENV_ROLES, ENV_ROLE_NO_ACCESS as NO_ACCESS
from .forms import BaseForm from .forms import BaseForm
from atst.forms.fields import SelectField from atst.forms.fields import SelectField
from atst.domain.permission_sets import PermissionSets from atst.domain.permission_sets import PermissionSets
from atst.utils.localization import translate from atst.utils.localization import translate
class EnvironmentForm(BaseEnvironmentForm):
role = RadioField(
"Role",
choices=ENV_ROLES,
default=None,
filters=[lambda x: None if x == "None" else x],
)
@property
def data(self):
_data = super().data
if "role" in _data and _data["role"] == NO_ACCESS:
_data["role"] = None
return _data
class PermissionsForm(FlaskForm): class PermissionsForm(FlaskForm):
perms_team_mgmt = SelectField( perms_team_mgmt = SelectField(
translate("portfolios.applications.members.new.manage_team"), translate("portfolios.applications.members.new.manage_team"),

View File

@ -5,6 +5,7 @@ from atst.domain.environments import Environments
from atst.domain.applications import Applications from atst.domain.applications import Applications
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.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.domain.exceptions import NotFoundError from atst.domain.exceptions import NotFoundError
@ -46,10 +47,10 @@ def sort_env_users_by_role(env):
users_list = [] users_list = []
no_access_users = env.application.users - env.users no_access_users = env.application.users - env.users
no_access_list = [ no_access_list = [
{"user_id": str(user.id), "user_name": user.full_name, "role_name": "no_access"} {"user_id": str(user.id), "user_name": user.full_name, "role_name": NO_ACCESS}
for user in no_access_users for user in no_access_users
] ]
users_list.append({"role": "no_access", "members": no_access_list}) users_list.append({"role": NO_ACCESS, "members": no_access_list})
for role in CSPRole: for role in CSPRole:
users_list.append( users_list.append(

View File

@ -6,6 +6,7 @@ from atst.domain.applications import Applications
from atst.domain.application_roles import ApplicationRoles from atst.domain.application_roles import ApplicationRoles
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.domain.environment_roles import EnvironmentRoles from atst.domain.environment_roles import EnvironmentRoles
from atst.domain.environments import Environments
from atst.domain.exceptions import AlreadyExistsError from atst.domain.exceptions import AlreadyExistsError
from atst.domain.permission_sets import PermissionSets from atst.domain.permission_sets import PermissionSets
from atst.domain.users import Users from atst.domain.users import Users
@ -97,15 +98,25 @@ def update_team(application_id):
form = TeamForm(http_request.form) form = TeamForm(http_request.form)
if form.validate(): if form.validate():
for member in form.members: for member_form in form.members:
app_role = ApplicationRoles.get(member.data["user_id"], application.id) app_role = ApplicationRoles.get(member_form.user_id.data, application.id)
new_perms = [ new_perms = [
perm perm
for perm in member.data["permission_sets"] for perm in member_form.data["permission_sets"]
if perm != PermissionSets.VIEW_APPLICATION if perm != PermissionSets.VIEW_APPLICATION
] ]
ApplicationRoles.update_permission_sets(app_role, new_perms) ApplicationRoles.update_permission_sets(app_role, new_perms)
flash("updated_application_members_permissions")
for environment_role_form in member_form.environment_roles:
user = Users.get(member_form.user_id.data)
environment = Environments.get(
environment_role_form.environment_id.data
)
Environments.update_env_role(
environment, user, environment_role_form.data.get("role")
)
flash("updated_application_team_settings", application_name=application.name)
return redirect( return redirect(
url_for( url_for(

View File

@ -186,10 +186,10 @@ MESSAGES = {
""", """,
"category": "success", "category": "success",
}, },
"updated_application_members_permissions": { "updated_application_team_settings": {
"title_template": translate("flash.success"), "title_template": translate("flash.success"),
"message_template": """ "message_template": """
<p>{{ "flash.updated_application_members_permissions" | translate }}</p> <p>{{ "flash.updated_application_team_settings" | translate({"application_name": application_name}) }}</p>
""", """,
"category": "success", "category": "success",
}, },

View File

@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils' import { shallowMount } from '@vue/test-utils'
import EditEnvironmentRole from '../forms/edit_environment_role' import { NO_ACCESS, EditEnvironmentRole } from '../forms/edit_environment_role'
describe('EditEnvironmentRole', () => { describe('EditEnvironmentRole', () => {
var initialRoleCategories, wrapper var initialRoleCategories, wrapper
@ -7,7 +7,7 @@ describe('EditEnvironmentRole', () => {
beforeEach(() => { beforeEach(() => {
initialRoleCategories = [ initialRoleCategories = [
{ {
role: 'no_access', role: NO_ACCESS,
members: [ members: [
{ role_name: null, user_id: '123' }, { role_name: null, user_id: '123' },
{ role_name: null, user_id: '456' }, { role_name: null, user_id: '456' },
@ -41,10 +41,10 @@ describe('EditEnvironmentRole', () => {
it('removes null roles to no_access', () => { it('removes null roles to no_access', () => {
let roles = wrapper.vm.sanitizeValues([ let roles = wrapper.vm.sanitizeValues([
{ role: 'no_access', members: [{ role_name: null }] }, { role: NO_ACCESS, members: [{ role_name: null }] },
]) ])
expect(roles).toEqual([ expect(roles).toEqual([
{ role: 'no_access', members: [{ role_name: 'no_access' }] }, { role: NO_ACCESS, members: [{ role_name: NO_ACCESS }] },
]) ])
}) })

View File

@ -0,0 +1,36 @@
import optionsinput from './options_input'
import { emitEvent } from '../lib/emitters'
export default {
name: 'environment-role',
components: {
optionsinput,
},
props: {
initialRole: String,
},
data: function() {
return {
role: this.initialRole,
expanded: false,
}
},
methods: {
toggle: function() {
this.expanded = !this.expanded
},
radioChange: function(e) {
this.role = e.target.value
emitEvent('field-change', this, {
value: e.target.value,
valid: true,
name: this.name,
watch: true,
})
},
},
}

View File

@ -1,7 +1,7 @@
import FormMixin from '../../mixins/form' import FormMixin from '../../mixins/form'
import Modal from '../../mixins/modal' import Modal from '../../mixins/modal'
import toggler from '../toggler' import toggler from '../toggler'
import EditEnvironmentRole from './edit_environment_role' import { EditEnvironmentRole } from './edit_environment_role'
export default { export default {
name: 'edit-application-roles', name: 'edit-application-roles',

View File

@ -5,7 +5,9 @@ import Modal from '../../mixins/modal'
// https://github.com/dod-ccpo/atst/pull/799/files#r282240663 // https://github.com/dod-ccpo/atst/pull/799/files#r282240663
// May also want to reconsider the data structure by storing the roles and members separately // May also want to reconsider the data structure by storing the roles and members separately
export default { export const NO_ACCESS = 'No Access'
export const EditEnvironmentRole = {
name: 'edit-environment-role', name: 'edit-environment-role',
mixins: [FormMixin], mixins: [FormMixin],
@ -26,7 +28,7 @@ export default {
roles.forEach(role => { roles.forEach(role => {
role.members.forEach(member => { role.members.forEach(member => {
if (member.role_name === null) { if (member.role_name === null) {
member.role_name = 'no_access' member.role_name = NO_ACCESS
} }
}) })
}) })
@ -34,7 +36,7 @@ export default {
}, },
checkNoAccess: function(role) { checkNoAccess: function(role) {
return role === 'no_access' return role === NO_ACCESS
}, },
toggleSection: function(sectionName) { toggleSection: function(sectionName) {

View File

@ -1,7 +1,8 @@
import editEnvironmentRole from './forms/edit_environment_role' import { EditEnvironmentRole } from './forms/edit_environment_role'
import FormMixin from '../mixins/form' import FormMixin from '../mixins/form'
import optionsinput from './options_input' import optionsinput from './options_input'
import textinput from './text_input' import textinput from './text_input'
import EnvironmentRole from './environment_role'
export default { export default {
name: 'toggler', name: 'toggler',
@ -16,10 +17,12 @@ export default {
}, },
components: { components: {
editEnvironmentRole, EditEnvironmentRole,
optionsinput, optionsinput,
textinput, textinput,
optionsinput, optionsinput,
EnvironmentRole,
toggler: this,
}, },
data: function() { data: function() {

View File

@ -18,7 +18,7 @@ import poc from './components/forms/poc'
import oversight from './components/forms/oversight' import oversight from './components/forms/oversight'
import toggler from './components/toggler' import toggler from './components/toggler'
import NewApplication from './components/forms/new_application' import NewApplication from './components/forms/new_application'
import EditEnvironmentRole from './components/forms/edit_environment_role' import { EditEnvironmentRole } from './components/forms/edit_environment_role'
import EditApplicationRoles from './components/forms/edit_application_roles' import EditApplicationRoles from './components/forms/edit_application_roles'
import MultiStepModalForm from './components/forms/multi_step_modal_form' import MultiStepModalForm from './components/forms/multi_step_modal_form'
import funding from './components/forms/funding' import funding from './components/forms/funding'
@ -39,6 +39,7 @@ import KoReview from './components/forms/ko_review'
import BaseForm from './components/forms/base_form' import BaseForm from './components/forms/base_form'
import DeleteConfirmation from './components/delete_confirmation' import DeleteConfirmation from './components/delete_confirmation'
import NewEnvironment from './components/forms/new_environment' import NewEnvironment from './components/forms/new_environment'
import EnvironmentRole from './components/environment_role'
Vue.config.productionTip = false Vue.config.productionTip = false
@ -80,6 +81,7 @@ const app = new Vue({
DeleteConfirmation, DeleteConfirmation,
nestedcheckboxinput, nestedcheckboxinput,
NewEnvironment, NewEnvironment,
EnvironmentRole,
}, },
mounted: function() { mounted: function() {

View File

@ -91,6 +91,30 @@
border-bottom: 1px solid $color-gray-lighter; border-bottom: 1px solid $color-gray-lighter;
} }
} }
.accordion-table__item__action-group {
padding: 1rem 3.2rem;
background-color: $color-gray-lightest;
button,
a {
margin: 0;
font-size: $small-font-size;
}
.icon-link {
padding-top: 0.5rem;
float: none;
}
> *:first-child {
padding-left: 0;
}
> *:last-child {
float: right;
}
}
} }
.accordion-table__item__toggler { .accordion-table__item__toggler {
@ -100,6 +124,10 @@
color: $color-blue; color: $color-blue;
padding: $gap; padding: $gap;
> span {
margin-left: auto;
}
.icon { .icon {
@include icon-size(12); @include icon-size(12);
@ -147,6 +175,17 @@
.accordion-table__item__expanded_first { .accordion-table__item__expanded_first {
float: left; float: left;
} }
.accordion-table__item__expanded-role {
.icon-link {
padding: 0;
vertical-align: text-top;
.icon {
margin: 0 0 0 0.25rem;
}
}
}
} }
} }
} }

View File

@ -1,7 +1,6 @@
.portfolio-panel-container { .portfolio-panel-container {
@include media($large-screen) { @include media($large-screen) {
@include grid-row; @include grid-row;
min-height: 500px; min-height: 500px;
} }
@ -38,7 +37,6 @@
&.icon-link--disabled { &.icon-link--disabled {
color: $color-gray-dark; color: $color-gray-dark;
opacity: 1; opacity: 1;
.icon { .icon {
@include icon-color($color-gray-dark); @include icon-color($color-gray-dark);
} }
@ -53,7 +51,6 @@
.icon-link { .icon-link {
color: $color-gray-medium; color: $color-gray-medium;
pointer-events: none; pointer-events: none;
&.icon-link--disabled { &.icon-link--disabled {
opacity: 1; opacity: 1;
} }
@ -63,7 +60,6 @@
.portfolio-header { .portfolio-header {
flex-direction: column; flex-direction: column;
@include media($small-screen) { @include media($small-screen) {
flex-direction: row; flex-direction: row;
} }
@ -103,10 +99,8 @@
.icon-link { .icon-link {
padding: 0.8rem 1.2rem; padding: 0.8rem 1.2rem;
&.active { &.active {
color: $color-gray; color: $color-gray;
.icon { .icon {
@include icon-color($color-gray); @include icon-color($color-gray);
} }
@ -150,7 +144,6 @@
.unfunded { .unfunded {
color: $color-red; color: $color-red;
.icon { .icon {
@include icon-color($color-red); @include icon-color($color-red);
} }
@ -166,7 +159,7 @@
} }
.portfolio-content { .portfolio-content {
margin: (6 * $gap) $gap 0 $gap; margin: 6 * $gap $gap 0 $gap;
.panel { .panel {
@include shadow-panel; @include shadow-panel;
@ -175,7 +168,6 @@
.member-list { .member-list {
.panel { .panel {
@include shadow-panel; @include shadow-panel;
padding-bottom: 0; padding-bottom: 0;
} }
@ -191,7 +183,7 @@
} }
tr:first-child { tr:first-child {
padding: 0 (2 * $gap) 0 (5 * $gap); padding: 0 2 * $gap 0 5 * $gap;
} }
td { td {
@ -203,14 +195,14 @@
th { th {
background-color: $color-gray-lightest; background-color: $color-gray-lightest;
padding: $gap (2 * $gap); padding: $gap 2 * $gap;
border-top: none; border-top: none;
border-bottom: none; border-bottom: none;
color: $color-gray; color: $color-gray;
} }
td:first-child { td:first-child {
padding: (2 * $gap) (2 * $gap) (2 * $gap) (5 * $gap); padding: 2 * $gap 2 * $gap 2 * $gap 5 * $gap;
} }
tbody { tbody {
@ -218,7 +210,7 @@
border-bottom: 1px solid $color-gray-lightest; border-bottom: 1px solid $color-gray-lightest;
font-size: 1.6rem; font-size: 1.6rem;
border-top: 0; border-top: 0;
padding: (3 * $gap) (2 * $gap); padding: 3 * $gap 2 * $gap;
.usa-button-disabled { .usa-button-disabled {
color: $color-gray-medium; color: $color-gray-medium;
@ -318,6 +310,11 @@
.alert { .alert {
margin: 4rem; margin: 4rem;
} }
.member-list__subhead {
font-weight: $font-normal;
font-size: $base-font-size;
}
} }
.application-content { .application-content {
@ -333,7 +330,6 @@
.block-list__footer { .block-list__footer {
border-bottom: none; border-bottom: none;
} }
.application-edit__env-list-item { .application-edit__env-list-item {
label { label {
color: $color-black; color: $color-black;
@ -373,7 +369,6 @@
.portfolio-applications__header--actions { .portfolio-applications__header--actions {
color: $color-blue; color: $color-blue;
font-size: $small-font-size; font-size: $small-font-size;
.icon { .icon {
@include icon-color($color-blue); @include icon-color($color-blue);
@include icon-size(14); @include icon-size(14);
@ -384,7 +379,6 @@
.application-list { .application-list {
.toggle-link { .toggle-link {
background-color: $color-blue-light; background-color: $color-blue-light;
.icon { .icon {
margin: $gap / 2; margin: $gap / 2;
} }
@ -402,7 +396,6 @@
.application-list-item__environment__csp_link { .application-list-item__environment__csp_link {
font-size: $small-font-size; font-size: $small-font-size;
font-weight: normal; font-weight: normal;
&:hover { &:hover {
background-color: $color-aqua-light; background-color: $color-aqua-light;
} }
@ -420,7 +413,6 @@
.subheading { .subheading {
@include subheading; @include subheading;
margin-top: 6 * $gap; margin-top: 6 * $gap;
margin-bottom: 2 * $gap; margin-bottom: 2 * $gap;
} }
@ -431,6 +423,7 @@
.pending-task-order { .pending-task-order {
background-color: $color-gold-lightest; background-color: $color-gold-lightest;
align-items: center; align-items: center;
margin: 0; margin: 0;
margin-bottom: 2 * $gap; margin-bottom: 2 * $gap;
@ -462,7 +455,6 @@
.icon--tiny { .icon--tiny {
@include icon-size(10); @include icon-size(10);
margin-left: 1rem; margin-left: 1rem;
} }
} }
@ -487,7 +479,7 @@
th { th {
background-color: $color-gray-lightest; background-color: $color-gray-lightest;
padding: $gap (2 * $gap); padding: $gap 2 * $gap;
border-top: none; border-top: none;
border-bottom: none; border-bottom: none;
color: $color-gray; color: $color-gray;
@ -566,7 +558,6 @@
.panel { .panel {
@include shadow-panel; @include shadow-panel;
margin-bottom: 4 * $gap; margin-bottom: 4 * $gap;
} }
} }
@ -586,7 +577,6 @@
input { input {
max-width: 30em; max-width: 30em;
} }
.icon-validation { .icon-validation {
left: 30em; left: 30em;
} }
@ -615,3 +605,44 @@
} }
} }
} }
.member-list__name {
margin-top: 1rem;
}
.member-list__role-select {
overflow: auto;
margin: 1.6rem -3.2rem -1.6rem -3.2rem;
padding: 2rem 3.2rem 2rem 5rem;
background: $color-gray-cool-light;
border-top: 1px solid $color-gray-lighter;
> label {
font-weight: $font-bold;
margin: 0;
}
> label:first-child + ul.member-list____role-select__radio {
display: flex;
background: $color-gray-cool-light;
li {
border-bottom: none;
label {
margin-top: 1rem;
margin-left: 2rem;
}
}
li:first-child > label {
margin-left: 0;
}
}
button {
font-size: $small-font-size;
float: right;
margin-right: 0;
}
}

View File

@ -73,7 +73,7 @@ $color-gray-lightest: #f1f1f1;
$color-gray-warm-dark: #494440; $color-gray-warm-dark: #494440;
$color-gray-warm-light: #e4e2e0; $color-gray-warm-light: #e4e2e0;
$color-gray-cool-light: #dce4ef; $color-gray-cool-light: #eff2f7;
$color-gold-dark: #cd841b; $color-gold-dark: #cd841b;
$color-gold: #fdb81e; $color-gold: #fdb81e;

View File

@ -12,7 +12,7 @@
<button type='button' class='icon-link modal__dismiss' v-on:click='closeModal("{{name}}")'> <button type='button' class='icon-link modal__dismiss' v-on:click='closeModal("{{name}}")'>
{{ Icon('x') }} {{ Icon('x') }}
<span> <span>
{{ "components.modal.close" | translate }} {{ "common.close" | translate }}
</span> </span>
</button> </button>
{% endif %} {% endif %}

View File

@ -11,7 +11,11 @@
<toggler inline-template> <toggler inline-template>
<li class="accordion-table__item"> <li class="accordion-table__item">
<div class="accordion-table__item-content row"> <div class="accordion-table__item-content row">
<div class="col col--grow">{{ member_form.user_name.data }}</div> <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_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_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">{{ OptionsInput(permissions_form.perms_del_env, label=False, watch=True) }}</div>
@ -26,10 +30,10 @@
{{ {{
ToggleButton( ToggleButton(
open_html=open_html, open_html=open_html,
close_html=close_html, close_html=close_html,
section_name="environments" section_name="environments"
) )
}} }}
</div> </div>
</div> </div>
@ -37,19 +41,58 @@
<ul> <ul>
{% for environment_form in environment_roles_form %} {% for environment_form in environment_roles_form %}
<li class="accordion-table__item__expanded"> <li class="accordion-table__item__expanded">
{{ environment_form.environment_name.data }} <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> </li>
{% endfor %} {% endfor %}
{% if user_can(permissions.DELETE_APPLICATION_MEMBER) %}
<li class="accordion-table__item__expanded action-group">
<span class="usa-button button-danger" v-on:click="openModal('{{ delete_modal_id }}')">
{{ "portfolios.applications.remove_member.button" | translate }}
</span>
</li>
{% endif %}
</ul> </ul>
<div class="accordion-table__item__action-group">
{% if user_can(permissions.ASSIGN_ENVIRONMENT_MEMBER) %}
<a class="icon-link">
{{ "portfolios.applications.team_settings.add_to_environment" | translate }}
{{ Icon("plus") }}
</a>
{% endif %}
{% if user_can(permissions.DELETE_APPLICATION_MEMBER) %}
<button
type="button"
class='usa-button button-danger'
v-on:click="openModal('{{ delete_modal_id }}')"
>
{{ "portfolios.applications.remove_member.button" | translate }}
</button>
{% endif %}
</div>
{% endcall %} {% endcall %}
{{ member_form.user_id() }} {{ member_form.user_id() }}
</li> </li>
</toggler> </toggler>
{% endfor %} {% endfor %}

View File

@ -1,3 +1,5 @@
{% from "components/toggle_list.html" import ToggleButton, ToggleSection %}
{% for member in team_form.members %} {% 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] %} {% set user_permissions = [member.permission_sets.perms_team_mgmt, member.permission_sets.perms_env_mgmt, member.permission_sets.perms_del_env] %}

View File

@ -52,62 +52,60 @@
{% if g.matchesPath("application-members") %} {% if g.matchesPath("application-members") %}
{% include "fragments/flash.html" %} {% include "fragments/flash.html" %}
{% endif %} {% endif %}
<header> <header>
<div class="responsive-table-wrapper__header"> <div class="responsive-table-wrapper__header">
<div class="responsive-table-wrapper__title row"> <div class="responsive-table-wrapper__title row">
<div class="h3"> <div class="h3">
{{ "portfolios.applications.team_settings.section.title" | translate({ "application_name": application.name }) }} {{ "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>
<a class="icon-link">
{{ Icon('info') }}
{{ "portfolios.admin.settings_info" | translate }}
</a>
</div> </div>
</header> </header>
<div class="accordion-table accordion-table-list"> <div class="accordion-table accordion-table-list">
<div class="accordion-table__head row"> <div class="accordion-table__head row">
<div class="col col--grow"> <div class="col col--grow">
{{ "common.name" | translate }} {{ "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> </div>
<div class="col col--grow"> <ul class="accordion-table__items">
{{ "portfolios.applications.team_settings.section.table.team_management" | translate }} {% if user_can(permissions.EDIT_APPLICATION_MEMBER) %}
</div> {% include "fragments/applications/edit_team.html" %}
<div class="col col--grow"> {% elif user_can(permissions.VIEW_APPLICATION_MEMBER) %}
{{ "portfolios.applications.team_settings.section.table.environment_management" | translate }} {% include "fragments/applications/read_only_team.html" %}
</div> {% endif %}
<div class="col col--grow"> </ul>
{{ "portfolios.applications.team_settings.section.table.delete_access" | translate }} </div>
</div>
<div class="col col--grow"> <div class="panel__footer">
&nbsp; <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> </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>
<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> </form>
</base-form> </base-form>

View File

@ -18,11 +18,11 @@ from atst.domain.environments import Environments
from atst.domain.permission_sets import PermissionSets from atst.domain.permission_sets import PermissionSets
from atst.domain.portfolios import Portfolios from atst.domain.portfolios import Portfolios
from atst.domain.exceptions import NotFoundError from atst.domain.exceptions import NotFoundError
from atst.models.environment_role import CSPRole from atst.models.environment_role import CSPRole
from atst.models.portfolio_role import Status as PortfolioRoleStatus from atst.models.portfolio_role import Status as PortfolioRoleStatus
from atst.forms.application import EditEnvironmentForm from atst.forms.application import EditEnvironmentForm
from atst.forms.app_settings import AppEnvRolesForm from atst.forms.app_settings import AppEnvRolesForm
from atst.forms.data import ENV_ROLE_NO_ACCESS as NO_ACCESS
from tests.utils import captured_templates from tests.utils import captured_templates
@ -166,7 +166,7 @@ def test_data_for_app_env_roles_form(app, client, user_session):
"env_id": env.id, "env_id": env.id,
"team_roles": [ "team_roles": [
{ {
"role": "no_access", "role": NO_ACCESS,
"members": [ "members": [
{ {
"user_id": str(app_role.user_id), "user_id": str(app_role.user_id),
@ -309,7 +309,7 @@ def test_update_team_env_roles(client, user_session):
"envs-0-team_roles-1-members-1-user_id": env_role_2.user.id, "envs-0-team_roles-1-members-1-user_id": env_role_2.user.id,
"envs-0-team_roles-1-members-1-role_name": CSPRole.BASIC_ACCESS.value, "envs-0-team_roles-1-members-1-role_name": CSPRole.BASIC_ACCESS.value,
"envs-0-team_roles-1-members-2-user_id": env_role_3.user.id, "envs-0-team_roles-1-members-2-user_id": env_role_3.user.id,
"envs-0-team_roles-1-members-2-role_name": "no_access", "envs-0-team_roles-1-members-2-role_name": NO_ACCESS,
} }
user_session(application.portfolio.owner) user_session(application.portfolio.owner)

View File

@ -3,6 +3,8 @@ import uuid
from flask import url_for from flask import url_for
from atst.domain.permission_sets import PermissionSets 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 * from tests.factories import *
@ -17,7 +19,7 @@ def test_application_team(client, user_session):
assert response.status_code == 200 assert response.status_code == 200
def test_update_team(client, user_session): def test_update_team_permissions(client, user_session):
application = ApplicationFactory.create() application = ApplicationFactory.create()
owner = application.portfolio.owner owner = application.portfolio.owner
app_role = ApplicationRoleFactory.create( app_role = ApplicationRoleFactory.create(
@ -91,6 +93,63 @@ def test_update_team_with_non_app_user(client, user_session):
assert response.status_code == 404 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=[]
)
app_user = app_role.user
environment = EnvironmentFactory.create(application=application)
env_role = EnvironmentRoleFactory.create(
user=app_user, 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-user_id": app_user.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
app_role = ApplicationRoleFactory.create(
application=application, permission_sets=[]
)
app_user = app_role.user
environment = EnvironmentFactory.create(application=application)
env_role = EnvironmentRoleFactory.create(
user=app_user, environment=environment, role=CSPRole.BASIC_ACCESS.value
)
user_session(owner)
response = client.post(
url_for("applications.update_team", application_id=application.id),
data={
"members-0-user_id": app_user.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()
def test_create_member(client, user_session): def test_create_member(client, user_session):
user = UserFactory.create() user = UserFactory.create()
application = ApplicationFactory.create( application = ApplicationFactory.create(

View File

@ -20,6 +20,7 @@ base_public:
common: common:
back: Back back: Back
cancel: Cancel cancel: Cancel
close: Close
confirm: Confirm confirm: Confirm
continue: Continue continue: Continue
delete: Delete delete: Delete
@ -46,7 +47,6 @@ common:
name: Name name: Name
components: components:
modal: modal:
close: Close
destructive_message: You will no longer be able to access this {resource} destructive_message: You will no longer be able to access this {resource}
destructive_title: Warning! This action is permanent destructive_title: Warning! This action is permanent
usa_header: usa_header:
@ -80,7 +80,7 @@ flash:
portfolio_home: Go to my portfolio home page portfolio_home: Go to my portfolio home page
success: Success! success: Success!
new_application_member: 'You have successfully invited {user_name} to the team.' new_application_member: 'You have successfully invited {user_name} to the team.'
updated_application_members_permissions: 'You have successfully updated member permissions.' updated_application_team_settings: 'You have updated the {application_name} team settings.'
footer: footer:
about_link_text: Joint Enterprise Defense Infrastructure about_link_text: Joint Enterprise Defense Infrastructure
browser_support: JEDI Cloud supported on these web browsers browser_support: JEDI Cloud supported on these web browsers
@ -468,6 +468,7 @@ portfolios:
subheading: Team Settings subheading: Team Settings
title: '{application_name} Team Settings' title: '{application_name} Team Settings'
user: User user: User
add_to_environment: Add to existing environment
team_text: Team team_text: Team
update_button_text: Save update_button_text: Save
members: members: