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)
for member in team_roles:
new_role = member["role"]
new_role = member["role_name"]
user = Users.get(member["user_id"])
Environments.update_env_role(
environment=environment, user=user, new_role=new_role
@ -113,6 +113,15 @@ class Environments(object):
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
def revoke_access(cls, environment, target_user):
EnvironmentRoles.delete(environment.id, target_user.id)

View File

@ -1,16 +1,32 @@
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 .data import ENV_ROLES
class EnvMemberRoleForm(FlaskForm):
name = StringField()
class MemberForm(FlaskForm):
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):
team_roles = FieldList(FormField(EnvMemberRoleForm))
class RoleForm(FlaskForm):
role = HiddenField()
members = FieldList(FormField(MemberForm))
class EnvironmentRolesForm(FlaskForm):
team_roles = FieldList(FormField(RoleForm))
env_id = HiddenField()
class AppEnvRolesForm(BaseForm):
envs = FieldList(FormField(EnvironmentRolesForm))

View File

@ -217,4 +217,6 @@ REQUIRED_DISTRIBUTIONS = [
("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
def users(self):
return [r.user for r in self.roles]
return {r.user for r in self.roles}
@property
def num_users(self):

View File

@ -1,10 +1,9 @@
from flask import redirect, render_template, request as http_request, url_for
from . import applications_bp
from atst.domain.environment_roles import EnvironmentRoles
from atst.domain.environments import Environments
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.domain.authz.decorator import user_can_access_decorator as user_can
from atst.models.environment_role import CSPRole
@ -20,37 +19,59 @@ def get_environments_obj_for_app(application):
"id": env.id,
"name": env.name,
"edit_form": EditEnvironmentForm(obj=env),
"members_form": EnvironmentRolesForm(data=data_for_env_members_form(env)),
"members": sort_env_users_by_role(env),
"member_count": len(env.users),
"members": [user.full_name for user in env.users],
}
environments_obj.append(env_data)
return environments_obj
def sort_env_users_by_role(env):
users_dict = {"no_access": []}
for role in CSPRole:
users_dict[role.value] = []
def serialize_members(member_list, role):
serialized_list = []
for user in env.application.users:
if user in env.users:
role = EnvironmentRoles.get(user.id, env.id)
users_dict[role.displayname].append(
{"name": user.full_name, "user_id": user.id}
for member in member_list:
serialized_list.append(
{
"user_id": str(member.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):
data = {"env_id": environment.id, "team_roles": []}
for user in environment.users:
env_role = EnvironmentRoles.get(user.id, environment.id)
data["team_roles"].append(
{"name": user.full_name, "user_id": user.id, "role": env_role.displayname}
def sort_env_users_by_role(env):
users_list = []
no_access_users = env.application.users - env.users
no_access_list = [
{"user_id": str(user.id), "user_name": user.full_name, "role_name": "no_access"}
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
@ -67,15 +88,19 @@ def check_users_are_in_application(user_ids, application):
@applications_bp.route("/applications/<application_id>/settings")
@user_can(Permissions.VIEW_APPLICATION, message="view application edit form")
def settings(application_id):
# refactor like portfolio admin render function
application = Applications.get(application_id)
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(
"portfolios/applications/settings.html",
application=application,
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,
fragment="application-environments",
_anchor="application-environments",
active_toggler=environment.id,
active_toggler_section="edit",
)
)
else:
@ -109,6 +136,11 @@ def update_environment(environment_id):
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)
),
active_toggler=environment.id,
active_toggler_section="edit",
),
400,
)
@ -143,13 +175,17 @@ def update(application_id):
def update_env_roles(environment_id):
environment = Environments.get(environment_id)
application = environment.application
env_roles_form = EnvironmentRolesForm(http_request.form)
if env_roles_form.validate():
form = AppEnvRolesForm(formdata=http_request.form)
if form.validate():
env_data = []
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)
env_data = env_data + role["members"]
except NotFoundError as err:
app.logger.warning(
"User {} requested environment role change for unauthorized user {} in application {}.".format(
@ -159,22 +195,36 @@ def update_env_roles(environment_id):
)
raise (err)
env_data = env_roles_form.data
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:
# TODO: Create a better pattern to handle when a form doesn't validate
# if a user is submitting the data via the web page then they
# should never have any form validation errors
return render_template(
return (
render_template(
"portfolios/applications/settings.html",
application=application,
form=ApplicationForm(
name=application.name, description=application.description
),
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',
"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": {
"title_template": "Application environments 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 textinput from '../text_input'
import Selector from '../selector'
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 {
name: 'edit-environment-role',
mixins: [FormMixin, Modal],
components: {
toggler,
Modal,
Selector,
textinput,
},
mixins: [FormMixin],
props: {
choices: Array,
initialData: String,
applicationId: String,
initialRoleCategories: Array,
},
data: function() {
return {
new_role: this.initialData,
selectedSection: null,
roleCategories: this.sanitizeValues(this.initialRoleCategories),
}
},
mounted: function() {
this.$root.$on('revoke-' + this.applicationId, this.revoke)
},
methods: {
change: function(e) {
this.new_role = e.target.value
sanitizeValues: function(roles) {
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: {
displayName: function() {
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
},
render: function(createElement) {
return createElement('p', 'Please implement inline-template')
},
}

View File

@ -1,3 +1,4 @@
import editEnvironmentRole from './forms/edit_environment_role'
import FormMixin from '../mixins/form'
import optionsinput from './options_input'
import textinput from './text_input'
@ -7,7 +8,16 @@ export default {
mixins: [FormMixin],
props: {
initialSelectedSection: {
type: String,
default: null,
},
},
components: {
editEnvironmentRole,
optionsinput,
textinput,
optionsinput,
},

View File

@ -25,11 +25,14 @@
.app-team-settings-link {
font-size: $small-font-size;
font-weight: $font-normal;
padding-left: $gap * 2;
}
.environment-roles {
padding: 0 ($gap * 3) ($gap * 3);
}
.environment-role {
padding: $gap * 3;
padding: ($gap * 2) 0;
h4 {
margin-bottom: $gap / 4;
@ -50,16 +53,43 @@
margin: $gap;
white-space: nowrap;
width: 20rem;
position: relative;
height: 3.6rem;
&.unassigned {
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 {
margin: $gap;
padding: ($gap / 2) $gap;
font-weight: $font-normal;
height: 3.6rem;
}
}
}
@ -94,3 +124,19 @@
font-weight: $font-normal;
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>
{% endmacro %}
{% macro ToggleSection(section_name) %}
<div v-show="selectedSection === '{{ section_name }}'">
{% macro ToggleSection(section_name, classes="") %}
<div v-show="selectedSection === '{{ section_name }}'" class='{{ classes }}'>
{{ caller() }}
</div>
{% 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/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 %}
{% 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">
<header>
@ -50,13 +30,10 @@
<ul class="accordion-table__items">
{% 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 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">
<div class="accordion-table__item-content row">
<div class="col col--grow">
@ -84,11 +61,11 @@
</div>
<div class="col col--grow icon-link icon-link--large accordion-table__item__toggler">
{% set open_members_button %}
{{ "common.members" | translate }} ({{ member_count }}) {{ Icon('caret_down') }}
{{ "common.members" | translate }} ({{ env['member_count'] }}) {{ Icon('caret_down') }}
{% endset %}
{% set close_members_button %}
{{ "common.members" | translate }} ({{ member_count }}) {{ Icon('caret_up') }}
{{ "common.members" | translate }} ({{ env['member_count'] }}) {{ Icon('caret_up') }}
{% endset %}
{{
@ -101,11 +78,8 @@
</div>
</div>
{% call ToggleSection(section_name="members") %}
<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>
{% for role, members in members_by_role.items() %}
{{ RolePanel(users=members, role=role) }}
{% endfor %}
{% call ToggleSection(section_name="members", classes="environment-roles") %}
{% include 'fragments/applications/edit_environment_team_form.html' %}
{% endcall %}
{% call ToggleSection(section_name="edit") %}
@ -152,3 +126,11 @@
</ul>
</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">
{% set open_members_button %}
{{ "common.members" | translate }} ({{ env['members'] | length }}) {{ Icon('caret_down') }}
{{ "common.members" | translate }} ({{ env['member_count'] }}) {{ Icon('caret_down') }}
{% endset %}
{% set close_members_button %}
{{ "common.members" | translate }} ({{ env['members'] | length }}) {{ Icon('caret_up') }}
{{ "common.members" | translate }} ({{ env['member_count'] }}) {{ Icon('caret_up') }}
{% endset %}
{{
@ -47,7 +47,7 @@
<ul>
{% for member in env['members'] %}
<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>
{% endfor %}
</ul>

View File

@ -68,15 +68,7 @@
{% if user_can(permissions.EDIT_APPLICATION) %}
{% 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) %}
{% include "fragments/applications/read_only_environments.html" %}
{% endif %}

View File

@ -79,18 +79,18 @@ def test_update_env_roles_by_environment():
team_roles = [
{
"user_id": env_role_1.user.id,
"name": env_role_1.user.full_name,
"role": CSPRole.BUSINESS_READ.value,
"user_name": env_role_1.user.full_name,
"role_name": CSPRole.BUSINESS_READ.value,
},
{
"user_id": env_role_2.user.id,
"name": env_role_2.user.full_name,
"role": CSPRole.NETWORK_ADMIN.value,
"user_name": env_role_2.user.full_name,
"role_name": CSPRole.NETWORK_ADMIN.value,
},
{
"user_id": env_role_3.user.id,
"name": env_role_3.user.full_name,
"role": None,
"user_name": env_role_3.user.full_name,
"role_name": None,
},
]
@ -136,6 +136,36 @@ def test_update_env_roles_by_member():
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):
developer = UserFactory.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.portfolio_role import Status as PortfolioRoleStatus
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
@ -48,6 +48,8 @@ def test_updating_application_environments_success(client, user_session):
_external=True,
fragment="application-environments",
_anchor="application-environments",
active_toggler=environment.id,
active_toggler_section="edit",
)
assert environment.name == "new name a"
@ -116,23 +118,88 @@ def test_edit_application_environments_obj(app, client, user_session):
assert response.status_code == 200
_, context = templates[0]
assert isinstance(context["members_form"], AppEnvRolesForm)
env_obj = context["environments_obj"][0]
assert env_obj["name"] == env.name
assert env_obj["id"] == env.id
assert isinstance(env_obj["edit_form"], EditEnvironmentForm)
assert isinstance(env_obj["members_form"], EnvironmentRolesForm)
assert env_obj["members"] == {
"no_access": [
{"name": app_role.user.full_name, "user_id": app_role.user_id}
assert (
env_obj["members"].sort()
== [env_role1.user.full_name, env_role2.user.full_name].sort()
)
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)
form_data = {
"env_id": environment.id,
"team_roles-0-user_id": env_role_1.user.id,
"team_roles-0-name": env_role_1.user.full_name,
"team_roles-0-role": CSPRole.NETWORK_ADMIN.value,
"team_roles-1-user_id": env_role_2.user.id,
"team_roles-1-name": env_role_2.user.full_name,
"team_roles-1-role": CSPRole.BASIC_ACCESS.value,
"team_roles-2-user_id": env_role_3.user.id,
"team_roles-2-name": env_role_3.user.full_name,
"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,
"envs-0-env_id": environment.id,
"envs-0-team_roles-0-members-0-user_id": app_role.user.id,
"envs-0-team_roles-0-members-0-role_name": CSPRole.TECHNICAL_READ.value,
"envs-0-team_roles-1-members-0-user_id": env_role_1.user.id,
"envs-0-team_roles-1-members-0-role_name": CSPRole.NETWORK_ADMIN.value,
"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-2-user_id": env_role_3.user.id,
"envs-0-team_roles-1-members-2-role_name": "no_access",
}
user_session(application.portfolio.owner)

View File

@ -333,6 +333,11 @@ fragments:
new_application_title: Add a new application
edit_environment_team_form:
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:
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