more granular invitation status and a display status for workspace members
This commit is contained in:
parent
97475ee64f
commit
e4bad109db
39
alembic/versions/e1081cf01780_adjust_invitation_status.py
Normal file
39
alembic/versions/e1081cf01780_adjust_invitation_status.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
"""adjust invitation status
|
||||||
|
|
||||||
|
Revision ID: e1081cf01780
|
||||||
|
Revises: a9d8c6b6221c
|
||||||
|
Create Date: 2018-11-01 12:24:10.970963
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from atst.models.invitation import Status
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'e1081cf01780'
|
||||||
|
down_revision = 'a9d8c6b6221c'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
conn = op.get_bind()
|
||||||
|
constraints = ", ".join(["'{}'::character varying::text".format(s.name) for s in Status])
|
||||||
|
conn.execute("ALTER TABLE invitations ALTER COLUMN status TYPE varchar(30)")
|
||||||
|
conn.execute("ALTER TABLE invitations DROP CONSTRAINT status")
|
||||||
|
conn.execute("ALTER TABLE invitations ADD CONSTRAINT status CHECK(status::text = ANY (ARRAY[{}]))".format(constraints))
|
||||||
|
|
||||||
|
class PreviousStatus(Enum):
|
||||||
|
ACCEPTED = "accepted"
|
||||||
|
REVOKED = "revoked"
|
||||||
|
PENDING = "pending"
|
||||||
|
REJECTED = "rejected"
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
conn = op.get_bind()
|
||||||
|
constraints = ", ".join(["'{}'::character varying::text".format(s.name) for s in PreviousStatus])
|
||||||
|
conn.execute("ALTER TABLE invitations ALTER COLUMN status TYPE varchar(8)")
|
||||||
|
conn.execute("ALTER TABLE invitations DROP CONSTRAINT status")
|
||||||
|
conn.execute("ALTER TABLE invitations ADD CONSTRAINT status CHECK(status::text = ANY (ARRAY[{}]))".format(constraints))
|
@ -71,11 +71,11 @@ class Invitations(object):
|
|||||||
|
|
||||||
if invite.user.dod_id != user.dod_id:
|
if invite.user.dod_id != user.dod_id:
|
||||||
if invite.is_pending:
|
if invite.is_pending:
|
||||||
Invitations._update_status(invite, InvitationStatus.REJECTED)
|
Invitations._update_status(invite, InvitationStatus.REJECTED_WRONG_USER)
|
||||||
raise WrongUserError(user, invite)
|
raise WrongUserError(user, invite)
|
||||||
|
|
||||||
elif invite.is_expired:
|
elif invite.is_expired:
|
||||||
Invitations._update_status(invite, InvitationStatus.REJECTED)
|
Invitations._update_status(invite, InvitationStatus.REJECTED_EXPIRED)
|
||||||
raise ExpiredError(invite)
|
raise ExpiredError(invite)
|
||||||
|
|
||||||
elif invite.is_accepted or invite.is_revoked or invite.is_rejected:
|
elif invite.is_accepted or invite.is_revoked or invite.is_rejected:
|
||||||
|
@ -4,7 +4,7 @@ import secrets
|
|||||||
|
|
||||||
from sqlalchemy import Column, ForeignKey, Enum as SQLAEnum, TIMESTAMP, String
|
from sqlalchemy import Column, ForeignKey, Enum as SQLAEnum, TIMESTAMP, String
|
||||||
from sqlalchemy.dialects.postgresql import UUID
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship, backref
|
||||||
|
|
||||||
from atst.models import Base, types
|
from atst.models import Base, types
|
||||||
from atst.models.mixins.timestamps import TimestampsMixin
|
from atst.models.mixins.timestamps import TimestampsMixin
|
||||||
@ -15,7 +15,8 @@ class Status(Enum):
|
|||||||
ACCEPTED = "accepted"
|
ACCEPTED = "accepted"
|
||||||
REVOKED = "revoked"
|
REVOKED = "revoked"
|
||||||
PENDING = "pending"
|
PENDING = "pending"
|
||||||
REJECTED = "rejected"
|
REJECTED_WRONG_USER = "rejected_wrong_user"
|
||||||
|
REJECTED_EXPIRED = "rejected_expired"
|
||||||
|
|
||||||
|
|
||||||
class Invitation(Base, TimestampsMixin, AuditableMixin):
|
class Invitation(Base, TimestampsMixin, AuditableMixin):
|
||||||
@ -29,7 +30,10 @@ class Invitation(Base, TimestampsMixin, AuditableMixin):
|
|||||||
workspace_role_id = Column(
|
workspace_role_id = Column(
|
||||||
UUID(as_uuid=True), ForeignKey("workspace_roles.id"), index=True
|
UUID(as_uuid=True), ForeignKey("workspace_roles.id"), index=True
|
||||||
)
|
)
|
||||||
workspace_role = relationship("WorkspaceRole", backref="invitations")
|
workspace_role = relationship(
|
||||||
|
"WorkspaceRole",
|
||||||
|
backref=backref("invitations", order_by="Invitation.time_created"),
|
||||||
|
)
|
||||||
|
|
||||||
inviter_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), index=True)
|
inviter_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), index=True)
|
||||||
inviter = relationship("User", backref="sent_invites", foreign_keys=[inviter_id])
|
inviter = relationship("User", backref="sent_invites", foreign_keys=[inviter_id])
|
||||||
@ -59,7 +63,15 @@ class Invitation(Base, TimestampsMixin, AuditableMixin):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def is_rejected(self):
|
def is_rejected(self):
|
||||||
return self.status == Status.REJECTED
|
return self.status in [Status.REJECTED_WRONG_USER, Status.REJECTED_EXPIRED]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_rejected_expired(self):
|
||||||
|
return self.status == Status.REJECTED_EXPIRED
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_rejected_wrong_user(self):
|
||||||
|
return self.status == Status.REJECTED_WRONG_USER
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_expired(self):
|
def is_expired(self):
|
||||||
|
@ -36,18 +36,26 @@ class WorkspaceRole(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
|
|||||||
self.role.name, self.workspace.name, self.user_id, self.id
|
self.role.name, self.workspace.name, self.user_id, self.id
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def latest_invitation(self):
|
||||||
|
if self.invitations:
|
||||||
|
return self.invitations[-1]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def display_status(self):
|
def display_status(self):
|
||||||
import ipdb; ipdb.set_trace()
|
|
||||||
if self.status == Status.ACTIVE:
|
if self.status == Status.ACTIVE:
|
||||||
return "Active"
|
return "Active"
|
||||||
else:
|
elif self.latest_invitation:
|
||||||
if any(i.is_expired for i in self.invitations):
|
if self.latest_invitation.is_rejected_expired:
|
||||||
return "Invitation Expired"
|
return "Invite expired"
|
||||||
elif any(i.is_rejected for i in self.invitations):
|
elif self.latest_invitation.is_rejected_wrong_user:
|
||||||
return "Invitation Rejected"
|
return "Error on invite"
|
||||||
|
elif self.latest_invitation.is_expired:
|
||||||
|
return "Invite expired"
|
||||||
else:
|
else:
|
||||||
return "Pending"
|
return "Pending"
|
||||||
|
else:
|
||||||
|
return "Unknown errors"
|
||||||
|
|
||||||
|
|
||||||
Index(
|
Index(
|
||||||
|
@ -55,7 +55,7 @@ def test_accept_expired_invitation():
|
|||||||
|
|
||||||
def test_accept_rejected_invite():
|
def test_accept_rejected_invite():
|
||||||
user = UserFactory.create()
|
user = UserFactory.create()
|
||||||
invite = InvitationFactory.create(user_id=user.id, status=Status.REJECTED)
|
invite = InvitationFactory.create(user_id=user.id, status=Status.REJECTED_EXPIRED)
|
||||||
with pytest.raises(InvitationError):
|
with pytest.raises(InvitationError):
|
||||||
Invitations.accept(user, invite.token)
|
Invitations.accept(user, invite.token)
|
||||||
|
|
||||||
|
@ -1,9 +1,17 @@
|
|||||||
|
import datetime
|
||||||
|
|
||||||
from atst.domain.environments import Environments
|
from atst.domain.environments import Environments
|
||||||
from atst.domain.workspaces import Workspaces
|
from atst.domain.workspaces import Workspaces
|
||||||
from atst.domain.projects import Projects
|
from atst.domain.projects import Projects
|
||||||
from atst.domain.workspace_users import WorkspaceUsers
|
from atst.domain.workspace_users import WorkspaceUsers
|
||||||
from atst.models.invitation import Status
|
from atst.models.workspace_role import Status
|
||||||
from tests.factories import RequestFactory, UserFactory, InvitationFactory, WorkspaceRoleFactory
|
from atst.models.invitation import Status as InvitationStatus
|
||||||
|
from tests.factories import (
|
||||||
|
RequestFactory,
|
||||||
|
UserFactory,
|
||||||
|
InvitationFactory,
|
||||||
|
WorkspaceRoleFactory,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_has_no_environment_roles():
|
def test_has_no_environment_roles():
|
||||||
@ -57,8 +65,34 @@ def test_role_displayname():
|
|||||||
assert workspace_user.role_displayname == "Developer"
|
assert workspace_user.role_displayname == "Developer"
|
||||||
|
|
||||||
|
|
||||||
def test_status_when_member_has_pending_invitation():
|
def test_status_when_member_is_active():
|
||||||
|
workspace_role = WorkspaceRoleFactory.create(status=Status.ACTIVE)
|
||||||
|
assert workspace_role.display_status == "Active"
|
||||||
|
|
||||||
|
|
||||||
|
def test_status_when_invitation_has_been_rejected_for_expirations():
|
||||||
workspace_role = WorkspaceRoleFactory.create(
|
workspace_role = WorkspaceRoleFactory.create(
|
||||||
invitations=[InvitationFactory.create(status=Status.ACCEPTED)]
|
invitations=[InvitationFactory.create(status=InvitationStatus.REJECTED_EXPIRED)]
|
||||||
)
|
)
|
||||||
assert workspace_role.display_status == "Accepted"
|
assert workspace_role.display_status == "Invite expired"
|
||||||
|
|
||||||
|
|
||||||
|
def test_status_when_invitation_has_been_rejected_for_wrong_user():
|
||||||
|
workspace_role = WorkspaceRoleFactory.create(
|
||||||
|
invitations=[
|
||||||
|
InvitationFactory.create(status=InvitationStatus.REJECTED_WRONG_USER)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
assert workspace_role.display_status == "Error on invite"
|
||||||
|
|
||||||
|
|
||||||
|
def test_status_when_invitation_is_expired():
|
||||||
|
workspace_role = WorkspaceRoleFactory.create(
|
||||||
|
invitations=[
|
||||||
|
InvitationFactory.create(
|
||||||
|
status=InvitationStatus.PENDING,
|
||||||
|
expiration_time=datetime.datetime.now() - datetime.timedelta(seconds=1),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
assert workspace_role.display_status == "Invite expired"
|
||||||
|
@ -363,7 +363,9 @@ def test_member_accepts_invalid_invite(client, user_session):
|
|||||||
user=user, workspace=workspace, status=WorkspaceRoleStatus.PENDING
|
user=user, workspace=workspace, status=WorkspaceRoleStatus.PENDING
|
||||||
)
|
)
|
||||||
invite = InvitationFactory.create(
|
invite = InvitationFactory.create(
|
||||||
user_id=user.id, workspace_role_id=ws_role.id, status=InvitationStatus.REJECTED
|
user_id=user.id,
|
||||||
|
workspace_role_id=ws_role.id,
|
||||||
|
status=InvitationStatus.REJECTED_WRONG_USER,
|
||||||
)
|
)
|
||||||
user_session(user)
|
user_session(user)
|
||||||
response = client.get(url_for("workspaces.accept_invitation", token=invite.token))
|
response = client.get(url_for("workspaces.accept_invitation", token=invite.token))
|
||||||
@ -411,7 +413,7 @@ def test_user_accepts_expired_invite(client, user_session):
|
|||||||
invite = InvitationFactory.create(
|
invite = InvitationFactory.create(
|
||||||
user_id=user.id,
|
user_id=user.id,
|
||||||
workspace_role_id=ws_role.id,
|
workspace_role_id=ws_role.id,
|
||||||
status=InvitationStatus.REJECTED,
|
status=InvitationStatus.REJECTED_EXPIRED,
|
||||||
expiration_time=datetime.datetime.now() - datetime.timedelta(seconds=1),
|
expiration_time=datetime.datetime.now() - datetime.timedelta(seconds=1),
|
||||||
)
|
)
|
||||||
user_session(user)
|
user_session(user)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user