diff --git a/atst/domain/environments.py b/atst/domain/environments.py index 9c8e4d52..515c7557 100644 --- a/atst/domain/environments.py +++ b/atst/domain/environments.py @@ -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) diff --git a/atst/forms/app_settings.py b/atst/forms/app_settings.py index 44bcbc8e..20e1aa25 100644 --- a/atst/forms/app_settings.py +++ b/atst/forms/app_settings.py @@ -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)) diff --git a/atst/forms/data.py b/atst/forms/data.py index 2e0b689d..2ddc3124 100644 --- a/atst/forms/data.py +++ b/atst/forms/data.py @@ -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") +] diff --git a/atst/models/environment.py b/atst/models/environment.py index 410d9e91..1817517b 100644 --- a/atst/models/environment.py +++ b/atst/models/environment.py @@ -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): diff --git a/atst/routes/applications/settings.py b/atst/routes/applications/settings.py index 21ea67a8..586e6d3d 100644 --- a/atst/routes/applications/settings.py +++ b/atst/routes/applications/settings.py @@ -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 serialize_members(member_list, role): + serialized_list = [] + + for member in member_list: + serialized_list.append( + { + "user_id": str(member.user_id), + "user_name": member.user.full_name, + "role_name": role, + } + ) + + return serialized_list + + def sort_env_users_by_role(env): - users_dict = {"no_access": []} + 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_dict[role.value] = [] + users_list.append( + { + "role": role.value, + "members": serialize_members( + Environments.get_members_by_role(env, role.value), role.value + ), + } + ) - 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} - ) - else: - users_dict["no_access"].append({"name": user.full_name, "user_id": user.id}) - - return users_dict + return users_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 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//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"]] - check_users_are_in_application(user_ids, application) + 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( - "portfolios/applications/settings.html", - application=application, - form=ApplicationForm( - name=application.name, description=application.description + 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", ), - environments_obj=get_environments_obj_for_app(application=application), + 400, ) diff --git a/atst/utils/flash.py b/atst/utils/flash.py index f2b07ed6..c411e22e 100644 --- a/atst/utils/flash.py +++ b/atst/utils/flash.py @@ -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", diff --git a/js/components/__tests__/edit_environment_role.test.js b/js/components/__tests__/edit_environment_role.test.js new file mode 100644 index 00000000..4421d58c --- /dev/null +++ b/js/components/__tests__/edit_environment_role.test.js @@ -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) + }) +}) diff --git a/js/components/forms/edit_environment_role.js b/js/components/forms/edit_environment_role.js index e9919f29..cbb09457 100644 --- a/js/components/forms/edit_environment_role.js +++ b/js/components/forms/edit_environment_role.js @@ -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 - }, - revoke: function() { - this.new_role = '' - }, - }, - 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 + checkNoAccess: function(role) { + return role === 'no_access' + }, + + 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 + } } } }, - label_class: function() { - return this.newRole === '' ? 'label' : 'label label--success' + + 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 = [] + } + } }, - newRole: function() { - return this.new_role + + 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() }, }, + + render: function(createElement) { + return createElement('p', 'Please implement inline-template') + }, } diff --git a/js/components/toggler.js b/js/components/toggler.js index f1c34c96..e433b294 100644 --- a/js/components/toggler.js +++ b/js/components/toggler.js @@ -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, }, diff --git a/styles/sections/_application_edit.scss b/styles/sections/_application_edit.scss index a24d9373..0795fc6c 100644 --- a/styles/sections/_application_edit.scss +++ b/styles/sections/_application_edit.scss @@ -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); + } + } +} diff --git a/templates/components/toggle_list.html b/templates/components/toggle_list.html index 08526c5a..bb16d87e 100644 --- a/templates/components/toggle_list.html +++ b/templates/components/toggle_list.html @@ -9,8 +9,8 @@ {% endmacro %} -{% macro ToggleSection(section_name) %} -
+{% macro ToggleSection(section_name, classes="") %} +
{{ caller() }}
{% endmacro %} diff --git a/templates/fragments/applications/edit_environment_team_form.html b/templates/fragments/applications/edit_environment_team_form.html new file mode 100644 index 00000000..393fa4f9 --- /dev/null +++ b/templates/fragments/applications/edit_environment_team_form.html @@ -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'] %} + +
+ {{ members_form.csrf_token }} + {{ env_form.env_id() }} + +
+
+

