diff --git a/.secrets.baseline b/.secrets.baseline index 3acc6dd6..26e8796a 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -3,7 +3,7 @@ "files": "^.secrets.baseline$", "lines": null }, - "generated_at": "2019-09-26T13:53:31Z", + "generated_at": "2019-09-30T13:51:34Z", "plugins_used": [ { "base64_limit": 4.5, @@ -199,5 +199,5 @@ } ] }, - "version": "0.12.6" + "version": "0.12.5" } diff --git a/atst/routes/applications/new.py b/atst/routes/applications/new.py index 2f3ce510..fe160d65 100644 --- a/atst/routes/applications/new.py +++ b/atst/routes/applications/new.py @@ -7,6 +7,11 @@ from atst.forms.application import NameAndDescriptionForm, EnvironmentsForm from atst.domain.authz.decorator import user_can_access_decorator as user_can from atst.models.permissions import Permissions from atst.utils.flash import formatted_flash as flash +from atst.routes.applications.settings import ( + get_members_data, + get_new_member_form, + handle_create_member, +) def get_new_application_form(form_data, form_class, application_id=None): @@ -24,6 +29,7 @@ def render_new_application_form( if application_id: application = Applications.get(application_id) render_args["form"] = form or form_class(obj=application) + render_args["application"] = application else: render_args["form"] = form or form_class() @@ -93,12 +99,19 @@ def create_or_update_new_application_step_1(portfolio_id, application_id=None): ) @user_can(Permissions.CREATE_APPLICATION, message="view create new application form") def view_new_application_step_2(portfolio_id, application_id): - return render_new_application_form( - "applications/new/step_2.html", - EnvironmentsForm, - portfolio_id=portfolio_id, - application_id=application_id, - ) + application = Applications.get(application_id) + render_args = { + "form": EnvironmentsForm( + data={ + "environment_names": [ + environment.name for environment in application.environments + ] + } + ), + "application": application, + } + + return render_template("applications/new/step_2.html", **render_args) @applications_bp.route( @@ -112,11 +125,11 @@ def update_new_application_step_2(portfolio_id, application_id): if form.validate(): application = Applications.get(application_id) application = Applications.update(application, form.data) - flash("application_created", application_name=application.name) + flash("application_environments_updated") return redirect( url_for( - "applications.portfolio_applications", - portfolio_id=application.portfolio_id, + "applications.update_new_application_step_3", + application_id=application_id, ) ) else: @@ -130,3 +143,32 @@ def update_new_application_step_2(portfolio_id, application_id): ), 400, ) + + +@applications_bp.route("/applications//step_3") +@user_can(Permissions.CREATE_APPLICATION, message="view create new application form") +def view_new_application_step_3(application_id): + application = Applications.get(application_id) + members = get_members_data(application) + new_member_form = get_new_member_form(application) + + return render_template( + "applications/new/step_3.html", + application_id=application_id, + application=application, + members=members, + new_member_form=new_member_form, + ) + + +@applications_bp.route("/applications//step_3", methods=["POST"]) +@user_can(Permissions.CREATE_APPLICATION, message="view create new application form") +def update_new_application_step_3(application_id): + + handle_create_member(application_id, http_request.form) + + return redirect( + url_for( + "applications.view_new_application_step_3", application_id=application_id + ) + ) diff --git a/atst/routes/applications/settings.py b/atst/routes/applications/settings.py index 74800b21..29efb1de 100644 --- a/atst/routes/applications/settings.py +++ b/atst/routes/applications/settings.py @@ -73,8 +73,9 @@ def filter_env_roles_form_data(member, environments): "environment_name": env.name, "role": NO_ACCESS, } - env_role = EnvironmentRoles.get_by_user_and_environment(member.user_id, env.id) - if env_role: + env_roles_set = set(env.roles).intersection(set(member.environment_roles)) + if len(env_roles_set) == 1: + (env_role,) = env_roles_set env_data["role"] = env_role.role env_roles_form_data.append(env_data) @@ -153,6 +154,41 @@ def send_application_invitation(invitee_email, inviter_name, token): ) +def handle_create_member(application_id, form_data): + application = Applications.get(application_id) + form = NewMemberForm(form_data) + + if form.validate(): + try: + invite = Applications.invite( + application=application, + inviter=g.current_user, + user_data=form.user_data.data, + permission_sets_names=form.data["permission_sets"], + 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, + application_name=application.name, + ) + + except AlreadyExistsError: + return render_template( + "error.html", message="There was an error processing your request." + ) + else: + pass + # TODO: flash error message + + @applications_bp.route("/applications//settings") @user_can(Permissions.VIEW_APPLICATION, message="view application edit form") def settings(application_id): @@ -283,39 +319,7 @@ def delete_environment(environment_id): 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.data["permission_sets"], - 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, - application_name=application.name, - ) - - except AlreadyExistsError: - return render_template( - "error.html", message="There was an error processing your request." - ) - else: - pass - # TODO: flash error message - + handle_create_member(application_id, http_request.form) return redirect( url_for( "applications.settings", diff --git a/templates/applications/new/step_1.html b/templates/applications/new/step_1.html index e5d95922..4c2e0fa3 100644 --- a/templates/applications/new/step_1.html +++ b/templates/applications/new/step_1.html @@ -12,13 +12,15 @@ {% set action = url_for('applications.create_new_application_step_1', portfolio_id=portfolio.id, application_id=application_id) %} {% endif %} - +{% block portfolio_header %} + {% include "portfolios/header.html" %} + {{ StickyCTA(text="Name and Describe New Application") }} +{% endblock %} + {% block application_content %} {% include "fragments/flash.html" %} -
{{ 'portfolios.applications.settings_heading' | translate }}
-
@@ -42,7 +44,7 @@ {% block next_button %} - {{ SaveButton(text=('portfolios.applications.next_button_text' | translate)) }} + {{ SaveButton(text=('portfolios.applications.new.step_1_button_text' | translate)) }} {% endblock %} diff --git a/templates/applications/new/step_2.html b/templates/applications/new/step_2.html index a737c349..fa0954e7 100644 --- a/templates/applications/new/step_2.html +++ b/templates/applications/new/step_2.html @@ -7,14 +7,17 @@ {% set secondary_breadcrumb = 'portfolios.applications.new_application_title' | translate %} +{% block portfolio_header %} + {{ StickyCTA(text=application.name) }} +{% endblock %} + {% block application_content %} {% set modalName = "newApplicationConfirmation" %} {% include "fragments/flash.html" %} -
{{ 'portfolios.applications.settings_heading' | translate }}
-
+
{{ form.csrf_token }} @@ -63,7 +66,7 @@ {% block next_button %} - {{ SaveButton(text=('portfolios.applications.create_button_text' | translate)) }} + {{ SaveButton(text=('portfolios.applications.new.step_2_button_text' | translate)) }} {% endblock %} diff --git a/templates/applications/new/step_3.html b/templates/applications/new/step_3.html new file mode 100644 index 00000000..c963beee --- /dev/null +++ b/templates/applications/new/step_3.html @@ -0,0 +1,26 @@ + +{% extends "applications/base.html" %} + +{% from "fragments/members.html" import MemberManagementTemplate %} +{% set secondary_breadcrumb = 'portfolios.applications.new_application_title' | translate %} + +{% block portfolio_header %} + {% include "portfolios/header.html" %} + {{ StickyCTA(text=application.name) }} +{% endblock %} + +{% block application_content %} + {{ MemberManagementTemplate( + application, + members, + new_member_form, + "applications.update_new_application_step_3", + user_can(permissions.CREATE_APPLICATION_MEMBER)) }} + + + + Return to Application Settings + + +{% endblock %} + diff --git a/templates/applications/settings.html b/templates/applications/settings.html index 68a84965..e4c51f2c 100644 --- a/templates/applications/settings.html +++ b/templates/applications/settings.html @@ -5,6 +5,7 @@ {% from "components/icon.html" import Icon %} {% import "applications/fragments/new_member_modal_content.html" as member_steps %} {% from "applications/fragments/member_perms_form_fields.html" import MemberPermsFields %} +{% from "fragments/members.html" import MemberManagementTemplate %} {% from "components/modal.html" import Modal %} {% from "components/multi_step_modal_form.html" import MultiStepModalForm %} {% from "components/pagination.html" import Pagination %} @@ -73,150 +74,12 @@
{% 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.settings.team_members' | translate }} - - {% set new_member_modal_name = "add-app-mem" %} - {% if user_can(permissions.CREATE_APPLICATION_MEMBER) %} - - {{ Icon("plus") }} - {{ "portfolios.applications.add_member" | translate }} - - {% endif %} -
- -
-
- {% if g.matchesPath("application-members") %} - {% include "fragments/flash.html" %} - {% endif %} - {% for member in members %} - {% set modal_name = "edit_member-{}".format(loop.index) %} - {% call Modal(modal_name) %} - - - - - {% endcall %} - - {% if user_can(permissions.DELETE_APPLICATION_MEMBER) and member.role_status == 'pending' %} - {% set revoke_invite_modal = "revoke_invite_{}".format(member.role_id) %} - {% call Modal(name=revoke_invite_modal, dismissable=True) %} -
-
- {{ member.form.csrf_token }} -

{{ "invites.revoke.modal_heading" | translate({'user_name': member.user_name}) }}

-
- - -
-
-
- {% endcall %} - {% endif %} - {% endfor %} - - - - - - - - - - - {% for member in members %} - {% set modal_name = "edit_member-{}".format(loop.index) %} - - - - - - - - {% endfor %} - -
MemberProject PermissionsEnvironment Access
- {{ member.user_name }} - - {{ Icon('edit') }} - -
- {% if member.role_status == 'pending' %} - INVITE PENDING - {% endif %} - -
- {% 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.DELETE_APPLICATION_MEMBER) and member.role_status == 'pending' %} - {% set revoke_invite_modal = "revoke_invite_{}".format(member.role_id) %} - Resend Invite
- {{ 'invites.revoke.button' | translate }} - {% endif %} -
-
- - {% if user_can(permissions.CREATE_APPLICATION_MEMBER) %} - {% import "applications/fragments/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 %} + {{ MemberManagementTemplate( + application, + members, + new_member_form, + "applications.settings", + user_can(permissions.CREATE_APPLICATION_MEMBER)) }}
{{ 'common.resource_names.environments' | translate }} diff --git a/templates/fragments/members.html b/templates/fragments/members.html new file mode 100644 index 00000000..31848326 --- /dev/null +++ b/templates/fragments/members.html @@ -0,0 +1,143 @@ +{% from "components/alert.html" import Alert %} +{% from "components/icon.html" import Icon %} +{% import "applications/fragments/new_member_modal_content.html" as member_steps %} +{% from "applications/fragments/member_perms_form_fields.html" import MemberPermsFields %} +{% from "components/modal.html" import Modal %} +{% from "components/multi_step_modal_form.html" import MultiStepModalForm %} +{% from "components/save_button.html" import SaveButton %} + +{% macro MemberManagementTemplate( + application, + members, + new_member_form, + action, + user_can_create_app_member=False +) %} + + + {% include "fragments/flash.html" %} + + {% if not application.members %} +
+

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

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

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

+ {% endif %} + + {% if user_can_create_app_member %} + {% 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(action, application_id=application.id), + steps=[ + member_steps.MemberStepOne(new_member_form), + member_steps.MemberStepTwo(new_member_form, application) + ], + ) }} + {% endif %} +
+ + {% else %} +
+ {{ 'portfolios.applications.settings.team_members' | translate }} + + {% set new_member_modal_name = "add-app-mem" %} + {% if user_can_create_app_member %} + + {{ Icon("plus") }} + {{ "portfolios.applications.add_member" | translate }} + + {% endif %} +
+ +
+
+ {% for member in members %} + {% set modal_name = "edit_member-{}".format(loop.index) %} + {% call Modal(modal_name) %} + + + + + {% endcall %} + {% endfor %} + + + + + + + + + + + {% for member in members %} + {% set modal_name = "edit_member-{}".format(loop.index) %} + + + + + + + + {% endfor %} + +
MemberProject PermissionsEnvironment Access
+ {{ member.user_name }} + + {{ Icon('edit') }} + +
+ {% if member.role_status == 'pending' %} + INVITE PENDING + {% endif %} + +
+ {% 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 member.role_status == 'pending' %} + Resend Invite
+ Revoke Invite + {% endif %} +
+
+ + {% if user_can_create_app_member %} + {% import "applications/fragments/new_member_modal_content.html" as member_steps %} + {{ MultiStepModalForm( + name=new_member_modal_name, + form=new_member_form, + form_action=url_for(action, application_id=application.id), + steps=[ + member_steps.MemberStepOne(new_member_form), + member_steps.MemberStepTwo(new_member_form, application) + ], + ) }} + {% endif %} +
+ {% endif %} + +{% endmacro %} diff --git a/tests/routes/applications/test_new.py b/tests/routes/applications/test_new.py index 22c86969..c39f48d5 100644 --- a/tests/routes/applications/test_new.py +++ b/tests/routes/applications/test_new.py @@ -1,7 +1,9 @@ from flask import url_for -from tests.factories import PortfolioFactory, ApplicationFactory -from atst.domain.applications import Applications +from tests.factories import PortfolioFactory, ApplicationFactory, UserFactory +from unittest.mock import Mock +from atst.forms.data import ENV_ROLE_NO_ACCESS as NO_ACCESS +from atst.models.application_invitation import ApplicationInvitation def test_get_name_and_description_form(client, user_session): @@ -94,3 +96,66 @@ def test_post_environments(client, session, user_session): assert response.status_code == 302 session.refresh(application) assert len(application.environments) == 3 + + +def test_get_members(client, session, user_session): + application = ApplicationFactory.create() + user_session(application.portfolio.owner) + response = client.get( + url_for( + "applications.view_new_application_step_3", application_id=application.id + ) + ) + assert response.status_code == 200 + + +def test_post_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, env_1) = application.environments + + 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, + "perms_env_mgmt": True, + "perms_team_mgmt": True, + "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 diff --git a/tests/routes/applications/test_settings.py b/tests/routes/applications/test_settings.py index 811bee36..c0b60694 100644 --- a/tests/routes/applications/test_settings.py +++ b/tests/routes/applications/test_settings.py @@ -21,6 +21,7 @@ from atst.models.portfolio_role import Status as PortfolioRoleStatus from atst.forms.application import EditEnvironmentForm from atst.forms.application_member import UpdateMemberForm from atst.forms.data import ENV_ROLE_NO_ACCESS as NO_ACCESS +from atst.routes.applications.settings import filter_env_roles_form_data from tests.utils import captured_templates @@ -559,3 +560,28 @@ def test_revoke_invite(client, user_session): assert invite.is_revoked assert app_role.status == ApplicationRoleStatus.DISABLED + + +def test_filter_environment_roles(): + application_role = ApplicationRoleFactory.create(user=None) + application_role2 = ApplicationRoleFactory.create( + user=None, application=application_role.application + ) + application_role3 = ApplicationRoleFactory.create( + user=None, application=application_role.application + ) + + environment = EnvironmentFactory.create(application=application_role.application) + + EnvironmentRoleFactory.create( + environment=environment, application_role=application_role + ) + EnvironmentRoleFactory.create( + environment=environment, application_role=application_role2 + ) + + environment_data = filter_env_roles_form_data(application_role, [environment]) + assert environment_data[0]["role"] != "No Access" + + environment_data = filter_env_roles_form_data(application_role3, [environment]) + assert environment_data[0]["role"] == "No Access" diff --git a/translations.yaml b/translations.yaml index aff4ad76..509dc8c7 100644 --- a/translations.yaml +++ b/translations.yaml @@ -302,8 +302,10 @@ portfolios: add_member: Add a New Team Member add_another_environment: Add another environment app_settings_text: App settings - create_button_text: Create - next_button_text: "Next: Environments" + new: + step_1_button_text: "Save and Add Environments" + step_2_button_text: "Save and Add Members" + step_3_button_text: Save Application 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