add officers to task order and redirect to TO when they accept a workspace invite

This commit is contained in:
dandds 2019-01-04 11:14:50 -05:00
parent 33de14caaf
commit d0bfa16f17
7 changed files with 137 additions and 5 deletions

View File

@ -0,0 +1,38 @@
"""add officers to task order
Revision ID: 71cbe76c3b87
Revises: 6172ac7b8b26
Create Date: 2019-01-04 10:16:50.062349
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '71cbe76c3b87'
down_revision = '6172ac7b8b26'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('task_orders', sa.Column('cor_id', postgresql.UUID(as_uuid=True), nullable=True))
op.add_column('task_orders', sa.Column('ko_id', postgresql.UUID(as_uuid=True), nullable=True))
op.add_column('task_orders', sa.Column('so_id', postgresql.UUID(as_uuid=True), nullable=True))
op.create_foreign_key(None, 'task_orders', 'users', ['so_id'], ['id'])
op.create_foreign_key(None, 'task_orders', 'users', ['cor_id'], ['id'])
op.create_foreign_key(None, 'task_orders', 'users', ['ko_id'], ['id'])
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'task_orders', type_='foreignkey')
op.drop_constraint(None, 'task_orders', type_='foreignkey')
op.drop_constraint(None, 'task_orders', type_='foreignkey')
op.drop_column('task_orders', 'so_id')
op.drop_column('task_orders', 'ko_id')
op.drop_column('task_orders', 'cor_id')
# ### end Alembic commands ###

View File

@ -4,6 +4,9 @@ from atst.database import db
from atst.models.task_order import TaskOrder
from .exceptions import NotFoundError
class TaskOrderError(Exception):
pass
class TaskOrders(object):
SECTIONS = {
@ -89,3 +92,17 @@ class TaskOrders(object):
return False
return True
OFFICERS = ["contracting_officer", "contracting_officer_representative", "security_officer"]
@classmethod
def add_officer(cls, task_order, user, role):
if role in TaskOrders.OFFICERS:
setattr(task_order, role, user)
db.session.add(task_order)
db.session.commit()
return task_order
else:
raise TaskOrderError("{} is not an officer role on task orders".format(role))

View File

@ -14,7 +14,18 @@ class TaskOrder(Base, mixins.TimestampsMixin):
workspace = relationship("Workspace")
user_id = Column(ForeignKey("users.id"))
creator = relationship("User")
creator = relationship("User", foreign_keys="TaskOrder.user_id")
ko_id = Column(ForeignKey("users.id"))
contracting_officer = relationship("User", foreign_keys="TaskOrder.ko_id")
cor_id = Column(ForeignKey("users.id"))
contracting_officer_representative = relationship(
"User", foreign_keys="TaskOrder.cor_id"
)
so_id = Column(ForeignKey("users.id"))
security_officer = relationship("User", foreign_keys="TaskOrder.so_id")
scope = Column(String) # Cloud Project Scope
defense_component = Column(String) # Department of Defense Component

View File

@ -154,7 +154,7 @@ class UpdateTaskOrderWorkflow(ShowTaskOrderWorkflow):
},
]
def _update_invitations(self):
def _update_officer_invitations(self):
for officer_type in self.OFFICER_INVITATIONS:
field = officer_type["field"]
if (
@ -169,16 +169,19 @@ class UpdateTaskOrderWorkflow(ShowTaskOrderWorkflow):
}
invite_service = InvitationService(
self.user,
self.task_order.workspace,
self.workspace,
{**officer_data, "workspace_role": officer_type["role"]},
subject=officer_type["subject"],
email_template=officer_type["template"],
)
invite_service.invite()
invite = invite_service.invite()
TaskOrders.add_officer(
self.task_order, invite.user, officer_type["role"]
)
def update(self):
self._update_task_order()
self._update_invitations()
self._update_officer_invitations()
return self.task_order

View File

@ -20,6 +20,25 @@ def send_invite_email(owner_name, token, new_member_email):
def accept_invitation(token):
invite = Invitations.accept(g.current_user, token)
# TODO: this will eventually redirect to different places depending on
# whether the user is an officer for the TO and what kind of officer they
# are. It will also have to manage cases like:
# - the logged-in user has multiple roles on the TO (e.g., KO and COR)
# - the logged-in user has officer roles on multiple unsigned TOs
for task_order in invite.workspace.task_orders:
if g.current_user == task_order.contracting_officer:
return redirect(
url_for("task_orders.new", screen=4, task_order_id=task_order.id)
)
elif g.current_user == task_order.contracting_officer_representative:
return redirect(
url_for("task_orders.new", screen=4, task_order_id=task_order.id)
)
elif g.current_user == task_order.security_officer:
return redirect(
url_for("task_orders.new", screen=4, task_order_id=task_order.id)
)
return redirect(
url_for("workspaces.show_workspace", workspace_id=invite.workspace.id)
)

View File

@ -146,12 +146,19 @@ def test_invite_officers_to_task_order(queue):
)
workflow.update()
workspace = task_order.workspace
# owner and three officers are workspace members
assert len(workspace.members) == 4
roles = [member.role.name for member in workspace.members]
# officers exist in roles
assert "contracting_officer" in roles
assert "contracting_officer_representative" in roles
assert "security_officer" in roles
# email invitations are enqueued
assert len(queue.get_queue()) == 3
# task order has relationship to user for each officer role
assert task_order.contracting_officer.dod_id == to_data["ko_dod_id"]
assert task_order.contracting_officer_representative.dod_id == to_data["cor_dod_id"]
assert task_order.security_officer.dod_id == to_data["so_dod_id"]
def test_update_does_not_resend_invitation():

View File

@ -6,6 +6,7 @@ from tests.factories import (
WorkspaceFactory,
WorkspaceRoleFactory,
InvitationFactory,
TaskOrderFactory,
)
from atst.domain.workspaces import Workspaces
from atst.models.workspace_role import Status as WorkspaceRoleStatus
@ -207,3 +208,39 @@ def test_existing_member_invite_resent_to_email_submitted_in_form(
assert user.email != "example@example.com"
assert send_mail_job.func.__func__.__name__ == "_send_mail"
assert send_mail_job.args[0] == ["example@example.com"]
def test_task_order_officer_accepts_invite(monkeypatch, client, user_session):
workspace = WorkspaceFactory.create()
task_order = TaskOrderFactory.create(workspace=workspace)
user_info = UserFactory.dictionary()
# create contracting officer
user_session(workspace.owner)
client.post(
url_for("task_orders.new", screen=3, task_order_id=task_order.id),
data={
"workspace_role": "contracting_officer",
"ko_first_name": user_info["first_name"],
"ko_last_name": user_info["last_name"],
"ko_email": user_info["email"],
"ko_dod_id": user_info["dod_id"],
"ko_invite": True,
},
)
# contracting officer accepts invitation
user = Users.get_by_dod_id(user_info["dod_id"])
token = user.invitations[0].token
monkeypatch.setattr(
"atst.domain.auth.should_redirect_to_user_profile", lambda *args: False
)
user_session(user)
response = client.get(url_for("workspaces.accept_invitation", token=token))
# user is redirected to the task order review page
assert response.status_code == 302
to_review_url = url_for(
"task_orders.new", screen=4, task_order_id=task_order.id, _external=True
)
assert response.headers["Location"] == to_review_url