Merge pull request #799 from dod-ccpo/edit-env-form-view-part-2

Edit env form view part 2
This commit is contained in:
leigh-mil 2019-05-14 11:14:46 -04:00 committed by GitHub
commit 4c2b6c331b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 613 additions and 179 deletions

View File

@ -98,7 +98,7 @@ class Environments(object):
environment = Environments.get(environment_id) environment = Environments.get(environment_id)
for member in team_roles: for member in team_roles:
new_role = member["role"] new_role = member["role_name"]
user = Users.get(member["user_id"]) user = Users.get(member["user_id"])
Environments.update_env_role( Environments.update_env_role(
environment=environment, user=user, new_role=new_role environment=environment, user=user, new_role=new_role
@ -113,6 +113,15 @@ class Environments(object):
environment=environment, user=member, new_role=new_role environment=environment, user=member, new_role=new_role
) )
@classmethod
def get_members_by_role(cls, env, role):
return (
db.session.query(EnvironmentRole)
.filter(EnvironmentRole.environment_id == env.id)
.filter(EnvironmentRole.role == role)
.all()
)
@classmethod @classmethod
def revoke_access(cls, environment, target_user): def revoke_access(cls, environment, target_user):
EnvironmentRoles.delete(environment.id, target_user.id) EnvironmentRoles.delete(environment.id, target_user.id)

View File

@ -1,16 +1,32 @@
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms.fields import StringField, HiddenField, RadioField, FieldList, FormField 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
class EnvMemberRoleForm(FlaskForm): class MemberForm(FlaskForm):
name = StringField()
user_id = HiddenField() user_id = HiddenField()
role = RadioField(choices=ENV_ROLES, coerce=BaseForm.remove_empty_string) user_name = StringField()
role_name = RadioField(choices=ENV_ROLES, default="no_access")
@property
def data(self):
_data = super().data
if "role_name" in _data and _data["role_name"] == "no_access":
_data["role_name"] = None
return _data
class EnvironmentRolesForm(BaseForm): class RoleForm(FlaskForm):
team_roles = FieldList(FormField(EnvMemberRoleForm)) role = HiddenField()
members = FieldList(FormField(MemberForm))
class EnvironmentRolesForm(FlaskForm):
team_roles = FieldList(FormField(RoleForm))
env_id = HiddenField() env_id = HiddenField()
class AppEnvRolesForm(BaseForm):
envs = FieldList(FormField(EnvironmentRolesForm))

View File

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

View File

