From 4d043363a7f8bf3b06f1ec27e73e4eefe593b88d Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Mon, 30 Sep 2019 13:52:39 -0400 Subject: [PATCH] Create route for resending an app invite Replace ApplicationInvitations._update_status() with revoke() because multiple functions used _update_status() and it was causing app roles to be disabled when they shouldn't have. Now app roles are disabled within the revoke function. Updated Invitations.resend() to accept user details so the invite info can be changed in the new invite --- .secrets.baseline | 4 +- atst/domain/invitations.py | 26 ++++++++----- atst/routes/applications/settings.py | 44 ++++++++++++++++++++++ atst/utils/flash.py | 5 +++ tests/routes/applications/test_settings.py | 31 +++++++++++++++ tests/test_access.py | 18 +++++++++ 6 files changed, 117 insertions(+), 11 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index 22f467a5..a203d223 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -3,7 +3,7 @@ "files": "^.secrets.baseline$", "lines": null }, - "generated_at": "2019-10-02T14:53:58Z", + "generated_at": "2019-09-30T16:06:53Z", "plugins_used": [ { "base64_limit": 4.5, @@ -194,7 +194,7 @@ "hashed_secret": "e4f14805dfd1e6af030359090c535e149e6b4207", "is_secret": false, "is_verified": false, - "line_number": 638, + "line_number": 651, "type": "Hex High Entropy String" } ] diff --git a/atst/domain/invitations.py b/atst/domain/invitations.py index 6b68db92..17f21c91 100644 --- a/atst/domain/invitations.py +++ b/atst/domain/invitations.py @@ -117,22 +117,30 @@ class BaseInvitations(object): return cls._update_status(invite, InvitationStatus.REVOKED) @classmethod - def resend(cls, inviter, token): + def resend(cls, inviter, token, user_info=None): previous_invitation = cls._get(token) cls._update_status(previous_invitation, InvitationStatus.REVOKED) - return cls.create( - inviter, - previous_invitation.role, - { + if user_info: + user_details = { + "email": user_info["email"], + "dod_id": user_info["dod_id"], + "first_name": user_info["first_name"], + "last_name": user_info["last_name"], + "phone_number": user_info["phone_number"], + "phone_ext": user_info["phone_ext"], + } + else: + user_details = { "email": previous_invitation.email, "dod_id": previous_invitation.dod_id, "first_name": previous_invitation.first_name, "last_name": previous_invitation.last_name, - "phone_number": previous_invitation.last_name, - }, - commit=True, - ) + "phone_number": previous_invitation.phone_number, + "phone_ext": previous_invitation.phone_ext, + } + + return cls.create(inviter, previous_invitation.role, user_details, commit=True) class PortfolioInvitations(BaseInvitations): diff --git a/atst/routes/applications/settings.py b/atst/routes/applications/settings.py index 29efb1de..87b5dd7f 100644 --- a/atst/routes/applications/settings.py +++ b/atst/routes/applications/settings.py @@ -12,6 +12,7 @@ from atst.domain.invitations import ApplicationInvitations from atst.forms.application_member import NewForm as NewMemberForm, UpdateMemberForm from atst.forms.application import NameAndDescriptionForm, EditEnvironmentForm from atst.forms.data import ENV_ROLE_NO_ACCESS as NO_ACCESS +from atst.forms.member import NewForm as MemberForm from atst.domain.authz.decorator import user_can_access_decorator as user_can from atst.models.permissions import Permissions from atst.domain.permission_sets import PermissionSets @@ -419,3 +420,46 @@ def revoke_invite(application_id, application_role_id): _anchor="application-members", ) ) + + +@applications_bp.route( + "/applications//members//resend_invite", + methods=["POST"], +) +@user_can(Permissions.EDIT_APPLICATION_MEMBER, message="resend application invitation") +def resend_invite(application_id, application_role_id): + app_role = ApplicationRoles.get_by_id(application_role_id) + invite = app_role.latest_invitation + form = MemberForm(http_request.form) + + if form.validate(): + new_invite = ApplicationInvitations.resend( + g.current_user, invite.token, form.data + ) + + send_application_invitation( + invitee_email=new_invite.email, + inviter_name=g.current_user.full_name, + token=new_invite.token, + ) + + flash( + "application_invite_resent", + user_name=new_invite.user_name, + application_name=app_role.application.name, + ) + else: + flash( + "application_invite_error", + user_name=app_role.user_name, + application_name=g.application.name, + ) + + return redirect( + url_for( + "applications.settings", + application_id=application_id, + fragment="application-members", + _anchor="application-members", + ) + ) diff --git a/atst/utils/flash.py b/atst/utils/flash.py index 613b55bf..623306d5 100644 --- a/atst/utils/flash.py +++ b/atst/utils/flash.py @@ -39,6 +39,11 @@ MESSAGES = { "message_template": "There was an error processing the invitation for {{ user_name }} from {{ application_name }}", "category": "error", }, + "application_invite_resent": { + "title_template": "Application invitation revoked", + "message_template": "You have successfully resent the invite for {{ user_name }} from {{ application_name }}", + "category": "success", + }, "application_invite_revoked": { "title_template": "Application invitation revoked", "message_template": "You have successfully revoked the invite for {{ user_name }} from {{ application_name }}", diff --git a/tests/routes/applications/test_settings.py b/tests/routes/applications/test_settings.py index 57077b15..c610f939 100644 --- a/tests/routes/applications/test_settings.py +++ b/tests/routes/applications/test_settings.py @@ -2,6 +2,7 @@ import pytest import uuid from flask import url_for, get_flashed_messages from unittest.mock import Mock +import datetime from tests.factories import * @@ -591,3 +592,33 @@ def test_filter_environment_roles(): environment_data = filter_env_roles_form_data(application_role3, [environment]) assert environment_data[0]["role"] == "No Access" + + def test_resend_invite(client, user_session, session): + user = UserFactory.create() + # need to set the time created to yesterday, otherwise the original invite and resent + # invite have the same time_created and then we can't rely on time to order the invites + yesterday = datetime.date.today() - datetime.timedelta(days=1) + invite = ApplicationInvitationFactory.create(user=user, time_created=yesterday) + app_role = invite.role + application = app_role.application + + user_session(application.portfolio.owner) + response = client.post( + url_for( + "applications.resend_invite", + application_id=application.id, + application_role_id=app_role.id, + ), + data={ + "first_name": user.first_name, + "last_name": user.last_name, + "dod_id": user.dod_id, + "email": "an_email@example.com", + }, + ) + + session.refresh(app_role) + assert response.status_code == 302 + assert invite.is_revoked + assert app_role.status == ApplicationRoleStatus.PENDING + assert app_role.latest_invitation.email == "an_email@example.com" diff --git a/tests/test_access.py b/tests/test_access.py index 283823d2..c72ac36d 100644 --- a/tests/test_access.py +++ b/tests/test_access.py @@ -595,6 +595,24 @@ def test_applications_revoke_invite(post_url_assert_status): post_url_assert_status(user, url, status) +# applications.resend_invite +def test_applications_resend_invite(post_url_assert_status): + ccpo = UserFactory.create_ccpo() + rando = UserFactory.create() + application = ApplicationFactory.create() + + for user, status in [(ccpo, 302), (application.portfolio.owner, 302), (rando, 404)]: + app_role = ApplicationRoleFactory.create() + invite = ApplicationInvitationFactory.create(role=app_role) + + url = url_for( + "applications.resend_invite", + application_id=application.id, + application_role_id=app_role.id, + ) + post_url_assert_status(user, url, status) + + # task_orders.download_task_order_pdf def test_task_orders_download_task_order_pdf_access(get_url_assert_status, monkeypatch): monkeypatch.setattr(