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(