Merge pull request #440 from dod-ccpo/resend-invitation

Resend a workspace invitation
This commit is contained in:
richard-dds 2018-11-13 14:46:01 -05:00 committed by GitHub
commit 1b4d054d7e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 212 additions and 14 deletions

View File

@ -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)

View File

@ -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

View File

@ -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",

View File

@ -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

View File

@ -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,
)
)

View File

@ -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,

View File

@ -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>

View File

@ -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 %}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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