From 50ceaa39de666cf473e0c535b20e2fbeafb0230e Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Thu, 5 Sep 2019 16:09:26 -0400 Subject: [PATCH 01/12] Move team table into app settings page --- atst/routes/applications/settings.py | 59 ++++++++ .../portfolios/applications/settings.html | 143 ++++++++++++++++++ 2 files changed, 202 insertions(+) diff --git a/atst/routes/applications/settings.py b/atst/routes/applications/settings.py index 9e748651..fa4616a8 100644 --- a/atst/routes/applications/settings.py +++ b/atst/routes/applications/settings.py @@ -5,12 +5,16 @@ from atst.domain.environments import Environments from atst.domain.applications import Applications from atst.domain.audit_log import AuditLog from atst.domain.common import Paginator +from atst.domain.environment_roles import EnvironmentRoles from atst.forms.app_settings import AppEnvRolesForm from atst.forms.application import ApplicationForm, EditEnvironmentForm +from atst.forms.application_member import NewForm as NewMemberForm from atst.forms.data import ENV_ROLE_NO_ACCESS as NO_ACCESS +from atst.forms.team import TeamForm from atst.domain.authz.decorator import user_can_access_decorator as user_can from atst.models.environment_role import CSPRole from atst.models.permissions import Permissions +from atst.domain.permission_sets import PermissionSets from atst.utils.flash import formatted_flash as flash @@ -79,12 +83,65 @@ def data_for_app_env_roles_form(application): return {"envs": nested_data} +def get_form_permission_value(member, edit_perm_set): + if member.has_permission_set(edit_perm_set): + return edit_perm_set + else: + return PermissionSets.VIEW_APPLICATION + + +def get_team_form(application): + team_data = [] + for member in application.members: + permission_sets = { + "perms_team_mgmt": get_form_permission_value( + member, PermissionSets.EDIT_APPLICATION_TEAM + ), + "perms_env_mgmt": get_form_permission_value( + member, PermissionSets.EDIT_APPLICATION_ENVIRONMENTS + ), + "perms_del_env": get_form_permission_value( + member, PermissionSets.DELETE_APPLICATION_ENVIRONMENTS + ), + } + roles = EnvironmentRoles.get_for_application_member(member.id) + environment_roles = [ + { + "environment_id": str(role.environment.id), + "environment_name": role.environment.name, + "role": role.role, + } + for role in roles + ] + team_data.append( + { + "role_id": member.id, + "user_name": member.user_name, + "permission_sets": permission_sets, + "environment_roles": environment_roles, + } + ) + + return TeamForm(data={"members": team_data}) + + +def get_new_member_form(application): + env_roles = [ + {"environment_id": e.id, "environment_name": e.name} + for e in application.environments + ] + + return NewMemberForm(data={"environment_roles": env_roles}) + + def render_settings_page(application, **kwargs): environments_obj = get_environments_obj_for_app(application=application) members_form = AppEnvRolesForm(data=data_for_app_env_roles_form(application)) new_env_form = EditEnvironmentForm() pagination_opts = Paginator.get_pagination_opts(http_request) audit_events = AuditLog.get_application_events(application, pagination_opts) + team_form = get_team_form(application) + new_member_form = get_new_member_form(application) if "application_form" not in kwargs: kwargs["application_form"] = ApplicationForm( @@ -98,6 +155,8 @@ def render_settings_page(application, **kwargs): members_form=members_form, new_env_form=new_env_form, audit_events=audit_events, + team_form=team_form, + new_member_form=new_member_form, **kwargs, ) diff --git a/templates/portfolios/applications/settings.html b/templates/portfolios/applications/settings.html index f715ea8e..939e0063 100644 --- a/templates/portfolios/applications/settings.html +++ b/templates/portfolios/applications/settings.html @@ -3,7 +3,9 @@ {% from "components/alert.html" import Alert %} {% from "components/delete_confirmation.html" import DeleteConfirmation %} {% from "components/icon.html" import Icon %} +{% import "fragments/applications/new_member_modal_content.html" as member_steps %} {% from "components/modal.html" import Modal %} +{% from "components/multi_step_modal_form.html" import MultiStepModalForm %} {% from "components/pagination.html" import Pagination %} {% from "components/save_button.html" import SaveButton %} {% from "components/text_input.html" import TextInput %} @@ -81,6 +83,147 @@ {% endif %} + {% if not application.members %} + {% set user_can_invite = user_can(permissions.CREATE_APPLICATION_MEMBER) %} + +
+

{{ ("portfolios.applications.team_settings.blank_slate.title" | translate) }}

+ + {{ Icon('avatar') }} + + {% if not user_can_invite %} +

{{ ("portfolios.applications.team_settings.blank_slate.sub_message" | translate) }}

+ {% endif %} + + {% if user_can_invite %} + {% set new_member_modal_name = "add-app-mem" %} + + {{ "portfolios.applications.team_settings.blank_slate.action_label" | translate }} + + {{ MultiStepModalForm( + name=new_member_modal_name, + form=new_member_form, + form_action=url_for("applications.create_member", application_id=application.id), + steps=[ + member_steps.MemberStepOne(new_member_form), + member_steps.MemberStepTwo(new_member_form, application) + ], + ) }} + {% endif %} +
+ + {% else %} +
+ {{ 'portfolios.applications.team_settings.subheading' | translate }} +
+ +
+ +
+
+ {% if g.matchesPath("application-members") %} + {% include "fragments/flash.html" %} + {% endif %} +
+
+
+
+ {{ "portfolios.applications.team_settings.section.title" | translate({ "application_name": application.name }) }} +

Members ({{ team_form.members | length }})

+
+
+
+
+ +
+
+
+ {{ "common.name" | translate }} +
+
+ {{ "portfolios.applications.team_settings.section.table.team_management" | translate }} +
+
+ {{ "portfolios.applications.team_settings.section.table.environment_management" | translate }} +
+
+ {{ "portfolios.applications.team_settings.section.table.delete_access" | translate }} +
+
+   +
+
+
    + {% if user_can(permissions.EDIT_APPLICATION_MEMBER) %} + {% include "fragments/applications/edit_team.html" %} + {% elif user_can(permissions.VIEW_APPLICATION_MEMBER) %} + {% include "fragments/applications/read_only_team.html" %} + {% endif %} +
+
+ + +
+
+
+ + {% if user_can(permissions.DELETE_APPLICATION_MEMBER) %} + {% for member_form in team_form.members %} + {% set delete_modal_id = "delete-user-{}".format(member_form.id) %} + {% call Modal(name=delete_modal_id) %} +

+ {{ "portfolios.applications.remove_member.header" | translate }} +