@ -21,7 +21,7 @@ class Environment(
@property @property
def users(self): def users(self):
return [r.user for r in self.roles] return {r.user for r in self.roles}
@property @property
def num_users(self): def num_users(self):

View File

@ -1,10 +1,9 @@
from flask import redirect, render_template, request as http_request, url_for from flask import redirect, render_template, request as http_request, url_for
from . import applications_bp from . import applications_bp
from atst.domain.environment_roles import EnvironmentRoles
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.forms.app_settings import EnvironmentRolesForm from atst.forms.app_settings import AppEnvRolesForm
from atst.forms.application import ApplicationForm, EditEnvironmentForm from atst.forms.application import ApplicationForm, EditEnvironmentForm
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
@ -20,37 +19,59 @@ def get_environments_obj_for_app(application):
"id": env.id, "id": env.id,
"name": env.name, "name": env.name,
"edit_form": EditEnvironmentForm(obj=env), "edit_form": EditEnvironmentForm(obj=env),
"members_form": EnvironmentRolesForm(data=data_for_env_members_form(env)), "member_count": len(env.users),
"members": sort_env_users_by_role(env), "members": [user.full_name for user in env.users],
} }
environments_obj.append(env_data) environments_obj.append(env_data)
return environments_obj return environments_obj
def sort_env_users_by_role(env): def serialize_members(member_list, role):
users_dict = {"no_access": []} serialized_list = []
for role in CSPRole:
users_dict[role.value] = []
for user in env.application.users: for member in member_list:
if user in env.users: serialized_list.append(
role = EnvironmentRoles.get(user.id, env.id) {
users_dict[role.displayname].append( "user_id": str(member.user_id),
{"name": user.full_name, "user_id": user.id} "user_name": member.user.full_name,
"role_name": role,
}
) )
else:
users_dict["no_access"].append({"name": user.full_name, "user_id": user.id})
return users_dict return serialized_list
def data_for_env_members_form(environment): def sort_env_users_by_role(env):
data = {"env_id": environment.id, "team_roles": []} users_list = []
for user in environment.users: no_access_users = env.application.users - env.users
env_role = EnvironmentRoles.get(user.id, environment.id) no_access_list = [
data["team_roles"].append( {"user_id": str(user.id), "user_name": user.full_name, "role_name": "no_access"}
{"name": user.full_name, "user_id": user.id, "role": env_role.displayname} for user in no_access_users
]
users_list.append({"role": "no_access", "members": no_access_list})
for role in CSPRole:
users_list.append(
{
"role": role.value,
"members": serialize_members(
Environments.get_members_by_role(env, role.value), role.value
),
}
)
return users_list
def data_for_app_env_roles_form(application):
data = {"envs": []}
for environment in application.environments:
data["envs"].append(
{
"env_id": environment.id,
"team_roles": sort_env_users_by_role(environment),
}
) )
return data return data
@ -67,15 +88,19 @@ def check_users_are_in_application(user_ids, application):
@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):
# refactor like portfolio admin render function
application = Applications.get(application_id) application = Applications.get(application_id)
form = ApplicationForm(name=application.name, description=application.description) form = ApplicationForm(name=application.name, description=application.description)
environments_obj = get_environments_obj_for_app(application=application)
members_form = AppEnvRolesForm(data=data_for_app_env_roles_form(application))
return render_template( return render_template(
"portfolios/applications/settings.html", "portfolios/applications/settings.html",
application=application, application=application,
form=form, form=form,
environments_obj=get_environments_obj_for_app(application=application), environments_obj=environments_obj,
members_form=members_form,
active_toggler=http_request.args.get("active_toggler"),
active_toggler_section=http_request.args.get("active_toggler_section"),
) )
@ -98,6 +123,8 @@ def update_environment(environment_id):
application_id=application.id, application_id=application.id,
fragment="application-environments", fragment="application-environments",
_anchor="application-environments", _anchor="application-environments",
active_toggler=environment.id,
active_toggler_section="edit",
) )
) )
else: else:
@ -109,6 +136,11 @@ def update_environment(environment_id):
name=application.name, description=application.description name=application.name, description=application.description
), ),
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)
),
active_toggler=environment.id,
active_toggler_section="edit",
), ),
400, 400,
) )
@ -143,13 +175,17 @@ def update(application_id):
def update_env_roles(environment_id): def update_env_roles(environment_id):
environment = Environments.get(environment_id) environment = Environments.get(environment_id)
application = environment.application application = environment.application
env_roles_form = EnvironmentRolesForm(http_request.form) form = AppEnvRolesForm(formdata=http_request.form)
if env_roles_form.validate():
if form.validate():
env_data = []
try: try:
user_ids = [user["user_id"] for user in env_roles_form.data["team_roles"]] for env in form.envs.data:
if env["env_id"] == str(environment.id):
for role in env["team_roles"]:
user_ids = [user["user_id"] for user in role["members"]]
check_users_are_in_application(user_ids, application) check_users_are_in_application(user_ids, application)
env_data = env_data + role["members"]
except NotFoundError as err: except NotFoundError as err:
app.logger.warning( app.logger.warning(
"User {} requested environment role change for unauthorized user {} in application {}.".format( "User {} requested environment role change for unauthorized user {} in application {}.".format(
@ -159,22 +195,36 @@ def update_env_roles(environment_id):
) )
raise (err) raise (err)
env_data = env_roles_form.data
Environments.update_env_roles_by_environment( Environments.update_env_roles_by_environment(
environment_id=environment_id, team_roles=env_data["team_roles"] environment_id=environment_id, team_roles=env_data
)
flash("application_environment_members_updated")
return redirect(
url_for(
"applications.settings",
application_id=application.id,
fragment="application-environments",
_anchor="application-environments",
active_toggler=environment.id,
active_toggler_section="members",
)
) )
return redirect(url_for("applications.settings", application_id=application.id))
else: else:
# TODO: Create a better pattern to handle when a form doesn't validate return (
# if a user is submitting the data via the web page then they render_template(
# should never have any form validation errors
return render_template(
"portfolios/applications/settings.html", "portfolios/applications/settings.html",
application=application, application=application,
form=ApplicationForm( form=ApplicationForm(
name=application.name, description=application.description name=application.name, description=application.description
), ),
environments_obj=get_environments_obj_for_app(application=application), environments_obj=get_environments_obj_for_app(application=application),
active_toggler=environment.id,
active_toggler_section="edit",
),
400,
) )

