Merge pull request #440 from dod-ccpo/resend-invitation
Resend a workspace invitation
This commit is contained in:
commit
1b4d054d7e
@ -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
|
||||
|
||||
@ -52,11 +54,11 @@ class Invitations(object):
|
||||
return invite
|
||||
|
||||
@classmethod
|
||||
def create(cls, workspace_role, inviter, user):
|
||||
def create(cls, inviter, workspace_role):
|
||||
invite = Invitation(
|
||||
workspace_role=workspace_role,
|
||||
inviter=inviter,
|
||||
user=user,
|
||||
user=workspace_role.user,
|
||||
status=InvitationStatus.PENDING,
|
||||
expiration_time=Invitations.current_expiration_time(),
|
||||
)
|
||||
@ -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)
|
||||
|
@ -81,3 +81,11 @@ class Invitation(Base, TimestampsMixin, AuditableMixin):
|
||||
def workspace(self):
|
||||
if self.workspace_role:
|
||||
return self.workspace_role.workspace
|
||||
|
||||
@property
|
||||
def user_email(self):
|
||||
return self.workspace_role.user.email
|
||||
|
||||
@property
|
||||
def user_name(self):
|
||||
return self.workspace_role.user.full_name
|
||||
|
@ -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",
|
||||
|
@ -7,10 +7,13 @@ from flask import (
|
||||
url_for,
|
||||
current_app as app,
|
||||
)
|
||||
import pendulum
|
||||
|
||||
from . import redirect_after_login_url
|
||||
from atst.domain.users import Users
|
||||
from atst.queue import queue
|
||||
from tests.factories import random_service_branch
|
||||
from atst.utils import pick
|
||||
|
||||
bp = Blueprint("dev", __name__)
|
||||
|
||||
@ -21,6 +24,11 @@ _DEV_USERS = {
|
||||
"last_name": "Stevenson",
|
||||
"atat_role_name": "ccpo",
|
||||
"email": "sam@example.com",
|
||||
"service_branch": random_service_branch(),
|
||||
"phone_number": "1234567890",
|
||||
"citizenship": "United States",
|
||||
"designation": "Military",
|
||||
"date_latest_training": pendulum.date(2018, 1, 1),
|
||||
},
|
||||
"amanda": {
|
||||
"dod_id": "2345678901",
|
||||
@ -28,6 +36,11 @@ _DEV_USERS = {
|
||||
"last_name": "Adamson",
|
||||
"atat_role_name": "default",
|
||||
"email": "amanda@example.com",
|
||||
"service_branch": random_service_branch(),
|
||||
"phone_number": "1234567890",
|
||||
"citizenship": "United States",
|
||||
"designation": "Military",
|
||||
"date_latest_training": pendulum.date(2018, 1, 1),
|
||||
},
|
||||
"brandon": {
|
||||
"dod_id": "3456789012",
|
||||
@ -35,6 +48,11 @@ _DEV_USERS = {
|
||||
"last_name": "Buchannan",
|
||||
"atat_role_name": "default",
|
||||
"email": "brandon@example.com",
|
||||
"service_branch": random_service_branch(),
|
||||
"phone_number": "1234567890",
|
||||
"citizenship": "United States",
|
||||
"designation": "Military",
|
||||
"date_latest_training": pendulum.date(2018, 1, 1),
|
||||
},
|
||||
"christina": {
|
||||
"dod_id": "4567890123",
|
||||
@ -42,6 +60,11 @@ _DEV_USERS = {
|
||||
"last_name": "Collins",
|
||||
"atat_role_name": "default",
|
||||
"email": "christina@example.com",
|
||||
"service_branch": random_service_branch(),
|
||||
"phone_number": "1234567890",
|
||||
"citizenship": "United States",
|
||||
"designation": "Military",
|
||||
"date_latest_training": pendulum.date(2018, 1, 1),
|
||||
},
|
||||
"dominick": {
|
||||
"dod_id": "5678901234",
|
||||
@ -49,6 +72,11 @@ _DEV_USERS = {
|
||||
"last_name": "Domingo",
|
||||
"atat_role_name": "default",
|
||||
"email": "dominick@example.com",
|
||||
"service_branch": random_service_branch(),
|
||||
"phone_number": "1234567890",
|
||||
"citizenship": "United States",
|
||||
"designation": "Military",
|
||||
"date_latest_training": pendulum.date(2018, 1, 1),
|
||||
},
|
||||
"erica": {
|
||||
"dod_id": "6789012345",
|
||||
@ -56,6 +84,11 @@ _DEV_USERS = {
|
||||
"last_name": "Eichner",
|
||||
"atat_role_name": "default",
|
||||
"email": "erica@example.com",
|
||||
"service_branch": random_service_branch(),
|
||||
"phone_number": "1234567890",
|
||||
"citizenship": "United States",
|
||||
"designation": "Military",
|
||||
"date_latest_training": pendulum.date(2018, 1, 1),
|
||||
},
|
||||
}
|
||||
|
||||
@ -66,10 +99,20 @@ def login_dev():
|
||||
user_data = _DEV_USERS[role]
|
||||
user = Users.get_or_create_by_dod_id(
|
||||
user_data["dod_id"],
|
||||
atat_role_name=user_data["atat_role_name"],
|
||||
first_name=user_data["first_name"],
|
||||
last_name=user_data["last_name"],
|
||||
email=user_data["email"],
|
||||
**pick(
|
||||
[
|
||||
"atat_role_name",
|
||||
"first_name",
|
||||
"last_name",
|
||||
"email",
|
||||
"service_branch",
|
||||
"phone_number",
|
||||
"citizenship",
|
||||
"designation",
|
||||
"date_latest_training",
|
||||
],
|
||||
user_data,
|
||||
),
|
||||
)
|
||||
session["user_id"] = user.id
|
||||
|
||||
|
@ -103,6 +103,7 @@ def show_workspace(workspace_id):
|
||||
def workspace_members(workspace_id):
|
||||
workspace = Workspaces.get_with_members(g.current_user, workspace_id)
|
||||
new_member_name = http_request.args.get("newMemberName")
|
||||
resent_invitation_to = http_request.args.get("resentInvitationTo")
|
||||
new_member = next(
|
||||
filter(lambda m: m.user_name == new_member_name, workspace.members), None
|
||||
)
|
||||
@ -127,6 +128,7 @@ def workspace_members(workspace_id):
|
||||
status_choices=MEMBER_STATUSES,
|
||||
members=members_list,
|
||||
new_member=new_member,
|
||||
resent_invitation_to=resent_invitation_to,
|
||||
)
|
||||
|
||||
|
||||
@ -255,11 +257,12 @@ def send_invite_email(owner_name, token, new_member_email):
|
||||
def create_member(workspace_id):
|
||||
workspace = Workspaces.get(g.current_user, workspace_id)
|
||||
form = NewMemberForm(http_request.form)
|
||||
user = g.current_user
|
||||
|
||||
if form.validate():
|
||||
try:
|
||||
new_member = Workspaces.create_member(g.current_user, workspace, form.data)
|
||||
invite = Invitations.create(new_member, g.current_user, new_member.user)
|
||||
new_member = Workspaces.create_member(user, workspace, form.data)
|
||||
invite = Invitations.create(user, new_member)
|
||||
send_invite_email(
|
||||
g.current_user.full_name, invite.token, new_member.user.email
|
||||
)
|
||||
@ -373,3 +376,16 @@ def revoke_invitation(workspace_id, token):
|
||||
Invitations.revoke(token)
|
||||
|
||||
return redirect(url_for("workspaces.workspace_members", workspace_id=workspace.id))
|
||||
|
||||
|
||||
@bp.route("/workspaces/<workspace_id>/invitations/<token>/resend", methods=["POST"])
|
||||
def resend_invitation(workspace_id, token):
|
||||
invite = Invitations.resend(g.current_user, workspace_id, token)
|
||||
send_invite_email(g.current_user.full_name, invite.token, invite.user_email)
|
||||
return redirect(
|
||||
url_for(
|
||||
"workspaces.workspace_members",
|
||||
workspace_id=workspace_id,
|
||||
resentInvitationTo=invite.user_name,
|
||||
)
|
||||
)
|
||||
|
@ -12,8 +12,9 @@ from atst.domain.requests import Requests
|
||||
from atst.domain.workspaces import Workspaces
|
||||
from atst.domain.projects import Projects
|
||||
from atst.domain.workspace_roles import WorkspaceRoles
|
||||
from atst.models.invitation import Status as InvitationStatus
|
||||
from atst.domain.exceptions import AlreadyExistsError
|
||||
from tests.factories import RequestFactory, TaskOrderFactory
|
||||
from tests.factories import RequestFactory, TaskOrderFactory, InvitationFactory
|
||||
from atst.routes.dev import _DEV_USERS as DEV_USERS
|
||||
|
||||
WORKSPACE_USERS = [
|
||||
@ -40,6 +41,41 @@ WORKSPACE_USERS = [
|
||||
},
|
||||
]
|
||||
|
||||
WORKSPACE_INVITED_USERS = [
|
||||
{
|
||||
"first_name": "Frederick",
|
||||
"last_name": "Fitzgerald",
|
||||
"email": "frederick@mil.gov",
|
||||
"workspace_role": "developer",
|
||||
"dod_id": "0000000004",
|
||||
"status": InvitationStatus.REJECTED_WRONG_USER
|
||||
},
|
||||
{
|
||||
"first_name": "Gina",
|
||||
"last_name": "Guzman",
|
||||
"email": "gina@mil.gov",
|
||||
"workspace_role": "developer",
|
||||
"dod_id": "0000000005",
|
||||
"status": InvitationStatus.REJECTED_EXPIRED
|
||||
},
|
||||
{
|
||||
"first_name": "Hector",
|
||||
"last_name": "Harper",
|
||||
"email": "hector@mil.gov",
|
||||
"workspace_role": "developer",
|
||||
"dod_id": "0000000006",
|
||||
"status": InvitationStatus.REVOKED
|
||||
},
|
||||
{
|
||||
"first_name": "Isabella",
|
||||
"last_name": "Ingram",
|
||||
"email": "isabella@mil.gov",
|
||||
"workspace_role": "developer",
|
||||
"dod_id": "0000000007",
|
||||
"status": InvitationStatus.PENDING
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def seed_db():
|
||||
users = []
|
||||
@ -78,6 +114,13 @@ def seed_db():
|
||||
ws_role = Workspaces.create_member(user, workspace, workspace_role)
|
||||
WorkspaceRoles.enable(ws_role)
|
||||
|
||||
for workspace_role in WORKSPACE_INVITED_USERS:
|
||||
ws_role = Workspaces.create_member(user, workspace, workspace_role)
|
||||
invitation = InvitationFactory.build(workspace_role=ws_role, status=workspace_role["status"])
|
||||
db.session.add(invitation)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
Projects.create(
|
||||
user,
|
||||
workspace=workspace,
|
||||
|
@ -39,6 +39,7 @@
|
||||
{% if editable %}
|
||||
<a href='{{ url_for("users.user") }}' class='icon-link'>edit account details</a>
|
||||
{% endif %}
|
||||
<div>
|
||||
{% 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 send an email to invite the user to join this workspace."
|
||||
)}}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -33,6 +33,17 @@
|
||||
) }}
|
||||
{% endif %}
|
||||
|
||||
{% if resent_invitation_to %}
|
||||
{% set message -%}
|
||||
<p>Successfully sent a new invitation to {{ resent_invitation_to }}.</p>
|
||||
{%- endset %}
|
||||
|
||||
{{ Alert('Invitation resent',
|
||||
message=message,
|
||||
level='success'
|
||||
) }}
|
||||
{% endif %}
|
||||
|
||||
{% set member_name = request.args.get("memberName") %}
|
||||
{% set updated_role = request.args.get("updatedRole") %}
|
||||
{% if updated_role %}
|
||||
|
@ -22,7 +22,7 @@ def test_create_invitation():
|
||||
workspace = WorkspaceFactory.create()
|
||||
user = UserFactory.create()
|
||||
ws_role = WorkspaceRoleFactory.create(user=user, workspace=workspace)
|
||||
invite = Invitations.create(ws_role, workspace.owner, user)
|
||||
invite = Invitations.create(workspace.owner, ws_role)
|
||||
assert invite.user == user
|
||||
assert invite.workspace_role == ws_role
|
||||
assert invite.inviter == workspace.owner
|
||||
@ -34,7 +34,7 @@ def test_accept_invitation():
|
||||
workspace = WorkspaceFactory.create()
|
||||
user = UserFactory.create()
|
||||
ws_role = WorkspaceRoleFactory.create(user=user, workspace=workspace)
|
||||
invite = Invitations.create(ws_role, workspace.owner, user)
|
||||
invite = Invitations.create(workspace.owner, ws_role)
|
||||
assert invite.is_pending
|
||||
accepted_invite = Invitations.accept(user, invite.token)
|
||||
assert accepted_invite.is_accepted
|
||||
@ -89,7 +89,7 @@ def test_accept_invitation_twice():
|
||||
workspace = WorkspaceFactory.create()
|
||||
user = UserFactory.create()
|
||||
ws_role = WorkspaceRoleFactory.create(user=user, workspace=workspace)
|
||||
invite = Invitations.create(ws_role, workspace.owner, user)
|
||||
invite = Invitations.create(workspace.owner, ws_role)
|
||||
Invitations.accept(user, invite.token)
|
||||
with pytest.raises(InvitationError):
|
||||
Invitations.accept(user, invite.token)
|
||||
@ -99,7 +99,17 @@ def test_revoke_invitation():
|
||||
workspace = WorkspaceFactory.create()
|
||||
user = UserFactory.create()
|
||||
ws_role = WorkspaceRoleFactory.create(user=user, workspace=workspace)
|
||||
invite = Invitations.create(ws_role, workspace.owner, user)
|
||||
invite = Invitations.create(workspace.owner, ws_role)
|
||||
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
|
||||
|
@ -21,7 +21,6 @@ from atst.domain.roles import Roles
|
||||
from atst.models.workspace_role import WorkspaceRole, Status as WorkspaceRoleStatus
|
||||
from atst.models.environment_role import EnvironmentRole
|
||||
from atst.models.invitation import Invitation, Status as InvitationStatus
|
||||
from atst.domain.workspaces import Workspaces
|
||||
from atst.domain.invitations import Invitations
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -457,3 +457,24 @@ def test_revoke_invitation(client, user_session):
|
||||
|
||||
assert response.status_code == 302
|
||||
assert invite.is_revoked
|
||||
|
||||
|
||||
def test_resend_invitation_sends_email(client, user_session, queue):
|
||||
user = UserFactory.create()
|
||||
workspace = WorkspaceFactory.create()
|
||||
ws_role = WorkspaceRoleFactory.create(
|
||||
user=user, workspace=workspace, status=WorkspaceRoleStatus.PENDING
|
||||
)
|
||||
invite = InvitationFactory.create(
|
||||
user_id=user.id, workspace_role_id=ws_role.id, status=InvitationStatus.PENDING
|
||||
)
|
||||
user_session(workspace.owner)
|
||||
client.post(
|
||||
url_for(
|
||||
"workspaces.resend_invitation",
|
||||
workspace_id=workspace.id,
|
||||
token=invite.token,
|
||||
)
|
||||
)
|
||||
|
||||
assert len(queue.get_queue()) == 1
|
||||
|
Loading…
x
Reference in New Issue
Block a user