+ {{ 'fragments.edit_environment_team_form.unassigned_title' | translate }} +

+

+
    +
    + {{ 'fragments.edit_environment_team_form.no_members' | translate }} +
    +
  • + + + + {{ Icon('edit', classes="icon--medium") }} + +
    +
    +
    +
      +
    • + + +
    • +
    +
    +
    +
    + +
  • +
+
+
+ {{ + SaveButton( + text=("portfolios.applications.update_button_text" | translate) + ) + }} +
+
+
+ +
+ {% endif %} +{% endfor %} diff --git a/templates/fragments/applications/edit_environments.html b/templates/fragments/applications/edit_environments.html index f6dc4912..7852ae7d 100644 --- a/templates/fragments/applications/edit_environments.html +++ b/templates/fragments/applications/edit_environments.html @@ -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 %} - -
-

{{ role }}

-
    - {% for user in users %} -
  • - {{ user.name }}{{ Icon('edit', classes="icon--medium right") }} -
  • - {% endfor %} - - {% if users == [] %} -
    Currently no members are in this role
    - {% endif %} -
-
-{% endmacro %}
@@ -50,13 +30,10 @@
    {% 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'] %} - +
  • @@ -65,15 +42,15 @@
    {% set edit_environment_button %} - {{ Icon('edit') }} + {{ Icon('edit') }} {% endset %} {{ ToggleButton( - open_html=edit_environment_button, - close_html=edit_environment_button, - section_name="edit" - ) + open_html=edit_environment_button, + close_html=edit_environment_button, + section_name="edit" + ) }}
    @@ -84,28 +61,25 @@
    - {% call ToggleSection(section_name="members") %} - - {% 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 @@
+ diff --git a/templates/fragments/applications/read_only_environments.html b/templates/fragments/applications/read_only_environments.html index 4a91fb3f..90181a2d 100644 --- a/templates/fragments/applications/read_only_environments.html +++ b/templates/fragments/applications/read_only_environments.html @@ -26,11 +26,11 @@ {% 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 @@ diff --git a/templates/portfolios/applications/settings.html b/templates/portfolios/applications/settings.html index 7de3e1b5..efa44f0f 100644 --- a/templates/portfolios/applications/settings.html +++ b/templates/portfolios/applications/settings.html @@ -68,15 +68,7 @@ {% if user_can(permissions.EDIT_APPLICATION) %} {% include "fragments/applications/edit_environments.html" %} - + {% elif user_can(permissions.VIEW_ENVIRONMENT) %} {% include "fragments/applications/read_only_environments.html" %} {% endif %} diff --git a/tests/domain/test_environments.py b/tests/domain/test_environments.py index 1db1b437..577c5b93 100644 --- a/tests/domain/test_environments.py +++ b/tests/domain/test_environments.py @@ -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( diff --git a/tests/routes/applications/test_settings.py b/tests/routes/applications/test_settings.py index d335e4bb..1e5bfb02 100644 --- a/tests/routes/applications/test_settings.py +++ b/tests/routes/applications/test_settings.py @@ -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} - ], - CSPRole.BASIC_ACCESS.value: [ - {"name": env_role1.user.full_name, "user_id": env_role1.user_id} - ], - CSPRole.NETWORK_ADMIN.value: [ - {"name": env_role2.user.full_name, "user_id": env_role2.user_id} - ], - CSPRole.BUSINESS_READ.value: [], - CSPRole.TECHNICAL_READ.value: [], + 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, + } + ], + }, + { + "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, + } + ], + }, + { + "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, + } + ], + }, + {"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) diff --git a/translations.yaml b/translations.yaml index 15f39fa8..449bd585 100644 --- a/translations.yaml +++ b/translations.yaml @@ -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?
Information Assurance (IA) training is an important step in cyber awareness. save_details_button: Save