View File

@ -7,6 +7,11 @@ MESSAGES = {
"message_template": 'The environment "{{ environment_name }}" has been deleted', "message_template": 'The environment "{{ environment_name }}" has been deleted',
"category": "success", "category": "success",
}, },
"application_environment_members_updated": {
"title_template": "Application environment members updated",
"message_template": "Application environment members have been updated",
"category": "success",
},
"application_environments_updated": { "application_environments_updated": {
"title_template": "Application environments updated", "title_template": "Application environments updated",
"message_template": "Application environments have been updated", "message_template": "Application environments have been updated",

View File

@ -0,0 +1,94 @@
import { shallowMount } from '@vue/test-utils'
import EditEnvironmentRole from '../forms/edit_environment_role'
describe('EditEnvironmentRole', () => {
var initialRoleCategories, wrapper
beforeEach(() => {
initialRoleCategories = [
{
role: 'no_access',
members: [
{ role_name: null, user_id: '123' },
{ role_name: null, user_id: '456' },
],
},
{
role: 'Basic Access',
members: [{ role_name: 'Basic Access', user_id: '789' }],
},
{
role: 'Network Admin',
members: [],
},
{
role: 'Business Read-only',
members: [
{ role_name: 'Business Read-only', user_id: '012' },
{ role_name: 'Business Read-only', user_id: '345' },
],
},
{
role: 'Technical Read-only',
members: [{ role_name: 'Technical Read-only', user_id: '678' }],
},
]
wrapper = shallowMount(EditEnvironmentRole, {
propsData: { initialRoleCategories },
})
})
it('removes null roles to no_access', () => {
let roles = wrapper.vm.sanitizeValues([
{ role: 'no_access', members: [{ role_name: null }] },
])
expect(roles).toEqual([
{ role: 'no_access', members: [{ role_name: 'no_access' }] },
])
})
it('gets the data for a user', () => {
let member_data = wrapper.vm.getUserInfo('678')
expect(member_data).toEqual({
role_name: 'Technical Read-only',
user_id: '678',
})
})
it('removes a user from role', () => {
let techRole = wrapper.vm.roleCategories.find(role => {
return role.role === 'Technical Read-only'
})
expect(techRole.members.length).toEqual(1)
wrapper.vm.removeUser('678')
expect(techRole.members.length).toEqual(0)
})
it('adds user to a role', () => {
let techRole = wrapper.vm.roleCategories.find(role => {
return role.role === 'Technical Read-only'
})
expect(techRole.members.length).toEqual(1)
wrapper.vm.addUser({ user_id: '901' }, 'Technical Read-only')
expect(techRole.members.length).toEqual(2)
})
it('updates users role', () => {
let techRole = wrapper.vm.roleCategories.find(role => {
return role.role === 'Technical Read-only'
})
let businessRole = wrapper.vm.roleCategories.find(role => {
return role.role === 'Business Read-only'
})
expect(techRole.members.length).toEqual(1)
expect(businessRole.members.length).toEqual(2)
wrapper.vm.updateRoles('678', 'Business Read-only')
expect(techRole.members.length).toEqual(0)
expect(businessRole.members.length).toEqual(3)
})
})

View File

@ -1,63 +1,96 @@
import FormMixin from '../../mixins/form' import FormMixin from '../../mixins/form'
import textinput from '../text_input'
import Selector from '../selector'
import Modal from '../../mixins/modal' import Modal from '../../mixins/modal'
import toggler from '../toggler'
// Note: If refactoring consider using nested vue components as suggested by Dan:
// 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
export default { export default {
name: 'edit-environment-role', name: 'edit-environment-role',
mixins: [FormMixin, Modal], mixins: [FormMixin],
components: {
toggler,
Modal,
Selector,
textinput,
},
props: { props: {
choices: Array, initialRoleCategories: Array,
initialData: String,
applicationId: String,
}, },
data: function() { data: function() {
return { return {
new_role: this.initialData, selectedSection: null,
roleCategories: this.sanitizeValues(this.initialRoleCategories),
} }
}, },
mounted: function() {
this.$root.$on('revoke-' + this.applicationId, this.revoke)
},
methods: { methods: {
change: function(e) { sanitizeValues: function(roles) {
this.new_role = e.target.value roles.forEach(role => {
role.members.forEach(member => {
if (member.role_name === null) {
member.role_name = 'no_access'
}
})
})
return roles
}, },
cancel: function() {
this.new_role = this.initialData checkNoAccess: function(role) {
return role === 'no_access'
}, },
revoke: function() {
this.new_role = '' toggleSection: function(sectionName) {
if (this.selectedSection === sectionName) {
this.selectedSection = null
} else {
this.selectedSection = sectionName
}
},
onInput: function(e) {
this.changed = true
this.updateRoles(e.target.attributes['user-id'].value, e.target.value)
this.showError = false
this.showValid = true
},
getUserInfo: function(userId) {
for (let role of this.roleCategories) {
for (let member of role.members) {
if (member.user_id === userId) {
return member
}
}
}
},
removeUser: function(userId) {
for (let role of this.roleCategories) {
role.members = role.members.filter(member => {
return member.user_id !== userId
})
if (!role.members) {
role.members = []
}
}
},
addUser: function(userInfo, newRole) {
this.roleCategories.forEach(role => {
if (role.role === newRole) {
userInfo.role_name = newRole
role.members.push(userInfo)
}
})
},
updateRoles: function(userId, newRole) {
var userInfo = this.getUserInfo(userId)
this.removeUser(userId)
this.addUser(userInfo, newRole)
this.toggleSection()
}, },
}, },
computed: { render: function(createElement) {
displayName: function() { return createElement('p', 'Please implement inline-template')
const newRole = this.newRole
for (var arr in this.choices) {
if (this.choices[arr][0] == newRole) {
return this.choices[arr][1].name
}
}
},
label_class: function() {
return this.newRole === '' ? 'label' : 'label label--success'
},
newRole: function() {
return this.new_role
},
}, },
} }

View File

@ -1,3 +1,4 @@
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'
@ -7,7 +8,16 @@ export default {
mixins: [FormMixin], mixins: [FormMixin],
props: {
initialSelectedSection: {
type: String,
default: null,
},
},
components: { components: {
editEnvironmentRole,
optionsinput,
textinput, textinput,
optionsinput, optionsinput,
}, },

View File

@ -25,11 +25,14 @@
.app-team-settings-link { .app-team-settings-link {
font-size: $small-font-size; font-size: $small-font-size;
font-weight: $font-normal; font-weight: $font-normal;
padding-left: $gap * 2; }
.environment-roles {
padding: 0 ($gap * 3) ($gap * 3);
} }
.environment-role { .environment-role {
padding: $gap * 3; padding: ($gap * 2) 0;
h4 { h4 {
margin-bottom: $gap / 4; margin-bottom: $gap / 4;
@ -50,16 +53,43 @@
margin: $gap; margin: $gap;
white-space: nowrap; white-space: nowrap;
width: 20rem; width: 20rem;
position: relative;
height: 3.6rem;
&.unassigned { &.unassigned {
border: solid 1px $color-gray-light; border: solid 1px $color-gray-light;
} }
.icon-link {
padding: 0;
}
.environment-role__user-field {
position: absolute;
background-color: $color-white;
margin-top: $gap * 2;
padding: $gap;
left: -0.1rem;
border: solid 1px $color-gray-light;
width: 20rem;
z-index: 3;
.usa-input {
margin: 0;
li {
background-color: $color-white;
border: none;
}
}
}
} }
.environment-role__no-user { .environment-role__no-user {
margin: $gap; margin: $gap;
padding: ($gap / 2) $gap; padding: ($gap / 2) $gap;
font-weight: $font-normal; font-weight: $font-normal;
height: 3.6rem;
} }
} }
} }
@ -94,3 +124,19 @@
font-weight: $font-normal; font-weight: $font-normal;
color: $color-gray-medium; color: $color-gray-medium;
} }
.application-list-item {
.usa-button-primary {
width: $search-button-width * 2;
}
.action-group-cancel {
position: relative;
.action-group-cancel__action {
position: absolute;
right: $search-button-width * 2 + $gap * 2;
top: -($gap * 8);
}
}
}