+ + {{ + Alert( + title=("components.modal.destructive_title" | translate), + message=("portfolios.applications.remove_member.alert.message" | translate({"user_name": member_form.user_name.data})), + level="warning" + ) + }} + + {{ + DeleteConfirmation( + modal_id=delete_modal_id, + delete_text=('portfolios.applications.remove_member.button' | translate), + delete_action=url_for('applications.remove_member', application_id=application.id, application_role_id=member_form.data.role_id), + form=member_form + ) + }} + {% endcall %} + {% endfor %} + {% endif %} + + {% if user_can(permissions.CREATE_APPLICATION_MEMBER) %} + {% import "fragments/applications/new_member_modal_content.html" as member_steps %} + {{ MultiStepModalForm( + name=new_member_modal_name, + form=new_member_form, + form_action=url_for("applications.create_member", application_id=application.id), + steps=[ + member_steps.MemberStepOne(new_member_form), + member_steps.MemberStepTwo(new_member_form, application) + ], + ) }} + {% endif %} +
+ {% endif %} +
{% if g.matchesPath("application-environments") %} From de74c1f5335d1f7a5475e6b0f344883596253eab Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Thu, 5 Sep 2019 16:18:47 -0400 Subject: [PATCH 02/12] Remove route for applications.team --- atst/routes/applications/team.py | 13 +++---------- styles/sections/_application_edit.scss | 5 ----- .../applications/edit_environment_team_form.html | 6 ------ templates/portfolios/applications/index.html | 6 ------ tests/routes/applications/test_team.py | 16 +++------------- tests/test_access.py | 14 -------------- 6 files changed, 6 insertions(+), 54 deletions(-) diff --git a/atst/routes/applications/team.py b/atst/routes/applications/team.py index be151916..03c489c3 100644 --- a/atst/routes/applications/team.py +++ b/atst/routes/applications/team.py @@ -80,13 +80,6 @@ def render_team_page(application): ) -@applications_bp.route("/applications//team") -@user_can(Permissions.VIEW_APPLICATION, message="view portfolio applications") -def team(application_id): - application = Applications.get(resource_id=application_id) - return render_team_page(application) - - @applications_bp.route("/application//team", methods=["POST"]) @user_can(Permissions.EDIT_APPLICATION_MEMBER, message="update application member") def update_team(application_id): @@ -115,7 +108,7 @@ def update_team(application_id): return redirect( url_for( - "applications.team", + "applications.settings", application_id=application_id, fragment="application-members", _anchor="application-members", @@ -172,7 +165,7 @@ def create_member(application_id): return redirect( url_for( - "applications.team", + "applications.settings", application_id=application_id, fragment="application-members", _anchor="application-members", @@ -197,7 +190,7 @@ def remove_member(application_id, application_role_id): return redirect( url_for( - "applications.team", + "applications.settings", _anchor="application-members", application_id=g.application.id, fragment="application-members", diff --git a/styles/sections/_application_edit.scss b/styles/sections/_application_edit.scss index 985b6956..89a56396 100644 --- a/styles/sections/_application_edit.scss +++ b/styles/sections/_application_edit.scss @@ -34,11 +34,6 @@ } } -.app-team-settings-link { - font-size: $small-font-size; - font-weight: $font-normal; -} - .environment-roles { padding: 0 ($gap * 3) ($gap * 3); diff --git a/templates/fragments/applications/edit_environment_team_form.html b/templates/fragments/applications/edit_environment_team_form.html index 3703b86a..88d55f47 100644 --- a/templates/fragments/applications/edit_environment_team_form.html +++ b/templates/fragments/applications/edit_environment_team_form.html @@ -4,12 +4,6 @@ {% for env_form in members_form.envs %} {% if env_form.env_id.data == env['id'] %} -
diff --git a/templates/portfolios/applications/index.html b/templates/portfolios/applications/index.html index 778cc40b..873078f5 100644 --- a/templates/portfolios/applications/index.html +++ b/templates/portfolios/applications/index.html @@ -51,12 +51,6 @@ {{ "portfolios.applications.app_settings_text" | translate }}
- - {{ "portfolios.applications.team_text" | translate }} ({{ application.members | length }}) - -
{% set has_environments = 0 < (application.environments|length) %} Environments ({{ application.environments|length }}) diff --git a/tests/routes/applications/test_team.py b/tests/routes/applications/test_team.py index 2ba80a9e..79d5e5d0 100644 --- a/tests/routes/applications/test_team.py +++ b/tests/routes/applications/test_team.py @@ -10,16 +10,6 @@ from atst.forms.data import ENV_ROLE_NO_ACCESS as NO_ACCESS from tests.factories import * -def test_application_team(client, user_session): - portfolio = PortfolioFactory.create() - application = ApplicationFactory.create(portfolio=portfolio) - - user_session(portfolio.owner) - - response = client.get(url_for("applications.team", application_id=application.id)) - assert response.status_code == 200 - - def test_update_team_permissions(client, user_session): application = ApplicationFactory.create() owner = application.portfolio.owner @@ -183,7 +173,7 @@ def test_create_member(monkeypatch, client, user_session, session): assert response.status_code == 302 expected_url = url_for( - "applications.team", + "applications.settings", application_id=application.id, fragment="application-members", _anchor="application-members", @@ -220,7 +210,7 @@ def test_remove_member_success(client, user_session): assert response.status_code == 302 assert response.location == url_for( - "applications.team", + "applications.settings", _anchor="application-members", _external=True, application_id=application.id, @@ -244,7 +234,7 @@ def test_remove_new_member_success(client, user_session): assert response.status_code == 302 assert response.location == url_for( - "applications.team", + "applications.settings", _anchor="application-members", _external=True, application_id=application.id, diff --git a/tests/test_access.py b/tests/test_access.py index 0d42c300..8a43b97a 100644 --- a/tests/test_access.py +++ b/tests/test_access.py @@ -585,20 +585,6 @@ def test_task_orders_new_post_routes(post_url_assert_status): post_url_assert_status(rando, url, 404, data=data) -def test_applications_application_team_access(get_url_assert_status): - ccpo = UserFactory.create_ccpo() - rando = UserFactory.create() - - portfolio = PortfolioFactory.create() - application = ApplicationFactory.create(portfolio=portfolio) - - url = url_for("applications.team", application_id=application.id) - - get_url_assert_status(ccpo, url, 200) - get_url_assert_status(portfolio.owner, url, 200) - get_url_assert_status(rando, url, 404) - - def test_portfolio_delete_access(post_url_assert_status): rando = UserFactory.create() owner = UserFactory.create() From 7c7624f25ee151cf21549ae7ec03046b7b5a17ac Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Thu, 5 Sep 2019 16:54:16 -0400 Subject: [PATCH 03/12] Move team routes to the settings file and delete files related to applications/team --- atst/routes/applications/__init__.py | 1 - atst/routes/applications/settings.py | 124 +++++++++- atst/routes/applications/team.py | 198 --------------- templates/portfolios/applications/team.html | 154 ------------ tests/routes/applications/test_settings.py | 251 +++++++++++++++++++ tests/routes/applications/test_team.py | 259 -------------------- 6 files changed, 374 insertions(+), 613 deletions(-) delete mode 100644 atst/routes/applications/team.py delete mode 100644 templates/portfolios/applications/team.html delete mode 100644 tests/routes/applications/test_team.py diff --git a/atst/routes/applications/__init__.py b/atst/routes/applications/__init__.py index 8ec1e4cf..6a98e188 100644 --- a/atst/routes/applications/__init__.py +++ b/atst/routes/applications/__init__.py @@ -5,7 +5,6 @@ applications_bp = Blueprint("applications", __name__) from . import index from . import new from . import settings -from . import team from . import invitations from atst.domain.environment_roles import EnvironmentRoles from atst.domain.exceptions import UnauthorizedError diff --git a/atst/routes/applications/settings.py b/atst/routes/applications/settings.py index fa4616a8..a0bb30d8 100644 --- a/atst/routes/applications/settings.py +++ b/atst/routes/applications/settings.py @@ -1,8 +1,10 @@ -from flask import redirect, render_template, request as http_request, url_for +from flask import redirect, render_template, request as http_request, url_for, g from . import applications_bp +from atst.domain.exceptions import AlreadyExistsError from atst.domain.environments import Environments from atst.domain.applications import Applications +from atst.domain.application_roles import ApplicationRoles from atst.domain.audit_log import AuditLog from atst.domain.common import Paginator from atst.domain.environment_roles import EnvironmentRoles @@ -16,6 +18,8 @@ from atst.models.environment_role import CSPRole from atst.models.permissions import Permissions from atst.domain.permission_sets import PermissionSets from atst.utils.flash import formatted_flash as flash +from atst.utils.localization import translate +from atst.jobs import send_mail def get_environments_obj_for_app(application): @@ -161,6 +165,17 @@ def render_settings_page(application, **kwargs): ) +def send_application_invitation(invitee_email, inviter_name, token): + body = render_template( + "emails/application/invitation.txt", owner=inviter_name, token=token + ) + send_mail.delay( + [invitee_email], + translate("email.application_invite", {"inviter_name": inviter_name}), + body, + ) + + @applications_bp.route("/applications//settings") @user_can(Permissions.VIEW_APPLICATION, message="view application edit form") def settings(application_id): @@ -323,3 +338,110 @@ def delete_environment(environment_id): fragment="application-environments", ) ) + + +@applications_bp.route("/application//team", methods=["POST"]) +@user_can(Permissions.EDIT_APPLICATION_MEMBER, message="update application member") +def update_team(application_id): + application = Applications.get(application_id) + form = TeamForm(http_request.form) + + if form.validate(): + for member_form in form.members: + app_role = ApplicationRoles.get_by_id(member_form.role_id.data) + new_perms = [ + perm + for perm in member_form.data["permission_sets"] + if perm != PermissionSets.VIEW_APPLICATION + ] + ApplicationRoles.update_permission_sets(app_role, new_perms) + + for environment_role_form in member_form.environment_roles: + environment = Environments.get( + environment_role_form.environment_id.data + ) + Environments.update_env_role( + environment, app_role, environment_role_form.data.get("role") + ) + + flash("updated_application_team_settings", application_name=application.name) + + return redirect( + url_for( + "applications.settings", + application_id=application_id, + fragment="application-members", + _anchor="application-members", + ) + ) + else: + return (render_settings_page(application), 400) + + +@applications_bp.route("/application//members/new", methods=["POST"]) +@user_can( + Permissions.CREATE_APPLICATION_MEMBER, message="create new application member" +) +def create_member(application_id): + application = Applications.get(application_id) + form = NewMemberForm(http_request.form) + + if form.validate(): + try: + invite = Applications.invite( + application=application, + inviter=g.current_user, + user_data=form.user_data.data, + permission_sets_names=form.permission_sets.data, + environment_roles_data=form.environment_roles.data, + ) + + send_application_invitation( + invitee_email=invite.email, + inviter_name=g.current_user.full_name, + token=invite.token, + ) + + flash("new_application_member", user_name=invite.user_name) + + except AlreadyExistsError: + return render_template( + "error.html", message="There was an error processing your request." + ) + else: + pass + # TODO: flash error message + + return redirect( + url_for( + "applications.settings", + application_id=application_id, + fragment="application-members", + _anchor="application-members", + ) + ) + + +@applications_bp.route( + "/applications//members//delete", + methods=["POST"], +) +@user_can(Permissions.DELETE_APPLICATION_MEMBER, message="remove application member") +def remove_member(application_id, application_role_id): + application_role = ApplicationRoles.get_by_id(application_role_id) + Applications.remove_member(application_role) + + flash( + "application_member_removed", + user_name=application_role.user_name, + application_name=g.application.name, + ) + + return redirect( + url_for( + "applications.settings", + _anchor="application-members", + application_id=g.application.id, + fragment="application-members", + ) + ) diff --git a/atst/routes/applications/team.py b/atst/routes/applications/team.py deleted file mode 100644 index 03c489c3..00000000 --- a/atst/routes/applications/team.py +++ /dev/null @@ -1,198 +0,0 @@ -from flask import render_template, request as http_request, g, url_for, redirect - - -from . import applications_bp -from atst.domain.applications import Applications -from atst.domain.application_roles import ApplicationRoles -from atst.domain.authz.decorator import user_can_access_decorator as user_can -from atst.domain.environment_roles import EnvironmentRoles -from atst.domain.environments import Environments -from atst.domain.exceptions import AlreadyExistsError -from atst.domain.permission_sets import PermissionSets -from atst.forms.application_member import NewForm as NewMemberForm -from atst.forms.team import TeamForm -from atst.models import Permissions -from atst.utils.flash import formatted_flash as flash -from atst.utils.localization import translate -from atst.jobs import send_mail - - -def get_form_permission_value(member, edit_perm_set): - if member.has_permission_set(edit_perm_set): - return edit_perm_set - else: - return PermissionSets.VIEW_APPLICATION - - -def get_team_form(application): - team_data = [] - for member in application.members: - permission_sets = { - "perms_team_mgmt": get_form_permission_value( - member, PermissionSets.EDIT_APPLICATION_TEAM - ), - "perms_env_mgmt": get_form_permission_value( - member, PermissionSets.EDIT_APPLICATION_ENVIRONMENTS - ), - "perms_del_env": get_form_permission_value( - member, PermissionSets.DELETE_APPLICATION_ENVIRONMENTS - ), - } - roles = EnvironmentRoles.get_for_application_member(member.id) - environment_roles = [ - { - "environment_id": str(role.environment.id), - "environment_name": role.environment.name, - "role": role.role, - } - for role in roles - ] - team_data.append( - { - "role_id": member.id, - "user_name": member.user_name, - "permission_sets": permission_sets, - "environment_roles": environment_roles, - } - ) - - return TeamForm(data={"members": team_data}) - - -def get_new_member_form(application): - env_roles = [ - {"environment_id": e.id, "environment_name": e.name} - for e in application.environments - ] - - return NewMemberForm(data={"environment_roles": env_roles}) - - -def render_team_page(application): - team_form = get_team_form(application) - new_member_form = get_new_member_form(application) - - return render_template( - "portfolios/applications/team.html", - application=application, - team_form=team_form, - new_member_form=new_member_form, - ) - - -@applications_bp.route("/application//team", methods=["POST"]) -@user_can(Permissions.EDIT_APPLICATION_MEMBER, message="update application member") -def update_team(application_id): - application = Applications.get(application_id) - form = TeamForm(http_request.form) - - if form.validate(): - for member_form in form.members: - app_role = ApplicationRoles.get_by_id(member_form.role_id.data) - new_perms = [ - perm - for perm in member_form.data["permission_sets"] - if perm != PermissionSets.VIEW_APPLICATION - ] - ApplicationRoles.update_permission_sets(app_role, new_perms) - - for environment_role_form in member_form.environment_roles: - environment = Environments.get( - environment_role_form.environment_id.data - ) - Environments.update_env_role( - environment, app_role, environment_role_form.data.get("role") - ) - - flash("updated_application_team_settings", application_name=application.name) - - return redirect( - url_for( - "applications.settings", - application_id=application_id, - fragment="application-members", - _anchor="application-members", - ) - ) - else: - return (render_team_page(application), 400) - - -def send_application_invitation(invitee_email, inviter_name, token): - body = render_template( - "emails/application/invitation.txt", owner=inviter_name, token=token - ) - send_mail.delay( - [invitee_email], - translate("email.application_invite", {"inviter_name": inviter_name}), - body, - ) - - -@applications_bp.route("/application//members/new", methods=["POST"]) -@user_can( - Permissions.CREATE_APPLICATION_MEMBER, message="create new application member" -) -def create_member(application_id): - application = Applications.get(application_id) - form = NewMemberForm(http_request.form) - - if form.validate(): - try: - invite = Applications.invite( - application=application, - inviter=g.current_user, - user_data=form.user_data.data, - permission_sets_names=form.permission_sets.data, - environment_roles_data=form.environment_roles.data, - ) - - send_application_invitation( - invitee_email=invite.email, - inviter_name=g.current_user.full_name, - token=invite.token, - ) - - flash("new_application_member", user_name=invite.user_name) - - except AlreadyExistsError: - return render_template( - "error.html", message="There was an error processing your request." - ) - else: - pass - # TODO: flash error message - - return redirect( - url_for( - "applications.settings", - application_id=application_id, - fragment="application-members", - _anchor="application-members", - ) - ) - - -@applications_bp.route( - "/applications//members//delete", - methods=["POST"], -) -@user_can(Permissions.DELETE_APPLICATION_MEMBER, message="remove application member") -def remove_member(application_id, application_role_id): - application_role = ApplicationRoles.get_by_id(application_role_id) - Applications.remove_member(application_role) - - flash( - "application_member_removed", - user_name=application_role.user_name, - application_name=g.application.name, - ) - - return redirect( - url_for( - "applications.settings", - _anchor="application-members", - application_id=g.application.id, - fragment="application-members", - ) - ) diff --git a/templates/portfolios/applications/team.html b/templates/portfolios/applications/team.html deleted file mode 100644 index ac71a3f8..00000000 --- a/templates/portfolios/applications/team.html +++ /dev/null @@ -1,154 +0,0 @@ -{% extends "portfolios/applications/base.html" %} - -{% from "components/icon.html" import Icon %} -{% from "components/multi_step_modal_form.html" import MultiStepModalForm %} -{% from 'components/save_button.html' import SaveButton %} -{% import "fragments/applications/new_member_modal_content.html" as member_steps %} -{% from "components/alert.html" import Alert %} -{% from "components/delete_confirmation.html" import DeleteConfirmation %} -{% from "components/modal.html" import Modal %} - -{% set secondary_breadcrumb = 'portfolios.applications.team_settings.title' | translate({ "application_name": application.name }) %} - -{% block application_content %} - {% if not application.members %} - {% set user_can_invite = user_can(permissions.CREATE_APPLICATION_MEMBER) %} - -
-

{{ ("portfolios.applications.team_settings.blank_slate.title" | translate) }}

- - {{ Icon('avatar') }} - - {% if not user_can_invite %} -

{{ ("portfolios.applications.team_settings.blank_slate.sub_message" | translate) }}

- {% endif %} - - {% if user_can_invite %} - {% set new_member_modal_name = "add-app-mem" %} -
- {{ "portfolios.applications.team_settings.blank_slate.action_label" | translate }} - - {{ MultiStepModalForm( - name=new_member_modal_name, - form=new_member_form, - form_action=url_for("applications.create_member", application_id=application.id), - steps=[ - member_steps.MemberStepOne(new_member_form), - member_steps.MemberStepTwo(new_member_form, application) - ], - ) }} - {% endif %} -
- - {% else %} -
- {{ 'portfolios.applications.team_settings.subheading' | translate }} -
- -
- - -
- {% if g.matchesPath("application-members") %} - {% include "fragments/flash.html" %} - {% endif %} -
-
-
-
- {{ "portfolios.applications.team_settings.section.title" | translate({ "application_name": application.name }) }} -

Members ({{ team_form.members | length }})

-
-
-
-
- -
-
-
- {{ "common.name" | translate }} -
-
- {{ "portfolios.applications.team_settings.section.table.team_management" | translate }} -
-
- {{ "portfolios.applications.team_settings.section.table.environment_management" | translate }} -
-
- {{ "portfolios.applications.team_settings.section.table.delete_access" | translate }} -
-
-   -
-
-
    - {% if user_can(permissions.EDIT_APPLICATION_MEMBER) %} - {% include "fragments/applications/edit_team.html" %} - {% elif user_can(permissions.VIEW_APPLICATION_MEMBER) %} - {% include "fragments/applications/read_only_team.html" %} - {% endif %} -
-
- - -
- -
- - {% if user_can(permissions.DELETE_APPLICATION_MEMBER) %} - {% for member_form in team_form.members %} - {% set delete_modal_id = "delete-user-{}".format(member_form.id) %} - {% call Modal(name=delete_modal_id) %} -

- {{ "portfolios.applications.remove_member.header" | translate }} -

- - {{ - Alert( - title=("components.modal.destructive_title" | translate), - message=("portfolios.applications.remove_member.alert.message" | translate({"user_name": member_form.user_name.data})), - level="warning" - ) - }} - - {{ - DeleteConfirmation( - modal_id=delete_modal_id, - delete_text=('portfolios.applications.remove_member.button' | translate), - delete_action=url_for('applications.remove_member', application_id=application.id, application_role_id=member_form.data.role_id), - form=member_form - ) - }} - {% endcall %} - {% endfor %} - {% endif %} - - {% if user_can(permissions.CREATE_APPLICATION_MEMBER) %} - {% import "fragments/applications/new_member_modal_content.html" as member_steps %} - {{ MultiStepModalForm( - name=new_member_modal_name, - form=new_member_form, - form_action=url_for("applications.create_member", application_id=application.id), - steps=[ - member_steps.MemberStepOne(new_member_form), - member_steps.MemberStepTwo(new_member_form, application) - ], - ) }} - {% endif %} -
- {% endif %} -{% endblock %} diff --git a/tests/routes/applications/test_settings.py b/tests/routes/applications/test_settings.py index 1e184048..7adc525e 100644 --- a/tests/routes/applications/test_settings.py +++ b/tests/routes/applications/test_settings.py @@ -1,5 +1,7 @@ import pytest +import uuid from flask import url_for, get_flashed_messages +from unittest.mock import Mock from tests.factories import * @@ -424,3 +426,252 @@ def test_delete_environment(client, user_session): assert environment.name in message["message"] # deletes environment assert len(application.environments) == 0 + + +def test_update_team_permissions(client, user_session): + application = ApplicationFactory.create() + owner = application.portfolio.owner + app_role = ApplicationRoleFactory.create( + application=application, permission_sets=[] + ) + user_session(owner) + response = client.post( + url_for("applications.update_team", application_id=application.id), + data={ + "members-0-role_id": app_role.id, + "members-0-permission_sets-perms_team_mgmt": PermissionSets.EDIT_APPLICATION_TEAM, + "members-0-permission_sets-perms_env_mgmt": PermissionSets.EDIT_APPLICATION_ENVIRONMENTS, + "members-0-permission_sets-perms_del_env": PermissionSets.DELETE_APPLICATION_ENVIRONMENTS, + }, + ) + + assert response.status_code == 302 + actual_perms_names = [perm.name for perm in app_role.permission_sets] + expected_perms_names = [ + PermissionSets.VIEW_APPLICATION, + PermissionSets.EDIT_APPLICATION_TEAM, + PermissionSets.EDIT_APPLICATION_ENVIRONMENTS, + PermissionSets.DELETE_APPLICATION_ENVIRONMENTS, + ] + assert expected_perms_names == actual_perms_names + + +def test_update_team_with_bad_permission_sets(client, user_session): + application = ApplicationFactory.create() + owner = application.portfolio.owner + app_role = ApplicationRoleFactory.create( + application=application, permission_sets=[] + ) + permission_sets = app_role.permission_sets + + user_session(owner) + response = client.post( + url_for("applications.update_team", application_id=application.id), + data={ + "members-0-role_id": app_role.id, + "members-0-permission_sets-perms_team_mgmt": PermissionSets.EDIT_APPLICATION_TEAM, + "members-0-permission_sets-perms_env_mgmt": "some random string", + }, + ) + assert response.status_code == 400 + assert app_role.permission_sets == permission_sets + + +def test_update_team_with_non_app_user(client, user_session): + application = ApplicationFactory.create() + owner = application.portfolio.owner + + user_session(owner) + response = client.post( + url_for("applications.update_team", application_id=application.id), + data={ + "members-0-role_id": str(uuid.uuid4()), + "members-0-permission_sets-perms_team_mgmt": PermissionSets.EDIT_APPLICATION_TEAM, + "members-0-permission_sets-perms_env_mgmt": PermissionSets.EDIT_APPLICATION_ENVIRONMENTS, + "members-0-permission_sets-perms_del_env": PermissionSets.DELETE_APPLICATION_ENVIRONMENTS, + }, + ) + + assert response.status_code == 404 + + +def test_update_team_environment_roles(client, user_session): + application = ApplicationFactory.create() + owner = application.portfolio.owner + app_role = ApplicationRoleFactory.create( + application=application, permission_sets=[] + ) + environment = EnvironmentFactory.create(application=application) + env_role = EnvironmentRoleFactory.create( + application_role=app_role, + environment=environment, + role=CSPRole.NETWORK_ADMIN.value, + ) + user_session(owner) + response = client.post( + url_for("applications.update_team", application_id=application.id), + data={ + "members-0-role_id": app_role.id, + "members-0-permission_sets-perms_team_mgmt": PermissionSets.EDIT_APPLICATION_TEAM, + "members-0-permission_sets-perms_env_mgmt": PermissionSets.EDIT_APPLICATION_ENVIRONMENTS, + "members-0-permission_sets-perms_del_env": PermissionSets.DELETE_APPLICATION_ENVIRONMENTS, + "members-0-environment_roles-0-environment_id": environment.id, + "members-0-environment_roles-0-role": CSPRole.TECHNICAL_READ.value, + }, + ) + + assert response.status_code == 302 + assert env_role.role == CSPRole.TECHNICAL_READ.value + + +def test_update_team_revoke_environment_access(client, user_session, db, session): + application = ApplicationFactory.create() + owner = application.portfolio.owner + user = UserFactory.create() + app_role = ApplicationRoleFactory.create( + application=application, user=user, permission_sets=[] + ) + environment = EnvironmentFactory.create(application=application) + env_role = EnvironmentRoleFactory.create( + application_role=app_role, + environment=environment, + role=CSPRole.BASIC_ACCESS.value, + ) + assert user in environment.users + + user_session(owner) + response = client.post( + url_for("applications.update_team", application_id=application.id), + data={ + "members-0-role_id": app_role.id, + "members-0-permission_sets-perms_team_mgmt": PermissionSets.EDIT_APPLICATION_TEAM, + "members-0-permission_sets-perms_env_mgmt": PermissionSets.EDIT_APPLICATION_ENVIRONMENTS, + "members-0-permission_sets-perms_del_env": PermissionSets.DELETE_APPLICATION_ENVIRONMENTS, + "members-0-environment_roles-0-environment_id": environment.id, + "members-0-environment_roles-0-role": NO_ACCESS, + }, + ) + + assert response.status_code == 302 + env_role_exists = db.exists().where(EnvironmentRole.id == env_role.id) + assert not session.query(env_role_exists).scalar() + assert user not in environment.users + + +def test_create_member(monkeypatch, client, user_session, session): + job_mock = Mock() + monkeypatch.setattr("atst.jobs.send_mail.delay", job_mock) + user = UserFactory.create() + application = ApplicationFactory.create( + environments=[{"name": "Naboo"}, {"name": "Endor"}] + ) + env = application.environments[0] + env_1 = application.environments[1] + + user_session(application.portfolio.owner) + + response = client.post( + url_for("applications.create_member", application_id=application.id), + data={ + "user_data-first_name": user.first_name, + "user_data-last_name": user.last_name, + "user_data-dod_id": user.dod_id, + "user_data-email": user.email, + "environment_roles-0-environment_id": env.id, + "environment_roles-0-role": "Basic Access", + "environment_roles-0-environment_name": env.name, + "environment_roles-1-environment_id": env_1.id, + "environment_roles-1-role": NO_ACCESS, + "environment_roles-1-environment_name": env_1.name, + "permission_sets-perms_env_mgmt": True, + "permission_sets-perms_team_mgmt": True, + "permission_sets-perms_del_env": True, + }, + ) + + assert response.status_code == 302 + expected_url = url_for( + "applications.settings", + application_id=application.id, + fragment="application-members", + _anchor="application-members", + _external=True, + ) + assert response.location == expected_url + assert len(application.roles) == 1 + environment_roles = application.roles[0].environment_roles + assert len(environment_roles) == 1 + assert environment_roles[0].environment == env + + invitation = ( + session.query(ApplicationInvitation).filter_by(dod_id=user.dod_id).one() + ) + assert invitation.role.application == application + + assert job_mock.called + + +def test_remove_member_success(client, user_session): + user = UserFactory.create() + application = ApplicationFactory.create() + application_role = ApplicationRoleFactory.create(application=application, user=user) + + user_session(application.portfolio.owner) + + response = client.post( + url_for( + "applications.remove_member", + application_id=application.id, + application_role_id=application_role.id, + ) + ) + + assert response.status_code == 302 + assert response.location == url_for( + "applications.settings", + _anchor="application-members", + _external=True, + application_id=application.id, + fragment="application-members", + ) + + +def test_remove_new_member_success(client, user_session): + application = ApplicationFactory.create() + application_role = ApplicationRoleFactory.create(application=application, user=None) + + user_session(application.portfolio.owner) + + response = client.post( + url_for( + "applications.remove_member", + application_id=application.id, + application_role_id=application_role.id, + ) + ) + + assert response.status_code == 302 + assert response.location == url_for( + "applications.settings", + _anchor="application-members", + _external=True, + application_id=application.id, + fragment="application-members", + ) + + +def test_remove_member_failure(client, user_session): + user = UserFactory.create() + application = ApplicationFactory.create() + + user_session(application.portfolio.owner) + + response = client.post( + url_for( + "applications.remove_member", + application_id=application.id, + application_role_id=uuid.uuid4(), + ) + ) + + assert response.status_code == 404 diff --git a/tests/routes/applications/test_team.py b/tests/routes/applications/test_team.py deleted file mode 100644 index 79d5e5d0..00000000 --- a/tests/routes/applications/test_team.py +++ /dev/null @@ -1,259 +0,0 @@ -import uuid -from unittest.mock import Mock - -from flask import url_for - -from atst.domain.permission_sets import PermissionSets -from atst.models import CSPRole -from atst.forms.data import ENV_ROLE_NO_ACCESS as NO_ACCESS - -from tests.factories import * - - -def test_update_team_permissions(client, user_session): - application = ApplicationFactory.create() - owner = application.portfolio.owner - app_role = ApplicationRoleFactory.create( - application=application, permission_sets=[] - ) - user_session(owner) - response = client.post( - url_for("applications.update_team", application_id=application.id), - data={ - "members-0-role_id": app_role.id, - "members-0-permission_sets-perms_team_mgmt": PermissionSets.EDIT_APPLICATION_TEAM, - "members-0-permission_sets-perms_env_mgmt": PermissionSets.EDIT_APPLICATION_ENVIRONMENTS, - "members-0-permission_sets-perms_del_env": PermissionSets.DELETE_APPLICATION_ENVIRONMENTS, - }, - ) - - assert response.status_code == 302 - actual_perms_names = [perm.name for perm in app_role.permission_sets] - expected_perms_names = [ - PermissionSets.VIEW_APPLICATION, - PermissionSets.EDIT_APPLICATION_TEAM, - PermissionSets.EDIT_APPLICATION_ENVIRONMENTS, - PermissionSets.DELETE_APPLICATION_ENVIRONMENTS, - ] - assert expected_perms_names == actual_perms_names - - -def test_update_team_with_bad_permission_sets(client, user_session): - application = ApplicationFactory.create() - owner = application.portfolio.owner - app_role = ApplicationRoleFactory.create( - application=application, permission_sets=[] - ) - permission_sets = app_role.permission_sets - - user_session(owner) - response = client.post( - url_for("applications.update_team", application_id=application.id), - data={ - "members-0-role_id": app_role.id, - "members-0-permission_sets-perms_team_mgmt": PermissionSets.EDIT_APPLICATION_TEAM, - "members-0-permission_sets-perms_env_mgmt": "some random string", - }, - ) - assert response.status_code == 400 - assert app_role.permission_sets == permission_sets - - -def test_update_team_with_non_app_user(client, user_session): - application = ApplicationFactory.create() - owner = application.portfolio.owner - - user_session(owner) - response = client.post( - url_for("applications.update_team", application_id=application.id), - data={ - "members-0-role_id": str(uuid.uuid4()), - "members-0-permission_sets-perms_team_mgmt": PermissionSets.EDIT_APPLICATION_TEAM, - "members-0-permission_sets-perms_env_mgmt": PermissionSets.EDIT_APPLICATION_ENVIRONMENTS, - "members-0-permission_sets-perms_del_env": PermissionSets.DELETE_APPLICATION_ENVIRONMENTS, - }, - ) - - assert response.status_code == 404 - - -def test_update_team_environment_roles(client, user_session): - application = ApplicationFactory.create() - owner = application.portfolio.owner - app_role = ApplicationRoleFactory.create( - application=application, permission_sets=[] - ) - environment = EnvironmentFactory.create(application=application) - env_role = EnvironmentRoleFactory.create( - application_role=app_role, - environment=environment, - role=CSPRole.NETWORK_ADMIN.value, - ) - user_session(owner) - response = client.post( - url_for("applications.update_team", application_id=application.id), - data={ - "members-0-role_id": app_role.id, - "members-0-permission_sets-perms_team_mgmt": PermissionSets.EDIT_APPLICATION_TEAM, - "members-0-permission_sets-perms_env_mgmt": PermissionSets.EDIT_APPLICATION_ENVIRONMENTS, - "members-0-permission_sets-perms_del_env": PermissionSets.DELETE_APPLICATION_ENVIRONMENTS, - "members-0-environment_roles-0-environment_id": environment.id, - "members-0-environment_roles-0-role": CSPRole.TECHNICAL_READ.value, - }, - ) - - assert response.status_code == 302 - assert env_role.role == CSPRole.TECHNICAL_READ.value - - -def test_update_team_revoke_environment_access(client, user_session, db, session): - application = ApplicationFactory.create() - owner = application.portfolio.owner - user = UserFactory.create() - app_role = ApplicationRoleFactory.create( - application=application, user=user, permission_sets=[] - ) - environment = EnvironmentFactory.create(application=application) - env_role = EnvironmentRoleFactory.create( - application_role=app_role, - environment=environment, - role=CSPRole.BASIC_ACCESS.value, - ) - assert user in environment.users - - user_session(owner) - response = client.post( - url_for("applications.update_team", application_id=application.id), - data={ - "members-0-role_id": app_role.id, - "members-0-permission_sets-perms_team_mgmt": PermissionSets.EDIT_APPLICATION_TEAM, - "members-0-permission_sets-perms_env_mgmt": PermissionSets.EDIT_APPLICATION_ENVIRONMENTS, - "members-0-permission_sets-perms_del_env": PermissionSets.DELETE_APPLICATION_ENVIRONMENTS, - "members-0-environment_roles-0-environment_id": environment.id, - "members-0-environment_roles-0-role": NO_ACCESS, - }, - ) - - assert response.status_code == 302 - env_role_exists = db.exists().where(EnvironmentRole.id == env_role.id) - assert not session.query(env_role_exists).scalar() - assert user not in environment.users - - -def test_create_member(monkeypatch, client, user_session, session): - job_mock = Mock() - monkeypatch.setattr("atst.jobs.send_mail.delay", job_mock) - user = UserFactory.create() - application = ApplicationFactory.create( - environments=[{"name": "Naboo"}, {"name": "Endor"}] - ) - env = application.environments[0] - env_1 = application.environments[1] - - user_session(application.portfolio.owner) - - response = client.post( - url_for("applications.create_member", application_id=application.id), - data={ - "user_data-first_name": user.first_name, - "user_data-last_name": user.last_name, - "user_data-dod_id": user.dod_id, - "user_data-email": user.email, - "environment_roles-0-environment_id": env.id, - "environment_roles-0-role": "Basic Access", - "environment_roles-0-environment_name": env.name, - "environment_roles-1-environment_id": env_1.id, - "environment_roles-1-role": NO_ACCESS, - "environment_roles-1-environment_name": env_1.name, - "permission_sets-perms_env_mgmt": True, - "permission_sets-perms_team_mgmt": True, - "permission_sets-perms_del_env": True, - }, - ) - - assert response.status_code == 302 - expected_url = url_for( - "applications.settings", - application_id=application.id, - fragment="application-members", - _anchor="application-members", - _external=True, - ) - assert response.location == expected_url - assert len(application.roles) == 1 - environment_roles = application.roles[0].environment_roles - assert len(environment_roles) == 1 - assert environment_roles[0].environment == env - - invitation = ( - session.query(ApplicationInvitation).filter_by(dod_id=user.dod_id).one() - ) - assert invitation.role.application == application - - assert job_mock.called - - -def test_remove_member_success(client, user_session): - user = UserFactory.create() - application = ApplicationFactory.create() - application_role = ApplicationRoleFactory.create(application=application, user=user) - - user_session(application.portfolio.owner) - - response = client.post( - url_for( - "applications.remove_member", - application_id=application.id, - application_role_id=application_role.id, - ) - ) - - assert response.status_code == 302 - assert response.location == url_for( - "applications.settings", - _anchor="application-members", - _external=True, - application_id=application.id, - fragment="application-members", - ) - - -def test_remove_new_member_success(client, user_session): - application = ApplicationFactory.create() - application_role = ApplicationRoleFactory.create(application=application, user=None) - - user_session(application.portfolio.owner) - - response = client.post( - url_for( - "applications.remove_member", - application_id=application.id, - application_role_id=application_role.id, - ) - ) - - assert response.status_code == 302 - assert response.location == url_for( - "applications.settings", - _anchor="application-members", - _external=True, - application_id=application.id, - fragment="application-members", - ) - - -def test_remove_member_failure(client, user_session): - user = UserFactory.create() - application = ApplicationFactory.create() - - user_session(application.portfolio.owner) - - response = client.post( - url_for( - "applications.remove_member", - application_id=application.id, - application_role_id=uuid.uuid4(), - ) - ) - - assert response.status_code == 404 From 5dbbe8d12781693598a5399801e4ea4d408be14d Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Thu, 5 Sep 2019 17:32:01 -0400 Subject: [PATCH 04/12] Add sticky header to App settings page --- styles/components/_sticky_cta.scss | 7 ++++++- templates/components/sticky_cta.html | 19 +++++++++++++++---- templates/portfolios/applications/base.html | 10 +++++----- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/styles/components/_sticky_cta.scss b/styles/components/_sticky_cta.scss index 2d841840..0656c660 100644 --- a/styles/components/_sticky_cta.scss +++ b/styles/components/_sticky_cta.scss @@ -4,7 +4,6 @@ z-index: 10; @include media($medium-screen) { - margin-left: -$gap * 5; margin-right: -$gap * 5; } @@ -53,4 +52,10 @@ } } } + + &-return-link { + padding-top: 1.6rem; + font-size: $small-font-size; + font-weight: $font-bold; + } } diff --git a/templates/components/sticky_cta.html b/templates/components/sticky_cta.html index 75e49ccf..4ba268c0 100644 --- a/templates/components/sticky_cta.html +++ b/templates/components/sticky_cta.html @@ -1,12 +1,23 @@ -{% macro StickyCTA(text) -%} +{% from 'components/icon.html' import Icon %} + +{% macro StickyCTA(text, return_link_url=None, return_link_text=None) -%}
+ {% if return_link_url and return_link_text %} + + {% endif %}

{{ text }}

-
- {{ caller() }} -
+ {% if caller %} +
+ {{ caller() }} +
+ {% endif %}
{%- endmacro %} diff --git a/templates/portfolios/applications/base.html b/templates/portfolios/applications/base.html index 2b633e70..040afa62 100644 --- a/templates/portfolios/applications/base.html +++ b/templates/portfolios/applications/base.html @@ -1,11 +1,11 @@ {% extends "portfolios/base.html" %} +{% from "components/sticky_cta.html" import StickyCTA %} + {% block portfolio_header %} -
-
- {{ secondary_breadcrumb }} -
-
+ {% if application %} + {{ StickyCTA(text=application.name, return_link_url=url_for('applications.portfolio_applications', portfolio_id=application.portfolio_id), return_link_text="BACK TO APPLICATIONS") }} + {% endif %} {% endblock %} {% block portfolio_content %} From 93e7ed9828dc89c0cb96ce564f4f86ad056a20f1 Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Thu, 5 Sep 2019 17:39:21 -0400 Subject: [PATCH 05/12] Update name and description section --- styles/elements/_inputs.scss | 1 + .../portfolios/applications/settings.html | 30 ++++++++----------- translations.yaml | 3 ++ 3 files changed, 16 insertions(+), 18 deletions(-) diff --git a/styles/elements/_inputs.scss b/styles/elements/_inputs.scss index 7805a2ef..c56c57cf 100644 --- a/styles/elements/_inputs.scss +++ b/styles/elements/_inputs.scss @@ -58,6 +58,7 @@ .usa-input { margin: ($gap * 4) ($gap * 2) ($gap * 4) 0; + max-width: 75rem; @include media($medium-screen) { margin: ($gap * 4) 0; diff --git a/templates/portfolios/applications/settings.html b/templates/portfolios/applications/settings.html index 939e0063..6b7134ec 100644 --- a/templates/portfolios/applications/settings.html +++ b/templates/portfolios/applications/settings.html @@ -14,7 +14,7 @@ {% block application_content %} -
{{ 'portfolios.applications.settings_heading' | translate }}
+
{{ 'portfolios.applications.settings.name_description' | translate }}
{% if user_can(permissions.EDIT_APPLICATION) %} @@ -22,32 +22,16 @@
{{ application_form.csrf_token }} -

- {{ "fragments.edit_application_form.explain" | translate }} -

{{ TextInput(application_form.name, optional=False) }} {{ TextInput(application_form.description, paragraph=True, optional=False) }}
-
- {% if user_can(permissions.DELETE_APPLICATION) %} -
- -
- {% endif %} -
@@ -244,6 +228,16 @@
{% if user_can(permissions.DELETE_APPLICATION) %} +
+ +
+ {% call Modal(name="delete-application") %}

{{ "portfolios.applications.delete.header" | translate }}

diff --git a/translations.yaml b/translations.yaml index c7b5fcfb..a402ab4d 100644 --- a/translations.yaml +++ b/translations.yaml @@ -59,6 +59,7 @@ common: 'no': 'No' response_label: Response required save: Save + save_changes: Save Changes undo: Undo view: View resource_names: @@ -308,6 +309,8 @@ portfolios: existing_application_title: '{application_name} Application Settings' new_application_title: New Application settings_heading: Application Settings + settings: + name_description: Name and Description team_settings: blank_slate: action_label: Invite a new team member From 3428551cec640102270b1c3bece996eb01a1b8ed Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Thu, 5 Sep 2019 18:13:24 -0400 Subject: [PATCH 06/12] Team members section read only version - display all members in a table and list perms and environments --- atst/routes/applications/settings.py | 12 +- styles/components/_portfolio_layout.scss | 10 ++ .../fragments/applications/edit_team.html | 88 ------------ .../applications/read_only_team.html | 47 ------- .../portfolios/applications/settings.html | 131 ++++++------------ translations.yaml | 11 +- 6 files changed, 66 insertions(+), 233 deletions(-) delete mode 100644 templates/fragments/applications/edit_team.html delete mode 100644 templates/fragments/applications/read_only_team.html diff --git a/atst/routes/applications/settings.py b/atst/routes/applications/settings.py index a0bb30d8..8e9ee64b 100644 --- a/atst/routes/applications/settings.py +++ b/atst/routes/applications/settings.py @@ -94,8 +94,8 @@ def get_form_permission_value(member, edit_perm_set): return PermissionSets.VIEW_APPLICATION -def get_team_form(application): - team_data = [] +def get_members_data(application): + members_data = [] for member in application.members: permission_sets = { "perms_team_mgmt": get_form_permission_value( @@ -117,7 +117,7 @@ def get_team_form(application): } for role in roles ] - team_data.append( + members_data.append( { "role_id": member.id, "user_name": member.user_name, @@ -126,7 +126,7 @@ def get_team_form(application): } ) - return TeamForm(data={"members": team_data}) + return members_data def get_new_member_form(application): @@ -144,8 +144,8 @@ def render_settings_page(application, **kwargs): new_env_form = EditEnvironmentForm() pagination_opts = Paginator.get_pagination_opts(http_request) audit_events = AuditLog.get_application_events(application, pagination_opts) - team_form = get_team_form(application) new_member_form = get_new_member_form(application) + members = get_members_data(application) if "application_form" not in kwargs: kwargs["application_form"] = ApplicationForm( @@ -159,8 +159,8 @@ def render_settings_page(application, **kwargs): members_form=members_form, new_env_form=new_env_form, audit_events=audit_events, - team_form=team_form, new_member_form=new_member_form, + members=members, **kwargs, ) diff --git a/styles/components/_portfolio_layout.scss b/styles/components/_portfolio_layout.scss index 88069f14..5c48422d 100644 --- a/styles/components/_portfolio_layout.scss +++ b/styles/components/_portfolio_layout.scss @@ -139,6 +139,9 @@ } table { + margin: 0; + width: 100%; + thead { th:first-child { padding-left: 3 * $gap; @@ -282,6 +285,13 @@ .application-content { .subheading { @include subheading; + position: relative; + + .icon-link { + position: absolute; + right: 0; + top: 0; + } } .panel { diff --git a/templates/fragments/applications/edit_team.html b/templates/fragments/applications/edit_team.html deleted file mode 100644 index 0f1bcb59..00000000 --- a/templates/fragments/applications/edit_team.html +++ /dev/null @@ -1,88 +0,0 @@ -{% from "components/options_input.html" import OptionsInput %} -{% from "components/toggle_list.html" import ToggleButton, ToggleSection %} - -{{ team_form.csrf_token }} - -{% for member_form in team_form.members %} - {% set delete_modal_id = "delete-user-{}".format(member_form.id) %} - {% set environment_roles_form = member_form.environment_roles %} - {% set permissions_form = member_form.permission_sets %} - - -
  • -
    -
    -
    - {{ member_form.user_name.data }} -
    -
    -
    {{ OptionsInput(permissions_form.perms_team_mgmt, label=False, watch=True) }}
    -
    {{ OptionsInput(permissions_form.perms_env_mgmt, label=False, watch=True) }}
    -
    {{ OptionsInput(permissions_form.perms_del_env, label=False, watch=True) }}
    - -
    - {% call ToggleSection(section_name="environments") %} -
      - {% for environment_form in environment_roles_form %} -
    • - -
      -
      -
      - {{ environment_form.environment_name.data }} -
      -
      -
      - - - -
      -
      -
      -
      - {{ environment_form.role.label }} - {{ environment_form.role(**{"v-on:change": "radioChange", "class": "member-list____role-select__radio"}) }} - - {{ environment_form.environment_id() }} -
      -
      -
      -
    • - {% endfor %} -
    -
    - {% if user_can(permissions.DELETE_APPLICATION_MEMBER) %} - - {{ "portfolios.applications.remove_member.button" | translate }} - - {% endif %} -
    - {% endcall %} - {{ member_form.role_id() }} -
  • -
    -{% endfor %} diff --git a/templates/fragments/applications/read_only_team.html b/templates/fragments/applications/read_only_team.html deleted file mode 100644 index 66d6947a..00000000 --- a/templates/fragments/applications/read_only_team.html +++ /dev/null @@ -1,47 +0,0 @@ -{% from "components/toggle_list.html" import ToggleButton, ToggleSection %} - -{% for member in team_form.members %} - {% set user_permissions = [member.permission_sets.perms_team_mgmt, member.permission_sets.perms_env_mgmt, member.permission_sets.perms_del_env] %} - - {% macro PermissionField(value) %} -
    {{ value }}
    - {% endmacro %} - - -
  • -
    -
    {{ member.user_name.data }}
    - {% for permission in user_permissions %} - {% set perm = dict(permission.choices).get(permission.data) %} - {{ PermissionField(perm) }} - {% endfor %} - -
    - {% call ToggleSection(section_name="environments") %} -
      - {% for environment in member.environment_roles %} -
    • - {{ environment.environment_name.data }} -
    • - {% endfor %} -
    - {% endcall %} -
  • -
    -{% endfor %} diff --git a/templates/portfolios/applications/settings.html b/templates/portfolios/applications/settings.html index 6b7134ec..574dd0b9 100644 --- a/templates/portfolios/applications/settings.html +++ b/templates/portfolios/applications/settings.html @@ -98,100 +98,49 @@ {% else %}
    - {{ 'portfolios.applications.team_settings.subheading' | translate }} + {{ 'portfolios.applications.settings.team_members' | translate }} + + {% set new_member_modal_name = "add-app-mem" %} + {% if user_can(permissions.CREATE_APPLICATION_MEMBER) %} + + {{ "portfolios.admin.add_new_member" | translate }} + {{ Icon("plus") }} + + {% endif %}
    - -
    -
    - {% if g.matchesPath("application-members") %} - {% include "fragments/flash.html" %} - {% endif %} -
    -
    -
    -
    - {{ "portfolios.applications.team_settings.section.title" | translate({ "application_name": application.name }) }} -

    Members ({{ team_form.members | length }})

    -
    -
    -
    -
    - -
    -
    -
    - {{ "common.name" | translate }} -
    -
    - {{ "portfolios.applications.team_settings.section.table.team_management" | translate }} -
    -
    - {{ "portfolios.applications.team_settings.section.table.environment_management" | translate }} -
    -
    - {{ "portfolios.applications.team_settings.section.table.delete_access" | translate }} -
    -
    -   -
    -
    -
      - {% if user_can(permissions.EDIT_APPLICATION_MEMBER) %} - {% include "fragments/applications/edit_team.html" %} - {% elif user_can(permissions.VIEW_APPLICATION_MEMBER) %} - {% include "fragments/applications/read_only_team.html" %} - {% endif %} -
    -
    - - -
    -
    -
    - - {% if user_can(permissions.DELETE_APPLICATION_MEMBER) %} - {% for member_form in team_form.members %} - {% set delete_modal_id = "delete-user-{}".format(member_form.id) %} - {% call Modal(name=delete_modal_id) %} -

    - {{ "portfolios.applications.remove_member.header" | translate }} -

    - - {{ - Alert( - title=("components.modal.destructive_title" | translate), - message=("portfolios.applications.remove_member.alert.message" | translate({"user_name": member_form.user_name.data})), - level="warning" - ) - }} - - {{ - DeleteConfirmation( - modal_id=delete_modal_id, - delete_text=('portfolios.applications.remove_member.button' | translate), - delete_action=url_for('applications.remove_member', application_id=application.id, application_role_id=member_form.data.role_id), - form=member_form - ) - }} - {% endcall %} - {% endfor %} - {% endif %} +
    + {% if g.matchesPath("application-members") %} + {% include "fragments/flash.html" %} + {% endif %} + + + + + + + + + + {% for member in members %} + + + + + + {% endfor %} + +
    MemberProject PermissionsEnvironment Access
    {{ member.user_name }} + {% for perm, value in member.permission_sets.items() %} + {{ ("portfolios.applications.members.{}.{}".format(perm, value)) | translate }}
    + {% endfor %} +
    + {% for env in member.environment_roles %} + {{ env.environment_name }}{% if not env == member.environment_roles[-1]%},{% endif %} + {% endfor %} +
    +
    {% if user_can(permissions.CREATE_APPLICATION_MEMBER) %} {% import "fragments/applications/new_member_modal_content.html" as member_steps %} diff --git a/translations.yaml b/translations.yaml index a402ab4d..4060ece1 100644 --- a/translations.yaml +++ b/translations.yaml @@ -311,6 +311,7 @@ portfolios: settings_heading: Application Settings settings: name_description: Name and Description + team_members: Team Members team_settings: blank_slate: action_label: Invite a new team member @@ -323,7 +324,6 @@ portfolios: environment_management: Environment Management team_management: Team Management title: '{application_name} Team' - subheading: Team Settings title: '{application_name} Team Settings' add_to_environment: Add to existing environment team_text: Team @@ -335,6 +335,15 @@ portfolios: manage_envs: 'Allow member to add and rename environments within the application.' delete_envs: 'Allow member to delete environments within the application.' manage_team: 'Allow member to add, update, and remove members from the application team.' + perms_team_mgmt: + view_application: View Team + edit_application_team: Edit Team + perms_env_mgmt: + view_application: View Environments + edit_application_environments: Edit Environments + perms_del_env: + view_application: "" + delete_application_environments: Delete Application index: empty: start_button: Start a new JEDI portfolio From a6caafada4e191641647bc950f4d1c9dc631a0d9 Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Fri, 6 Sep 2019 11:11:22 -0400 Subject: [PATCH 07/12] Update delete app button and section --- Pipfile | 1 + atst/filters.py | 11 ++++++ .../portfolios/applications/settings.html | 36 ++++++++++++++----- translations.yaml | 2 ++ 4 files changed, 42 insertions(+), 8 deletions(-) diff --git a/Pipfile b/Pipfile index a84969ea..08d9e35b 100644 --- a/Pipfile +++ b/Pipfile @@ -26,6 +26,7 @@ azure-storage = "*" azure-storage-common = "*" boto3 = "*" celery = "*" +inflect = "*" [dev-packages] bandit = "*" diff --git a/atst/filters.py b/atst/filters.py index 48b8166c..408b5afa 100644 --- a/atst/filters.py +++ b/atst/filters.py @@ -1,5 +1,6 @@ import re import datetime +import inflect from atst.utils.localization import translate from flask import render_template from jinja2 import contextfilter @@ -67,6 +68,14 @@ def renderAuditEvent(event): return render_template("audit_log/events/default.html", event=event) +def numberToWords(integer): + return inflect.engine().number_to_words(integer) + + +def pluralize(word, number): + return inflect.engine().plural(word, number) + + def register_filters(app): app.jinja_env.filters["iconSvg"] = iconSvg app.jinja_env.filters["dollars"] = dollars @@ -76,6 +85,8 @@ def register_filters(app): app.jinja_env.filters["pageWindow"] = pageWindow app.jinja_env.filters["renderAuditEvent"] = renderAuditEvent app.jinja_env.filters["withExtraParams"] = with_extra_params + app.jinja_env.filters["numberToWords"] = numberToWords + app.jinja_env.filters["pluralize"] = pluralize @contextfilter def translateWithoutCache(context, *kwargs): diff --git a/templates/portfolios/applications/settings.html b/templates/portfolios/applications/settings.html index 574dd0b9..7ab84a3f 100644 --- a/templates/portfolios/applications/settings.html +++ b/templates/portfolios/applications/settings.html @@ -176,15 +176,35 @@
    +
    + {% if user_can(permissions.DELETE_APPLICATION) %} -
    - + {% set env_count = application.environments | length %} + {% set pluralized_env = "environment" | pluralize(env_count) %} + +
    + {{ "portfolios.applications.delete.subheading" | translate }} +
    + +
    +
    +
    +
    + {{ "portfolios.applications.delete.panel_text" | translate({"name": application.name, "env_count": env_count | numberToWords, "pluralized_env": pluralized_env}) | safe }} +
    +
    +
    + +
    +
    +
    +
    {% call Modal(name="delete-application") %} diff --git a/translations.yaml b/translations.yaml index 4060ece1..faa4ad0e 100644 --- a/translations.yaml +++ b/translations.yaml @@ -298,6 +298,8 @@ portfolios: message: You will lose access to this application and all environments will be removed from the CSP. Your reporting and activity will still be accessible. button: Delete application header: Are you sure you want to delete this application? + panel_text: 'Deleting {name} will delete this applicaiton along with all {env_count} {pluralized_env}. This cannot be undone.' + subheading: Delete Application enter_env_name: "Enter environment name:" environments: name: Name From 5c9dd7199992e279950e77356745c5cd7e69d969 Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Fri, 6 Sep 2019 15:30:53 -0400 Subject: [PATCH 08/12] Update the environments section and delete unused templates --- styles/components/_portfolio_layout.scss | 28 ++++ .../applications/add_new_environment.html | 14 +- .../edit_environment_team_form.html | 91 ------------- .../applications/edit_environments.html | 128 ------------------ .../applications/read_only_environments.html | 76 ----------- .../portfolios/applications/settings.html | 109 +++++++++++++-- 6 files changed, 128 insertions(+), 318 deletions(-) delete mode 100644 templates/fragments/applications/edit_environment_team_form.html delete mode 100644 templates/fragments/applications/edit_environments.html delete mode 100644 templates/fragments/applications/read_only_environments.html diff --git a/styles/components/_portfolio_layout.scss b/styles/components/_portfolio_layout.scss index 5c48422d..848467dd 100644 --- a/styles/components/_portfolio_layout.scss +++ b/styles/components/_portfolio_layout.scss @@ -322,6 +322,34 @@ input#delete-application { margin-top: $gap * 3; } + + .accordion-table__item-content.form-row { + margin-bottom: 0; + margin-top: 0; + padding-bottom: 0; + } + + li.accordion-table__item__expanded { + height: auto; + } + + .environment-list__item { + position: relative; + height: 7rem; + } + + span.accordion-table__item__toggler.icon-link { + font-size: $small-font-size; + font-weight: $font-normal; + position: absolute; + left: -$gap * 1.25; + bottom: 0; + } + + a.application-list-item__environment__csp_link.icon-link { + font-size: $small-font-size; + font-weight: $font-normal; + } } .activity-log { diff --git a/templates/fragments/applications/add_new_environment.html b/templates/fragments/applications/add_new_environment.html index f1e72e4f..e8190e14 100644 --- a/templates/fragments/applications/add_new_environment.html +++ b/templates/fragments/applications/add_new_environment.html @@ -28,15 +28,9 @@
    - - + + {{ Icon('plus') }} + {{ "portfolios.applications.add_environment" | translate }} + - diff --git a/templates/fragments/applications/edit_environment_team_form.html b/templates/fragments/applications/edit_environment_team_form.html deleted file mode 100644 index 88d55f47..00000000 --- a/templates/fragments/applications/edit_environment_team_form.html +++ /dev/null @@ -1,91 +0,0 @@ -{% from "components/icon.html" import Icon %} -{% from "components/save_button.html" import SaveButton %} - - -{% for env_form in members_form.envs %} - {% if env_form.env_id.data == env['id'] %} -
    - {{ 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=("common.save" | translate) - ) - }} -
    -
    -
    - -
    - {% endif %} -{% endfor %} diff --git a/templates/fragments/applications/edit_environments.html b/templates/fragments/applications/edit_environments.html deleted file mode 100644 index ee3d5377..00000000 --- a/templates/fragments/applications/edit_environments.html +++ /dev/null @@ -1,128 +0,0 @@ -{% from "components/delete_confirmation.html" import DeleteConfirmation %} -{% from "components/icon.html" import Icon %} -{% from "components/modal.html" import Modal %} -{% from "components/options_input.html" import OptionsInput %} -{% from "components/save_button.html" import SaveButton %} -{% from "components/text_input.html" import TextInput %} -{% from "components/toggle_list.html" import ToggleButton, ToggleSection %} - - -
    -
    -
    -
    -
    {{ 'portfolios.applications.environments_heading' | translate }}
    -
    - - {{ Icon('info') }} - {{ "portfolios.admin.settings_info" | translate }} - -
    -
    - -
    -
    -
    {{ "portfolios.applications.environments.name" | translate }}
    -
    {{ "portfolios.applications.environments.edit_name" | translate }}
    -
    {{ "common.delete" | translate }}
    -
    {{ "common.members" | translate }}
    -
    - -
      - {% for env in environments_obj %} - {% set delete_environment_modal_id = "delete_modal_environment{}".format(env['id']) %} - {% set edit_form = env['edit_form'] %} - - -
    • -
      -
      - {{ env['name'] }} -
      -
      - - {% set edit_environment_button %} - {{ Icon('edit') }} - {% endset %} - - {{ - ToggleButton( - open_html=edit_environment_button, - close_html=edit_environment_button, - section_name="edit" - ) - }} - -
      -
      - - {{ Icon('trash') }} - -
      - -
      - - {% call ToggleSection(section_name="members", classes="environment-roles") %} - {% include 'fragments/applications/edit_environment_team_form.html' %} - {% endcall %} - - {% call ToggleSection(section_name="edit") %} -
        -
      • -
        - {{ edit_form.csrf_token }} - {{ TextInput(edit_form.name, validation='requiredField') }} - {{ - SaveButton( - text=("common.save" | translate) - ) - }} -
        -
      • -
      - {% endcall %} -
    • -
      - - {% call Modal(name=delete_environment_modal_id) %} -

      - {{ 'fragments.edit_environment_team_form.delete_environment_title' | translate }} -

      - - {{ - Alert( - level="warning", - title=('components.modal.destructive_title' | translate), - message=('components.modal.destructive_message' | translate({"resource": "environment"})), - ) - }} - - {{ - DeleteConfirmation( - modal_id=delete_environment_modal_id, - delete_text=('portfolios.applications.environments.delete.button' | translate), - delete_action= url_for('applications.delete_environment', environment_id=env['id']), - form=edit_form - ) - }} - {% endcall %} - {% endfor %} -
    -
    -
    diff --git a/templates/fragments/applications/read_only_environments.html b/templates/fragments/applications/read_only_environments.html deleted file mode 100644 index 90181a2d..00000000 --- a/templates/fragments/applications/read_only_environments.html +++ /dev/null @@ -1,76 +0,0 @@ -{% from "components/icon.html" import Icon %} -{% from "components/toggle_list.html" import ToggleButton, ToggleSection %} - -
    -
    -
    -
    -
    {{ 'portfolios.applications.environments_heading' | translate }}
    -
    -
    -
    - -
    -
    - {{ "portfolios.applications.environments.name" | translate }} -
    - -
      - {% for env in environments_obj %} - -
    • -
      - - {{ env['name'] }} - - - - {% set open_members_button %} - {{ "common.members" | translate }} ({{ env['member_count'] }}) {{ Icon('caret_down') }} - {% endset %} - - {% set close_members_button %} - {{ "common.members" | translate }} ({{ env['member_count'] }}) {{ Icon('caret_up') }} - {% endset %} - - {{ - ToggleButton( - open_html=open_members_button, - close_html=close_members_button, - section_name="members" - ) - }} - -
      - - {% call ToggleSection(section_name="members") %} -
        - {% for member in env['members'] %} -
      • -
        {{ member }}
        -
      • - {% endfor %} -
      - {% endcall %} - - {% call ToggleSection(section_name="edit") %} -
        -
      • -
        -
        -
        -
        - Row here -
        -
        -
        -
        -
      • -
      - {% endcall %} -
    • -
      - {% endfor %} -
    -
    -
    diff --git a/templates/portfolios/applications/settings.html b/templates/portfolios/applications/settings.html index 7ab84a3f..6ff8a496 100644 --- a/templates/portfolios/applications/settings.html +++ b/templates/portfolios/applications/settings.html @@ -9,6 +9,7 @@ {% from "components/pagination.html" import Pagination %} {% from "components/save_button.html" import SaveButton %} {% from "components/text_input.html" import TextInput %} +{% from "components/toggle_list.html" import ToggleButton, ToggleSection %} {% set secondary_breadcrumb = 'portfolios.applications.existing_application_title' | translate({ "application_name": application.name }) %} @@ -157,22 +158,104 @@ {% endif %} -
    -
    - {% if g.matchesPath("application-environments") %} - {% include "fragments/flash.html" %} - {% endif %} - {% if user_can(permissions.EDIT_ENVIRONMENT) %} - {% include "fragments/applications/edit_environments.html" %} +
    + Environments - {% if user_can(permissions.CREATE_ENVIRONMENT) %} - {% include "fragments/applications/add_new_environment.html" %} - {% endif %} + {% if user_can(permissions.CREATE_ENVIRONMENT) %} + {% include "fragments/applications/add_new_environment.html" %} + {% endif %} +
    - {% elif user_can(permissions.VIEW_ENVIRONMENT) %} - {% include "fragments/applications/read_only_environments.html" %} - {% endif %} + + {% if g.matchesPath("application-environments") %} + {% include "fragments/flash.html" %} + {% endif %} + +
    +
    +
    +
      + {% for env in environments_obj %} + {% set edit_form = env['edit_form'] %} + +
    • +
      +
      +
      + + {{ env['name'] }} + + + {% set edit_environment_button %} + {{ Icon('edit') }} + {% endset %} + + {{ + ToggleButton( + open_html=edit_environment_button, + close_html=edit_environment_button, + section_name="edit" + ) + }} + + + {% set open_members_button %} + {{ env['member_count'] }} members + {% endset %} + + {% set close_members_button %} + {{ env['member_count'] }} members + {% endset %} + + {{ + ToggleButton( + open_html=open_members_button, + close_html=close_members_button, + section_name="members" + ) + }} + +
      + +
      + +
      + + {% call ToggleSection(section_name="members") %} +
        + {% for member in env['members'] %} +
      • + {{ member }} +
      • + {% endfor %} +
      + {% endcall %} + + {% call ToggleSection(section_name="edit") %} +
        +
      • +
        + {{ edit_form.csrf_token }} + {{ TextInput(edit_form.name, validation='requiredField') }} + {{ + SaveButton( + text=("common.save" | translate) + ) + }} +
        +
      • +
      + {% endcall %} +
    • +
      + {% endfor %} +
    +
    From 7132edc1f132d503fafb4f9019f348a736050518 Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Fri, 6 Sep 2019 15:41:43 -0400 Subject: [PATCH 09/12] Clean up html and move text into translations --- .../portfolios/applications/settings.html | 29 ++++++------------- translations.yaml | 7 +++-- 2 files changed, 14 insertions(+), 22 deletions(-) diff --git a/templates/portfolios/applications/settings.html b/templates/portfolios/applications/settings.html index 6ff8a496..0f8229e1 100644 --- a/templates/portfolios/applications/settings.html +++ b/templates/portfolios/applications/settings.html @@ -104,8 +104,8 @@ {% set new_member_modal_name = "add-app-mem" %} {% if user_can(permissions.CREATE_APPLICATION_MEMBER) %} - {{ "portfolios.admin.add_new_member" | translate }} {{ Icon("plus") }} + {{ "portfolios.applications.add_member" | translate }} {% endif %}
    @@ -158,21 +158,18 @@ {% endif %} -
    - Environments + {{ 'common.resource_names.environments' | translate }} {% if user_can(permissions.CREATE_ENVIRONMENT) %} {% include "fragments/applications/add_new_environment.html" %} {% endif %}
    - - {% if g.matchesPath("application-environments") %} - {% include "fragments/flash.html" %} - {% endif %} -
    + {% if g.matchesPath("application-environments") %} + {% include "fragments/flash.html" %} + {% endif %}
      @@ -200,28 +197,20 @@ }} - {% set open_members_button %} - {{ env['member_count'] }} members - {% endset %} - - {% set close_members_button %} - {{ env['member_count'] }} members - {% endset %} - + {% set members_button = "portfolios.applications.member_count" | translate({'count': env['member_count']}) %} {{ ToggleButton( - open_html=open_members_button, - close_html=close_members_button, + open_html=members_button, + close_html=members_button, section_name="members" ) }}
    -
    diff --git a/translations.yaml b/translations.yaml index faa4ad0e..11ed462d 100644 --- a/translations.yaml +++ b/translations.yaml @@ -281,13 +281,15 @@ portfolios: portfolio_name: Portfolio name applications: add_application_text: Add a new application - add_environment: Add new environment + add_environment: Create an Environment + add_member: Add a New Team Member add_another_environment: Add another environment app_settings_text: App settings create_button_text: Create create_new_env: Create a new environment. create_new_env_info: Creating an environment gives you access to the Cloud Service Provider. This environment will function within the constraints of the task order, and any costs will be billed against the portfolio. csp_console_text: CSP console + csp_link: Cloud Service Provider Link remove_member: alert: message: '{user_name} will no longer be able to access this application or any of its environments' @@ -298,7 +300,7 @@ portfolios: message: You will lose access to this application and all environments will be removed from the CSP. Your reporting and activity will still be accessible. button: Delete application header: Are you sure you want to delete this application? - panel_text: 'Deleting {name} will delete this applicaiton along with all {env_count} {pluralized_env}. This cannot be undone.' + panel_text: 'Deleting {name} will delete this application along with all {env_count} {pluralized_env}. This cannot be undone.' subheading: Delete Application enter_env_name: "Enter environment name:" environments: @@ -309,6 +311,7 @@ portfolios: environments_description: Each environment created within an application is logically separated from one another for easier management and security. environments_heading: Application environments existing_application_title: '{application_name} Application Settings' + member_count: '{count} members' new_application_title: New Application settings_heading: Application Settings settings: From 3e3dfc50561aeefcf46bbb015f3f2c285bc7e863 Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Mon, 9 Sep 2019 14:51:59 -0400 Subject: [PATCH 10/12] Remove inflect dependency --- Pipfile | 1 - atst/filters.py | 11 ----------- templates/portfolios/applications/settings.html | 8 ++++++-- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/Pipfile b/Pipfile index 08d9e35b..a84969ea 100644 --- a/Pipfile +++ b/Pipfile @@ -26,7 +26,6 @@ azure-storage = "*" azure-storage-common = "*" boto3 = "*" celery = "*" -inflect = "*" [dev-packages] bandit = "*" diff --git a/atst/filters.py b/atst/filters.py index 408b5afa..48b8166c 100644 --- a/atst/filters.py +++ b/atst/filters.py @@ -1,6 +1,5 @@ import re import datetime -import inflect from atst.utils.localization import translate from flask import render_template from jinja2 import contextfilter @@ -68,14 +67,6 @@ def renderAuditEvent(event): return render_template("audit_log/events/default.html", event=event) -def numberToWords(integer): - return inflect.engine().number_to_words(integer) - - -def pluralize(word, number): - return inflect.engine().plural(word, number) - - def register_filters(app): app.jinja_env.filters["iconSvg"] = iconSvg app.jinja_env.filters["dollars"] = dollars @@ -85,8 +76,6 @@ def register_filters(app): app.jinja_env.filters["pageWindow"] = pageWindow app.jinja_env.filters["renderAuditEvent"] = renderAuditEvent app.jinja_env.filters["withExtraParams"] = with_extra_params - app.jinja_env.filters["numberToWords"] = numberToWords - app.jinja_env.filters["pluralize"] = pluralize @contextfilter def translateWithoutCache(context, *kwargs): diff --git a/templates/portfolios/applications/settings.html b/templates/portfolios/applications/settings.html index 0f8229e1..68550046 100644 --- a/templates/portfolios/applications/settings.html +++ b/templates/portfolios/applications/settings.html @@ -252,7 +252,11 @@ {% if user_can(permissions.DELETE_APPLICATION) %} {% set env_count = application.environments | length %} - {% set pluralized_env = "environment" | pluralize(env_count) %} + {% if env_count == 1 %} + {% set pluralized_env = "environment" %} + {% else %} + {% set pluralized_env = "environments" %} + {% endif %}
    {{ "portfolios.applications.delete.subheading" | translate }} @@ -262,7 +266,7 @@
    - {{ "portfolios.applications.delete.panel_text" | translate({"name": application.name, "env_count": env_count | numberToWords, "pluralized_env": pluralized_env}) | safe }} + {{ "portfolios.applications.delete.panel_text" | translate({"name": application.name, "env_count": env_count , "pluralized_env": pluralized_env}) | safe }}
    From a4df658857ec3204739b9978f3ac1a27241846af Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Mon, 9 Sep 2019 15:16:43 -0400 Subject: [PATCH 11/12] Move cancel button in add env form --- styles/components/_portfolio_layout.scss | 2 +- .../fragments/applications/add_new_environment.html | 12 +++++------- templates/portfolios/applications/settings.html | 2 +- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/styles/components/_portfolio_layout.scss b/styles/components/_portfolio_layout.scss index 848467dd..240c3c35 100644 --- a/styles/components/_portfolio_layout.scss +++ b/styles/components/_portfolio_layout.scss @@ -287,7 +287,7 @@ @include subheading; position: relative; - .icon-link { + .icon-link__add { position: absolute; right: 0; top: 0; diff --git a/templates/fragments/applications/add_new_environment.html b/templates/fragments/applications/add_new_environment.html index e8190e14..32fded93 100644 --- a/templates/fragments/applications/add_new_environment.html +++ b/templates/fragments/applications/add_new_environment.html @@ -18,17 +18,15 @@
    - + {{ Icon('plus') }} {{ "portfolios.applications.add_environment" | translate }} diff --git a/templates/portfolios/applications/settings.html b/templates/portfolios/applications/settings.html index 68550046..1c1957b0 100644 --- a/templates/portfolios/applications/settings.html +++ b/templates/portfolios/applications/settings.html @@ -103,7 +103,7 @@ {% set new_member_modal_name = "add-app-mem" %} {% if user_can(permissions.CREATE_APPLICATION_MEMBER) %} - + {{ Icon("plus") }} {{ "portfolios.applications.add_member" | translate }} From e391c3269df62074dd2b2de16dbdef4292f42b44 Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Mon, 9 Sep 2019 15:23:37 -0400 Subject: [PATCH 12/12] Remove unused route to update team roles --- atst/routes/applications/settings.py | 39 ------- tests/routes/applications/test_settings.py | 130 --------------------- 2 files changed, 169 deletions(-) diff --git a/atst/routes/applications/settings.py b/atst/routes/applications/settings.py index 8e9ee64b..e586e317 100644 --- a/atst/routes/applications/settings.py +++ b/atst/routes/applications/settings.py @@ -12,7 +12,6 @@ from atst.forms.app_settings import AppEnvRolesForm from atst.forms.application import ApplicationForm, EditEnvironmentForm from atst.forms.application_member import NewForm as NewMemberForm from atst.forms.data import ENV_ROLE_NO_ACCESS as NO_ACCESS -from atst.forms.team import TeamForm from atst.domain.authz.decorator import user_can_access_decorator as user_can from atst.models.environment_role import CSPRole from atst.models.permissions import Permissions @@ -340,44 +339,6 @@ def delete_environment(environment_id): ) -@applications_bp.route("/application//team", methods=["POST"]) -@user_can(Permissions.EDIT_APPLICATION_MEMBER, message="update application member") -def update_team(application_id): - application = Applications.get(application_id) - form = TeamForm(http_request.form) - - if form.validate(): - for member_form in form.members: - app_role = ApplicationRoles.get_by_id(member_form.role_id.data) - new_perms = [ - perm - for perm in member_form.data["permission_sets"] - if perm != PermissionSets.VIEW_APPLICATION - ] - ApplicationRoles.update_permission_sets(app_role, new_perms) - - for environment_role_form in member_form.environment_roles: - environment = Environments.get( - environment_role_form.environment_id.data - ) - Environments.update_env_role( - environment, app_role, environment_role_form.data.get("role") - ) - - flash("updated_application_team_settings", application_name=application.name) - - return redirect( - url_for( - "applications.settings", - application_id=application_id, - fragment="application-members", - _anchor="application-members", - ) - ) - else: - return (render_settings_page(application), 400) - - @applications_bp.route("/application//members/new", methods=["POST"]) @user_can( Permissions.CREATE_APPLICATION_MEMBER, message="create new application member" diff --git a/tests/routes/applications/test_settings.py b/tests/routes/applications/test_settings.py index 7adc525e..9222167d 100644 --- a/tests/routes/applications/test_settings.py +++ b/tests/routes/applications/test_settings.py @@ -428,136 +428,6 @@ def test_delete_environment(client, user_session): assert len(application.environments) == 0 -def test_update_team_permissions(client, user_session): - application = ApplicationFactory.create() - owner = application.portfolio.owner - app_role = ApplicationRoleFactory.create( - application=application, permission_sets=[] - ) - user_session(owner) - response = client.post( - url_for("applications.update_team", application_id=application.id), - data={ - "members-0-role_id": app_role.id, - "members-0-permission_sets-perms_team_mgmt": PermissionSets.EDIT_APPLICATION_TEAM, - "members-0-permission_sets-perms_env_mgmt": PermissionSets.EDIT_APPLICATION_ENVIRONMENTS, - "members-0-permission_sets-perms_del_env": PermissionSets.DELETE_APPLICATION_ENVIRONMENTS, - }, - ) - - assert response.status_code == 302 - actual_perms_names = [perm.name for perm in app_role.permission_sets] - expected_perms_names = [ - PermissionSets.VIEW_APPLICATION, - PermissionSets.EDIT_APPLICATION_TEAM, - PermissionSets.EDIT_APPLICATION_ENVIRONMENTS, - PermissionSets.DELETE_APPLICATION_ENVIRONMENTS, - ] - assert expected_perms_names == actual_perms_names - - -def test_update_team_with_bad_permission_sets(client, user_session): - application = ApplicationFactory.create() - owner = application.portfolio.owner - app_role = ApplicationRoleFactory.create( - application=application, permission_sets=[] - ) - permission_sets = app_role.permission_sets - - user_session(owner) - response = client.post( - url_for("applications.update_team", application_id=application.id), - data={ - "members-0-role_id": app_role.id, - "members-0-permission_sets-perms_team_mgmt": PermissionSets.EDIT_APPLICATION_TEAM, - "members-0-permission_sets-perms_env_mgmt": "some random string", - }, - ) - assert response.status_code == 400 - assert app_role.permission_sets == permission_sets - - -def test_update_team_with_non_app_user(client, user_session): - application = ApplicationFactory.create() - owner = application.portfolio.owner - - user_session(owner) - response = client.post( - url_for("applications.update_team", application_id=application.id), - data={ - "members-0-role_id": str(uuid.uuid4()), - "members-0-permission_sets-perms_team_mgmt": PermissionSets.EDIT_APPLICATION_TEAM, - "members-0-permission_sets-perms_env_mgmt": PermissionSets.EDIT_APPLICATION_ENVIRONMENTS, - "members-0-permission_sets-perms_del_env": PermissionSets.DELETE_APPLICATION_ENVIRONMENTS, - }, - ) - - assert response.status_code == 404 - - -def test_update_team_environment_roles(client, user_session): - application = ApplicationFactory.create() - owner = application.portfolio.owner - app_role = ApplicationRoleFactory.create( - application=application, permission_sets=[] - ) - environment = EnvironmentFactory.create(application=application) - env_role = EnvironmentRoleFactory.create( - application_role=app_role, - environment=environment, - role=CSPRole.NETWORK_ADMIN.value, - ) - user_session(owner) - response = client.post( - url_for("applications.update_team", application_id=application.id), - data={ - "members-0-role_id": app_role.id, - "members-0-permission_sets-perms_team_mgmt": PermissionSets.EDIT_APPLICATION_TEAM, - "members-0-permission_sets-perms_env_mgmt": PermissionSets.EDIT_APPLICATION_ENVIRONMENTS, - "members-0-permission_sets-perms_del_env": PermissionSets.DELETE_APPLICATION_ENVIRONMENTS, - "members-0-environment_roles-0-environment_id": environment.id, - "members-0-environment_roles-0-role": CSPRole.TECHNICAL_READ.value, - }, - ) - - assert response.status_code == 302 - assert env_role.role == CSPRole.TECHNICAL_READ.value - - -def test_update_team_revoke_environment_access(client, user_session, db, session): - application = ApplicationFactory.create() - owner = application.portfolio.owner - user = UserFactory.create() - app_role = ApplicationRoleFactory.create( - application=application, user=user, permission_sets=[] - ) - environment = EnvironmentFactory.create(application=application) - env_role = EnvironmentRoleFactory.create( - application_role=app_role, - environment=environment, - role=CSPRole.BASIC_ACCESS.value, - ) - assert user in environment.users - - user_session(owner) - response = client.post( - url_for("applications.update_team", application_id=application.id), - data={ - "members-0-role_id": app_role.id, - "members-0-permission_sets-perms_team_mgmt": PermissionSets.EDIT_APPLICATION_TEAM, - "members-0-permission_sets-perms_env_mgmt": PermissionSets.EDIT_APPLICATION_ENVIRONMENTS, - "members-0-permission_sets-perms_del_env": PermissionSets.DELETE_APPLICATION_ENVIRONMENTS, - "members-0-environment_roles-0-environment_id": environment.id, - "members-0-environment_roles-0-role": NO_ACCESS, - }, - ) - - assert response.status_code == 302 - env_role_exists = db.exists().where(EnvironmentRole.id == env_role.id) - assert not session.query(env_role_exists).scalar() - assert user not in environment.users - - def test_create_member(monkeypatch, client, user_session, session): job_mock = Mock() monkeypatch.setattr("atst.jobs.send_mail.delay", job_mock)