diff --git a/atst/domain/invitations.py b/atst/domain/invitations.py index 99b30b27..738525ab 100644 --- a/atst/domain/invitations.py +++ b/atst/domain/invitations.py @@ -4,6 +4,8 @@ from sqlalchemy.orm.exc import NoResultFound from atst.database import db from atst.models.invitation import Invitation, Status as InvitationStatus from atst.domain.workspace_roles import WorkspaceRoles +from atst.domain.authz import Authorization, Permissions +from atst.domain.workspaces import Workspaces from .exceptions import NotFoundError @@ -104,3 +106,18 @@ class Invitations(object): def revoke(cls, token): invite = Invitations._get(token) return Invitations._update_status(invite, InvitationStatus.REVOKED) + + @classmethod + def resend(cls, user, workspace_id, token): + workspace = Workspaces.get(user, workspace_id) + Authorization.check_workspace_permission( + user, + workspace, + Permissions.ASSIGN_AND_UNASSIGN_ATAT_ROLE, + "resend a workspace invitation", + ) + + previous_invitation = Invitations._get(token) + Invitations._update_status(previous_invitation, InvitationStatus.REVOKED) + + return Invitations.create(user, previous_invitation.workspace_role) diff --git a/atst/models/workspace_role.py b/atst/models/workspace_role.py index ad223c5f..c006b87a 100644 --- a/atst/models/workspace_role.py +++ b/atst/models/workspace_role.py @@ -109,6 +109,12 @@ class WorkspaceRole(Base, mixins.TimestampsMixin, mixins.AuditableMixin): def has_environment_roles(self): return self.num_environment_roles > 0 + @property + def can_resend_invitation(self): + return self.latest_invitation and ( + self.latest_invitation.is_rejected or self.latest_invitation.is_expired + ) + Index( "workspace_role_user_workspace", diff --git a/atst/routes/workspaces.py b/atst/routes/workspaces.py index a8f6493d..e1d6c321 100644 --- a/atst/routes/workspaces.py +++ b/atst/routes/workspaces.py @@ -374,3 +374,9 @@ def revoke_invitation(workspace_id, token): Invitations.revoke(token) return redirect(url_for("workspaces.workspace_members", workspace_id=workspace.id)) + + +@bp.route("/workspaces//invitations//resend", methods=["POST"]) +def resend_invitation(workspace_id, token): + Invitations.resend(g.current_user, workspace_id, token) + return redirect(url_for("workspaces.workspace_members", workspace_id=workspace_id)) diff --git a/templates/workspaces/members/edit.html b/templates/workspaces/members/edit.html index e840ffef..fcaf9553 100644 --- a/templates/workspaces/members/edit.html +++ b/templates/workspaces/members/edit.html @@ -39,6 +39,7 @@ {% if editable %} edit account details {% endif %} +
{% if member.latest_invitation.is_pending %} {{ ConfirmationButton( "Revoke Invitation", @@ -46,6 +47,15 @@ form.csrf_token ) }} {% endif %} + {% if member.can_resend_invitation %} + {{ ConfirmationButton ( + "Resend Invitation", + url_for("workspaces.resend_invitation", workspace_id=workspace.id, token=member.latest_invitation.token), + form.csrf_token, + confirm_msg="Are you sure? This will invalidate the previously sent invitation." + )}} + {% endif %} +
diff --git a/tests/domain/test_invitations.py b/tests/domain/test_invitations.py index cb2adfe5..e63258e8 100644 --- a/tests/domain/test_invitations.py +++ b/tests/domain/test_invitations.py @@ -103,3 +103,13 @@ def test_revoke_invitation(): assert invite.is_pending Invitations.revoke(invite.token) assert invite.is_revoked + + +def test_resend_invitation(): + workspace = WorkspaceFactory.create() + user = UserFactory.create() + ws_role = WorkspaceRoleFactory.create(user=user, workspace=workspace) + invite = Invitations.create(workspace.owner, ws_role) + Invitations.resend(workspace.owner, workspace.id, invite.token) + assert ws_role.invitations[0].is_revoked + assert ws_role.invitations[1].is_pending diff --git a/tests/models/test_workspace_role.py b/tests/models/test_workspace_role.py index 573b67ef..3622e88a 100644 --- a/tests/models/test_workspace_role.py +++ b/tests/models/test_workspace_role.py @@ -96,3 +96,17 @@ def test_status_when_invitation_is_expired(): ] ) assert workspace_role.display_status == "Invite expired" + + +def test_can_not_resend_invitation_if_active(): + workspace_role = WorkspaceRoleFactory.create( + invitations=[InvitationFactory.create(status=InvitationStatus.ACCEPTED)] + ) + assert not workspace_role.can_resend_invitation + + +def test_can_resend_invitation_if_expired(): + workspace_role = WorkspaceRoleFactory.create( + invitations=[InvitationFactory.create(status=InvitationStatus.REJECTED_EXPIRED)] + ) + assert workspace_role.can_resend_invitation