View File

@ -9,8 +9,8 @@
</span> </span>
{% endmacro %} {% endmacro %}
{% macro ToggleSection(section_name) %} {% macro ToggleSection(section_name, classes="") %}
<div v-show="selectedSection === '{{ section_name }}'"> <div v-show="selectedSection === '{{ section_name }}'" class='{{ classes }}'>
{{ caller() }} {{ caller() }}
</div> </div>
{% endmacro %} {% endmacro %}

View File

@ -0,0 +1,97 @@
{% 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.user_id)" class="icon-link right">
{{ Icon('edit', classes="icon--medium") }}
</span>
<div
v-show="selectedSection === member.user_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.user_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 + '-user_id'"
v-bind:name="'envs-{{ loop.index0 }}-team_roles-' + roleindex + '-members-' + memberindex + '-user_id'"
type="hidden"
v-bind:value='member.user_id'>
</li>
</ul>
</div>
<div class='action-group'>
{{
SaveButton(
text=("portfolios.applications.update_button_text" | 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,31 +1,11 @@
{% 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 %}
{% from "components/modal.html" import Modal %} {% from "components/modal.html" import Modal %}
{% from "components/options_input.html" import OptionsInput %}
{% 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 %} {% from "components/toggle_list.html" import ToggleButton, ToggleSection %}
{% macro RolePanel(users=[], role='no_access') %}
{% if role == 'no_access' %}
{% set role = 'Unassigned (No Access)' %}
{% set unassigned = True %}
{% endif %}
<div class='environment-role'>
<h4>{{ role }}</h4>
<ul class='environment-role__users'>
{% for user in users %}
<li class="environment-role__user {{ 'unassigned' if unassigned }}">
{{ user.name }}{{ Icon('edit', classes="icon--medium right") }}
</li>
{% endfor %}
{% if users == [] %}
<div class='environment-role__no-user'>Currently no members are in this role</div>
{% endif %}
</ul>
</div>
{% endmacro %}
<div class="application-list-item application-list"> <div class="application-list-item application-list">
<header> <header>
@ -50,13 +30,10 @@
<ul class="accordion-table__items"> <ul class="accordion-table__items">
{% for env in environments_obj %} {% for env in environments_obj %}
{% set edit_form = env['edit_form'] %}
{% set member_count = env['members_form'].data['team_roles'] | length %}
{% set members_by_role = env['members'] %}
{% set unassigned = members_by_role['no_access'] %}
{% set delete_environment_modal_id = "delete_modal_environment{}".format(env['id']) %} {% set delete_environment_modal_id = "delete_modal_environment{}".format(env['id']) %}
{% set edit_form = env['edit_form'] %}
<toggler inline-template {% if edit_form.errors %}initial-selected-section="edit"{% endif %}> <toggler inline-template {% if active_toggler == (env['id'] | safe) %}initial-selected-section="{{ active_toggler_section }}"{% endif %}>
<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"> <div class="col col--grow">
@ -84,11 +61,11 @@
</div> </div>
<div class="col col--grow icon-link icon-link--large accordion-table__item__toggler"> <div class="col col--grow icon-link icon-link--large accordion-table__item__toggler">
{% set open_members_button %} {% set open_members_button %}
{{ "common.members" | translate }} ({{ member_count }}) {{ Icon('caret_down') }} {{ "common.members" | translate }} ({{ env['member_count'] }}) {{ Icon('caret_down') }}
{% endset %} {% endset %}
{% set close_members_button %} {% set close_members_button %}
{{ "common.members" | translate }} ({{ member_count }}) {{ Icon('caret_up') }} {{ "common.members" | translate }} ({{ env['member_count'] }}) {{ Icon('caret_up') }}
{% endset %} {% endset %}
{{ {{
@ -101,11 +78,8 @@
</div> </div>
</div> </div>
{% call ToggleSection(section_name="members") %} {% call ToggleSection(section_name="members", classes="environment-roles") %}
<div class='app-team-settings-link'>Need to add someone new to the team? <a href='{{ url_for("applications.team", application_id=application.id) }}'>Jump to Team Settings</a></div> {% include 'fragments/applications/edit_environment_team_form.html' %}
{% for role, members in members_by_role.items() %}
{{ RolePanel(users=members, role=role) }}
{% endfor %}
{% endcall %} {% endcall %}
{% call ToggleSection(section_name="edit") %} {% call ToggleSection(section_name="edit") %}
@ -152,3 +126,11 @@
</ul> </ul>
</div> </div>
</div> </div>
<div class="panel__footer">
<div class="action-group">
<a class='icon-link'>
{{ "portfolios.applications.add_environment" | translate }}
{{ Icon('plus') }}
</a>
</div>
</div>

View File

@ -26,11 +26,11 @@
<span class="icon-link icon-link--large accordion-table__item__toggler"> <span class="icon-link icon-link--large accordion-table__item__toggler">
{% set open_members_button %} {% set open_members_button %}
{{ "common.members" | translate }} ({{ env['members'] | length }}) {{ Icon('caret_down') }} {{ "common.members" | translate }} ({{ env['member_count'] }}) {{ Icon('caret_down') }}
{% endset %} {% endset %}
{% set close_members_button %} {% set close_members_button %}
{{ "common.members" | translate }} ({{ env['members'] | length }}) {{ Icon('caret_up') }} {{ "common.members" | translate }} ({{ env['member_count'] }}) {{ Icon('caret_up') }}
{% endset %} {% endset %}
{{ {{
@ -47,7 +47,7 @@
<ul> <ul>
{% for member in env['members'] %} {% for member in env['members'] %}
<li class="accordion-table__item__expanded"> <li class="accordion-table__item__expanded">
<div class="accordion-table__item__expanded_first">{{ member.name }}</div> <div class="accordion-table__item__expanded_first">{{ member }}</div>
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>

View File

@ -68,15 +68,7 @@
{% if user_can(permissions.EDIT_APPLICATION) %} {% if user_can(permissions.EDIT_APPLICATION) %}
{% include "fragments/applications/edit_environments.html" %} {% include "fragments/applications/edit_environments.html" %}
<div class="panel__footer">
<div class="action-group">
<button class="usa-button usa-button-primary" tabindex="0" type="submit">{{ 'portfolios.applications.update_button_text' | translate }}</button>
<a class='icon-link'>
{{ "portfolios.applications.add_environment" | translate }}
{{ Icon('plus') }}
</a>
</div>
</div>
{% elif user_can(permissions.VIEW_ENVIRONMENT) %} {% elif user_can(permissions.VIEW_ENVIRONMENT) %}
{% include "fragments/applications/read_only_environments.html" %} {% include "fragments/applications/read_only_environments.html" %}
{% endif %} {% endif %}

View File

@ -79,18 +79,18 @@ def test_update_env_roles_by_environment():
team_roles = [ team_roles = [
{ {
"user_id": env_role_1.user.id, "user_id": env_role_1.user.id,
"name": env_role_1.user.full_name, "user_name": env_role_1.user.full_name,
"role": CSPRole.BUSINESS_READ.value, "role_name": CSPRole.BUSINESS_READ.value,
}, },
{ {
"user_id": env_role_2.user.id, "user_id": env_role_2.user.id,
"name": env_role_2.user.full_name, "user_name": env_role_2.user.full_name,
"role": CSPRole.NETWORK_ADMIN.value, "role_name": CSPRole.NETWORK_ADMIN.value,
}, },
{ {
"user_id": env_role_3.user.id, "user_id": env_role_3.user.id,
"name": env_role_3.user.full_name, "user_name": env_role_3.user.full_name,
"role": None, "role_name": None,
}, },
] ]
@ -136,6 +136,36 @@ def test_update_env_roles_by_member():
assert not EnvironmentRoles.get(user.id, testing.id) assert not EnvironmentRoles.get(user.id, testing.id)
def test_get_members_by_role(db):
environment = EnvironmentFactory.create()
env_role_1 = EnvironmentRoleFactory.create(
environment=environment, role=CSPRole.BASIC_ACCESS.value
)
env_role_2 = EnvironmentRoleFactory.create(
environment=environment, role=CSPRole.TECHNICAL_READ.value
)
env_role_3 = EnvironmentRoleFactory.create(
environment=environment, role=CSPRole.TECHNICAL_READ.value
)
rando_env = EnvironmentFactory.create()
rando_env_role = EnvironmentRoleFactory.create(
environment=rando_env, role=CSPRole.BASIC_ACCESS.value
)
basic_access_members = Environments.get_members_by_role(
environment, CSPRole.BASIC_ACCESS.value
)
technical_read_members = Environments.get_members_by_role(
environment, CSPRole.TECHNICAL_READ.value
)
assert basic_access_members == [env_role_1]
assert rando_env_role not in basic_access_members
assert technical_read_members == [env_role_2, env_role_3]
assert (
Environments.get_members_by_role(environment, CSPRole.BUSINESS_READ.value) == []
)
def test_get_scoped_environments(db): def test_get_scoped_environments(db):
developer = UserFactory.create() developer = UserFactory.create()
portfolio = PortfolioFactory.create( portfolio = PortfolioFactory.create(

View File

@ -22,7 +22,7 @@ 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 EnvironmentRolesForm from atst.forms.app_settings import AppEnvRolesForm
from tests.utils import captured_templates from tests.utils import captured_templates
@ -48,6 +48,8 @@ def test_updating_application_environments_success(client, user_session):
_external=True, _external=True,
fragment="application-environments", fragment="application-environments",
_anchor="application-environments", _anchor="application-environments",
active_toggler=environment.id,
active_toggler_section="edit",
) )
assert environment.name == "new name a" assert environment.name == "new name a"
@ -116,23 +118,88 @@ def test_edit_application_environments_obj(app, client, user_session):
assert response.status_code == 200 assert response.status_code == 200
_, context = templates[0] _, context = templates[0]
assert isinstance(context["members_form"], AppEnvRolesForm)
env_obj = context["environments_obj"][0] env_obj = context["environments_obj"][0]
assert env_obj["name"] == env.name assert env_obj["name"] == env.name
assert env_obj["id"] == env.id assert env_obj["id"] == env.id
assert isinstance(env_obj["edit_form"], EditEnvironmentForm) assert isinstance(env_obj["edit_form"], EditEnvironmentForm)
assert isinstance(env_obj["members_form"], EnvironmentRolesForm) assert (
assert env_obj["members"] == { env_obj["members"].sort()
"no_access": [ == [env_role1.user.full_name, env_role2.user.full_name].sort()
{"name": app_role.user.full_name, "user_id": app_role.user_id} )
def test_data_for_app_env_roles_form(app, client, user_session):
portfolio = PortfolioFactory.create()
application = Applications.create(
portfolio,
"Snazzy Application",
"A new application for me and my friends",
{"env"},
)
env = application.environments[0]
app_role = ApplicationRoleFactory.create(application=application)
env_role1 = EnvironmentRoleFactory.create(
environment=env, role=CSPRole.BASIC_ACCESS.value
)
ApplicationRoleFactory.create(application=application, user=env_role1.user)
env_role2 = EnvironmentRoleFactory.create(
environment=env, role=CSPRole.NETWORK_ADMIN.value
)
ApplicationRoleFactory.create(application=application, user=env_role2.user)
user_session(portfolio.owner)
with captured_templates(app) as templates:
response = app.test_client().get(
url_for("applications.settings", application_id=application.id)
)
assert response.status_code == 200
_, context = templates[0]
members_form = context["members_form"]
assert isinstance(members_form, AppEnvRolesForm)
assert members_form.data == {
"envs": [
{
"env_id": env.id,
"team_roles": [
{
"role": "no_access",
"members": [
{
"user_id": str(app_role.user_id),
"user_name": app_role.user.full_name,
"role_name": None,
}
], ],
CSPRole.BASIC_ACCESS.value: [ },
{"name": env_role1.user.full_name, "user_id": env_role1.user_id} {
"role": CSPRole.BASIC_ACCESS.value,
"members": [
{
"user_id": str(env_role1.user_id),
"user_name": env_role1.user.full_name,
"role_name": CSPRole.BASIC_ACCESS.value,
}
], ],
CSPRole.NETWORK_ADMIN.value: [ },
{"name": env_role2.user.full_name, "user_id": env_role2.user_id} {
"role": CSPRole.NETWORK_ADMIN.value,
"members": [
{
"user_id": str(env_role2.user_id),
"user_name": env_role2.user.full_name,
"role_name": CSPRole.NETWORK_ADMIN.value,
}
], ],
CSPRole.BUSINESS_READ.value: [], },
CSPRole.TECHNICAL_READ.value: [], {"role": CSPRole.BUSINESS_READ.value, "members": []},
{"role": CSPRole.TECHNICAL_READ.value, "members": []},
],
}
]
} }
@ -234,19 +301,15 @@ def test_update_team_env_roles(client, user_session):
app_role = ApplicationRoleFactory.create(application=application) app_role = ApplicationRoleFactory.create(application=application)
form_data = { form_data = {
"env_id": environment.id, "envs-0-env_id": environment.id,
"team_roles-0-user_id": env_role_1.user.id, "envs-0-team_roles-0-members-0-user_id": app_role.user.id,
"team_roles-0-name": env_role_1.user.full_name, "envs-0-team_roles-0-members-0-role_name": CSPRole.TECHNICAL_READ.value,
"team_roles-0-role": CSPRole.NETWORK_ADMIN.value, "envs-0-team_roles-1-members-0-user_id": env_role_1.user.id,
"team_roles-1-user_id": env_role_2.user.id, "envs-0-team_roles-1-members-0-role_name": CSPRole.NETWORK_ADMIN.value,
"team_roles-1-name": env_role_2.user.full_name, "envs-0-team_roles-1-members-1-user_id": env_role_2.user.id,
"team_roles-1-role": CSPRole.BASIC_ACCESS.value, "envs-0-team_roles-1-members-1-role_name": CSPRole.BASIC_ACCESS.value,
"team_roles-2-user_id": env_role_3.user.id, "envs-0-team_roles-1-members-2-user_id": env_role_3.user.id,
"team_roles-2-name": env_role_3.user.full_name, "envs-0-team_roles-1-members-2-role_name": "no_access",
"team_roles-2-role": "",
"team_roles-3-user_id": app_role.user.id,
"team_roles-3-name": app_role.user.full_name,
"team_roles-3-role": CSPRole.TECHNICAL_READ.value,
} }
user_session(application.portfolio.owner) user_session(application.portfolio.owner)

View File

@ -333,6 +333,11 @@ fragments:
new_application_title: Add a new application new_application_title: Add a new application
edit_environment_team_form: edit_environment_team_form:
delete_environment_title: Are you sure you want to delete this environment? delete_environment_title: Are you sure you want to delete this environment?
add_new_member_text: Need to add someone new to the team?
add_new_member_link: Jump to Team Settings
unassigned_title: Unassigned (No Access)
no_members: Currently no members are in this role
no_access: No Access
edit_user_form: edit_user_form:
date_last_training_tooltip: When was the last time you completed the IA training? <br> Information Assurance (IA) training is an important step in cyber awareness. date_last_training_tooltip: When was the last time you completed the IA training? <br> Information Assurance (IA) training is an important step in cyber awareness.
save_details_button: Save save_details_button: Save