From cceb6bf868c688b44b655422ced4f5e725179494 Mon Sep 17 00:00:00 2001 From: dandds Date: Thu, 3 Jan 2019 13:00:34 -0500 Subject: [PATCH 01/17] checkbox fields for inviting KO, etc, on task order form --- atst/forms/task_order.py | 28 ++++++++++++++++++++++++ templates/task_orders/new/oversight.html | 5 ++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/atst/forms/task_order.py b/atst/forms/task_order.py index 28063a5c..64a3d84a 100644 --- a/atst/forms/task_order.py +++ b/atst/forms/task_order.py @@ -1,4 +1,5 @@ from wtforms.fields import ( + BooleanField, IntegerField, RadioField, SelectField, @@ -96,6 +97,33 @@ class OversightForm(CacheableForm): so_email = StringField("Email") so_dod_id = StringField("DOD ID") + ko_invite = BooleanField( + "Invite KO to Task Order Builder", + description=""" + Your KO will need to approve funding for this Task Order by logging + into the JEDI Cloud Portal, submitting the Task Order documents + within their official system of record, and electronically signing. + You may choose to skip this for now and invite them later. + """, + ) + cor_invite = BooleanField( + "Invite COR to Task Order Builder", + description=""" + Your COR may assist with submitting the Task Order documents within + their official system of record. You may choose to skip this for + now and invite them later. + """, + ) + so_invite = BooleanField( + "Invite Security Officer to Task Order Builder", + description=""" + Your Security Officer will need to answer some security + configuration questions in order to generate a DD-254 document, + then electronically sign. You may choose to skip this for now + and invite them later. + """, + ) + class ReviewForm(CacheableForm): pass diff --git a/templates/task_orders/new/oversight.html b/templates/task_orders/new/oversight.html index 42c316e9..59ea3797 100644 --- a/templates/task_orders/new/oversight.html +++ b/templates/task_orders/new/oversight.html @@ -1,6 +1,7 @@ {% extends 'task_orders/_new.html' %} {% from "components/user_info.html" import UserInfo %} +{% from "components/checkbox_input.html" import CheckboxInput %} {% block heading %} Oversight @@ -12,13 +13,15 @@

Contracting Officer (KO) Information

- {{ UserInfo(form.ko_first_name, form.ko_last_name, form.ko_email, form.ko_dod_id) }} +{{ CheckboxInput(form.ko_invite) }}

Contractive Officer Representative (COR) Information

{{ UserInfo(form.cor_first_name, form.cor_last_name, form.cor_email, form.cor_dod_id) }} +{{ CheckboxInput(form.cor_invite) }}

Security Officer Information

{{ UserInfo(form.so_first_name, form.so_last_name, form.so_email, form.so_dod_id) }} +{{ CheckboxInput(form.so_invite) }} {% endblock %} From c3cd11cb9ad84b152cf777f2249a7177d30a0400 Mon Sep 17 00:00:00 2001 From: dandds Date: Thu, 3 Jan 2019 13:40:21 -0500 Subject: [PATCH 02/17] service class for making and sending invitations --- atst/routes/workspaces/members.py | 9 +++---- atst/services/invitation.py | 45 +++++++++++++++++++++++++++++++ tests/services/test_invitation.py | 17 ++++++++++++ 3 files changed, 65 insertions(+), 6 deletions(-) create mode 100644 atst/services/invitation.py create mode 100644 tests/services/test_invitation.py diff --git a/atst/routes/workspaces/members.py b/atst/routes/workspaces/members.py index f3f06d4e..7b463d03 100644 --- a/atst/routes/workspaces/members.py +++ b/atst/routes/workspaces/members.py @@ -3,13 +3,13 @@ import re from flask import render_template, request as http_request, g, redirect, url_for from . import workspaces_bp -from atst.routes.workspaces.invitations import send_invite_email from atst.domain.exceptions import AlreadyExistsError from atst.domain.projects import Projects from atst.domain.workspaces import Workspaces from atst.domain.workspace_roles import WorkspaceRoles, MEMBER_STATUS_CHOICES from atst.domain.environments import Environments from atst.domain.environment_roles import EnvironmentRoles +from atst.services.invitation import Invitation as InvitationService from atst.forms.new_member import NewMemberForm from atst.forms.edit_member import EditMemberForm from atst.forms.data import ( @@ -19,7 +19,6 @@ from atst.forms.data import ( ) from atst.domain.authz import Authorization from atst.models.permissions import Permissions -from atst.domain.invitations import Invitations from atst.utils.flash import formatted_flash as flash @@ -68,13 +67,11 @@ def new_member(workspace_id): 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(user, workspace, form.data) - invite = Invitations.create(user, new_member, form.data["email"]) - send_invite_email(g.current_user.full_name, invite.token, invite.email) + invite_service = InvitationService(g.current_user, workspace, form.data) + invite_service.invite() flash("new_workspace_member", new_member=new_member, workspace=workspace) diff --git a/atst/services/invitation.py b/atst/services/invitation.py new file mode 100644 index 00000000..1df4cefb --- /dev/null +++ b/atst/services/invitation.py @@ -0,0 +1,45 @@ +# send invite to KO +# create KO user (Workspaces.create_member) +# needs workspace role name +# new_member = Workspaces.create_member(user, workspace, ws_role_name) +# invite = Invitations.create(user, new_member, form.data["email"]) +# send_invite_email(g.current_user.full_name, invite.token, invite.email) +from flask import render_template + +from atst.domain.workspaces import Workspaces +from atst.domain.invitations import Invitations +from atst.queue import queue + + +class Invitation: + def __init__( + self, + inviter, + workspace, + user_info, + subject="{} has invited you to a JEDI Cloud Workspace", + email_template="emails/invitation.txt", + ): + self.inviter = inviter + self.workspace = workspace + self.user_info = user_info + self.subject = subject + self.email_template = email_template + + def invite(self): + member = self._create_member() + email = self.user_info.get("email") + invite = self._create_invite(member, email) + self._send_invite_email(invite.token, email) + + return invite + + def _create_member(self): + return Workspaces.create_member(self.inviter, self.workspace, self.user_info) + + def _create_invite(self, member, email): + return Invitations.create(self.inviter, member, email) + + def _send_invite_email(self, token, email): + body = render_template(self.email_template, owner=self.inviter, token=token) + queue.send_mail([email], self.subject.format(self.inviter), body) diff --git a/tests/services/test_invitation.py b/tests/services/test_invitation.py new file mode 100644 index 00000000..ef15db02 --- /dev/null +++ b/tests/services/test_invitation.py @@ -0,0 +1,17 @@ +from tests.factories import UserFactory, WorkspaceFactory + +from atst.services.invitation import Invitation + + +def test_invite_member(queue): + inviter = UserFactory.create() + new_member = UserFactory.create() + workspace = WorkspaceFactory.create(owner=inviter) + invite_service = Invitation( + inviter, + workspace, + {**new_member.to_dictionary(), "workspace_role": "developer"}, + ) + new_invitation = invite_service.invite() + assert new_invitation == new_member.invitations[0] + assert len(queue.get_queue()) == 1 From 4f1ca550d5620c1afef3b800b20752c46c190bf5 Mon Sep 17 00:00:00 2001 From: dandds Date: Thu, 3 Jan 2019 13:56:46 -0500 Subject: [PATCH 03/17] new roles for task order officers --- atst/domain/roles.py | 18 ++++++++++++++++++ atst/models/permissions.py | 3 +++ 2 files changed, 21 insertions(+) diff --git a/atst/domain/roles.py b/atst/domain/roles.py index 25c08e8a..b11d2552 100644 --- a/atst/domain/roles.py +++ b/atst/domain/roles.py @@ -140,6 +140,24 @@ WORKSPACE_ROLES = [ Permissions.VIEW_WORKSPACE, ], }, + { + "name": "contracting_officer", + "description": "Can approve funding for a task order. Has view and edit permissions for task orders in a workspace.", + "display_name": "Contracting Officer", + "permissions": [Permissions.VIEW_WORKSPACE, Permissions.KO_SIGN_TASK_ORDER], + }, + { + "name": "contracting_officer_representative", + "description": "Assists in submitting task order documents. Can update Task Order.", + "display_name": "Contracting Officer Representative", + "permissions": [Permissions.VIEW_WORKSPACE], + }, + { + "name": "security_officer", + "description": "Can edit security questions for a task order.", + "display_name": "Contracting Officer", + "permissions": [Permissions.VIEW_WORKSPACE, Permissions.SO_SIGN_TASK_ORDER], + }, ] diff --git a/atst/models/permissions.py b/atst/models/permissions.py index f7adc406..112517a3 100644 --- a/atst/models/permissions.py +++ b/atst/models/permissions.py @@ -42,3 +42,6 @@ class Permissions(object): ADD_TAG_TO_WORKSPACE = "add_tag_to_workspace" REMOVE_TAG_FROM_WORKSPACE = "remove_tag_from_workspace" + + KO_SIGN_TASK_ORDER = "ko_sign_task_order" + SO_SIGN_TASK_ORDER = "so_sign_task_order" From 55bfbc469670afab0496a051105f3f4019ccc853 Mon Sep 17 00:00:00 2001 From: dandds Date: Thu, 3 Jan 2019 14:56:21 -0500 Subject: [PATCH 04/17] invitations for task order officers --- atst/routes/task_orders/new.py | 48 ++++++++++++++++++- atst/services/invitation.py | 10 ++-- .../routes/task_orders/test_new_task_order.py | 25 +++++++++- 3 files changed, 74 insertions(+), 9 deletions(-) diff --git a/atst/routes/task_orders/new.py b/atst/routes/task_orders/new.py index c4d68e5a..bf6b3c3f 100644 --- a/atst/routes/task_orders/new.py +++ b/atst/routes/task_orders/new.py @@ -11,6 +11,7 @@ from . import task_orders_bp from atst.domain.task_orders import TaskOrders from atst.domain.workspaces import Workspaces import atst.forms.task_order as task_order_form +from atst.services.invitation import Invitation as InvitationService TASK_ORDER_SECTIONS = [ @@ -114,7 +115,7 @@ class UpdateTaskOrderWorkflow(ShowTaskOrderWorkflow): def validate(self): return self.form.validate() - def update(self): + def _update_task_order(self): if self.task_order: TaskOrders.update(self.task_order, **self.form.data) else: @@ -124,6 +125,51 @@ class UpdateTaskOrderWorkflow(ShowTaskOrderWorkflow): self._task_order = TaskOrders.create(workspace=ws, creator=self.user) TaskOrders.update(self.task_order, **to_data) + OFFICER_INVITATIONS = [ + { + "field": "ko_invite", + "prefix": "ko", + "role": "contracting_officer", + "subject": "Review a task order", + "template": "emails/invitation.txt", + }, + { + "field": "cor_invite", + "prefix": "cor", + "role": "contracting_officer_representative", + "subject": "Help with a task order", + "template": "emails/invitation.txt", + }, + { + "field": "so_invite", + "prefix": "so", + "role": "security_officer", + "subject": "Review security for a task order", + "template": "emails/invitation.txt", + }, + ] + + def _update_invitations(self): + for officer_type in self.OFFICER_INVITATIONS: + field = officer_type["field"] + if hasattr(self.form, field) and self.form[field].data: + prefix = officer_type["prefix"] + officer_data = { + field: getattr(self.task_order, prefix + "_" + field) + for field in ["first_name", "last_name", "email", "dod_id"] + } + invite_service = InvitationService( + self.user, + self.task_order.workspace, + {**officer_data, "workspace_role": officer_type["role"]}, + subject=officer_type["subject"], + email_template=officer_type["template"], + ) + invite_service.invite() + + def update(self): + self._update_task_order() + self._update_invitations() return self.task_order diff --git a/atst/services/invitation.py b/atst/services/invitation.py index 1df4cefb..f8023078 100644 --- a/atst/services/invitation.py +++ b/atst/services/invitation.py @@ -1,9 +1,3 @@ -# send invite to KO -# create KO user (Workspaces.create_member) -# needs workspace role name -# new_member = Workspaces.create_member(user, workspace, ws_role_name) -# invite = Invitations.create(user, new_member, form.data["email"]) -# send_invite_email(g.current_user.full_name, invite.token, invite.email) from flask import render_template from atst.domain.workspaces import Workspaces @@ -41,5 +35,7 @@ class Invitation: return Invitations.create(self.inviter, member, email) def _send_invite_email(self, token, email): - body = render_template(self.email_template, owner=self.inviter, token=token) + body = render_template( + self.email_template, owner=self.inviter.full_name, token=token + ) queue.send_mail([email], self.subject.format(self.inviter), body) diff --git a/tests/routes/task_orders/test_new_task_order.py b/tests/routes/task_orders/test_new_task_order.py index 09412b3d..c364a0b5 100644 --- a/tests/routes/task_orders/test_new_task_order.py +++ b/tests/routes/task_orders/test_new_task_order.py @@ -4,7 +4,7 @@ from flask import url_for from atst.domain.task_orders import TaskOrders from atst.routes.task_orders.new import ShowTaskOrderWorkflow, UpdateTaskOrderWorkflow -from tests.factories import UserFactory, TaskOrderFactory +from tests.factories import UserFactory, TaskOrderFactory, WorkspaceFactory def test_new_task_order(client, user_session): @@ -129,3 +129,26 @@ def test_update_task_order_with_existing_task_order(): assert workflow.task_order.start_date != to_data["start_date"] workflow.update() assert workflow.task_order.start_date.strftime("%m/%d/%Y") == to_data["start_date"] + + +def test_invite_officers_to_task_order(queue): + user = UserFactory.create() + workspace = WorkspaceFactory.create(owner=user) + task_order = TaskOrderFactory.create(creator=user, workspace=workspace) + to_data = { + **TaskOrderFactory.dictionary(), + "ko_invite": True, + "cor_invite": True, + "so_invite": True, + } + workflow = UpdateTaskOrderWorkflow( + to_data, user, screen=3, task_order_id=task_order.id + ) + workflow.update() + workspace = task_order.workspace + assert len(workspace.members) == 4 + roles = [member.role.name for member in workspace.members] + assert "contracting_officer" in roles + assert "contracting_officer_representative" in roles + assert "security_officer" in roles + assert len(queue.get_queue()) == 3 From 33de14caaf962243211e3ab2af137f001b51c0b9 Mon Sep 17 00:00:00 2001 From: dandds Date: Thu, 3 Jan 2019 16:25:21 -0500 Subject: [PATCH 05/17] prevent redundant invitations for task order officers --- atst/models/workspace.py | 18 +++++++++++++++++ atst/routes/task_orders/new.py | 11 +++++++++- .../routes/task_orders/test_new_task_order.py | 20 +++++++++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/atst/models/workspace.py b/atst/models/workspace.py index d02acd19..126b6fd8 100644 --- a/atst/models/workspace.py +++ b/atst/models/workspace.py @@ -63,3 +63,21 @@ class Workspace(Base, mixins.TimestampsMixin, mixins.AuditableMixin): return "".format( self.name, self.request_id, self.user_count, self.id ) + + def _find_by_role(self, role): + try: + return [member for member in self.members if member.role.name == role] + except StopIteration: + return None + + @property + def contracting_officer(self): + return self._find_by_role("contracting_officer") + + @property + def contracting_officer_representative(self): + return self._find_by_role("contracting_officer_representative") + + @property + def security_officer(self): + return self._find_by_role("security_officer") diff --git a/atst/routes/task_orders/new.py b/atst/routes/task_orders/new.py index bf6b3c3f..ea996b29 100644 --- a/atst/routes/task_orders/new.py +++ b/atst/routes/task_orders/new.py @@ -112,6 +112,11 @@ class UpdateTaskOrderWorkflow(ShowTaskOrderWorkflow): def form(self): return self._section["form"](self.form_data) + @property + def workspace(self): + if self.task_order: + return self.task_order.workspace + def validate(self): return self.form.validate() @@ -152,7 +157,11 @@ class UpdateTaskOrderWorkflow(ShowTaskOrderWorkflow): def _update_invitations(self): for officer_type in self.OFFICER_INVITATIONS: field = officer_type["field"] - if hasattr(self.form, field) and self.form[field].data: + if ( + hasattr(self.form, field) + and self.form[field].data + and not getattr(self.workspace, officer_type["role"]) + ): prefix = officer_type["prefix"] officer_data = { field: getattr(self.task_order, prefix + "_" + field) diff --git a/tests/routes/task_orders/test_new_task_order.py b/tests/routes/task_orders/test_new_task_order.py index c364a0b5..a6404853 100644 --- a/tests/routes/task_orders/test_new_task_order.py +++ b/tests/routes/task_orders/test_new_task_order.py @@ -152,3 +152,23 @@ def test_invite_officers_to_task_order(queue): assert "contracting_officer_representative" in roles assert "security_officer" in roles assert len(queue.get_queue()) == 3 + + +def test_update_does_not_resend_invitation(): + user = UserFactory.create() + contracting_officer = UserFactory.create() + workspace = WorkspaceFactory.create(owner=user) + task_order = TaskOrderFactory.create( + creator=user, + workspace=workspace, + ko_first_name=contracting_officer.first_name, + ko_last_name=contracting_officer.last_name, + ko_dod_id=contracting_officer.dod_id, + ) + to_data = {**task_order.to_dictionary(), "ko_invite": True} + workflow = UpdateTaskOrderWorkflow( + to_data, user, screen=3, task_order_id=task_order.id + ) + for i in range(2): + workflow.update() + assert len(contracting_officer.invitations) == 1 From d0bfa16f1755a0f2b6e72bc725a63fed7a479765 Mon Sep 17 00:00:00 2001 From: dandds Date: Fri, 4 Jan 2019 11:14:50 -0500 Subject: [PATCH 06/17] add officers to task order and redirect to TO when they accept a workspace invite --- ...71cbe76c3b87_add_officers_to_task_order.py | 38 +++++++++++++++++++ atst/domain/task_orders.py | 17 +++++++++ atst/models/task_order.py | 13 ++++++- atst/routes/task_orders/new.py | 11 ++++-- atst/routes/workspaces/invitations.py | 19 ++++++++++ .../routes/task_orders/test_new_task_order.py | 7 ++++ tests/routes/workspaces/test_invitations.py | 37 ++++++++++++++++++ 7 files changed, 137 insertions(+), 5 deletions(-) create mode 100644 alembic/versions/71cbe76c3b87_add_officers_to_task_order.py diff --git a/alembic/versions/71cbe76c3b87_add_officers_to_task_order.py b/alembic/versions/71cbe76c3b87_add_officers_to_task_order.py new file mode 100644 index 00000000..a90ca5ba --- /dev/null +++ b/alembic/versions/71cbe76c3b87_add_officers_to_task_order.py @@ -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 ### diff --git a/atst/domain/task_orders.py b/atst/domain/task_orders.py index a7b800d5..1fa2ac4d 100644 --- a/atst/domain/task_orders.py +++ b/atst/domain/task_orders.py @@ -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)) diff --git a/atst/models/task_order.py b/atst/models/task_order.py index 29b220d2..27b6cc06 100644 --- a/atst/models/task_order.py +++ b/atst/models/task_order.py @@ -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 diff --git a/atst/routes/task_orders/new.py b/atst/routes/task_orders/new.py index ea996b29..ea01786f 100644 --- a/atst/routes/task_orders/new.py +++ b/atst/routes/task_orders/new.py @@ -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 diff --git a/atst/routes/workspaces/invitations.py b/atst/routes/workspaces/invitations.py index 129c87bc..af1d423e 100644 --- a/atst/routes/workspaces/invitations.py +++ b/atst/routes/workspaces/invitations.py @@ -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) ) diff --git a/tests/routes/task_orders/test_new_task_order.py b/tests/routes/task_orders/test_new_task_order.py index a6404853..eded7da0 100644 --- a/tests/routes/task_orders/test_new_task_order.py +++ b/tests/routes/task_orders/test_new_task_order.py @@ -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(): diff --git a/tests/routes/workspaces/test_invitations.py b/tests/routes/workspaces/test_invitations.py index d6e99ee0..1fcc4145 100644 --- a/tests/routes/workspaces/test_invitations.py +++ b/tests/routes/workspaces/test_invitations.py @@ -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 From 356a0fab2ca6a71baf1cefe66afb185ba581f6c4 Mon Sep 17 00:00:00 2001 From: dandds Date: Fri, 4 Jan 2019 12:27:54 -0500 Subject: [PATCH 07/17] TaskOrders domain responsible for adding officers --- atst/domain/task_orders.py | 21 ++++++++++++++++----- atst/routes/task_orders/new.py | 14 ++++++++------ atst/routes/workspaces/members.py | 5 ++++- atst/services/invitation.py | 26 ++++++++++---------------- tests/domain/test_task_orders.py | 23 +++++++++++++++++++++-- tests/services/test_invitation.py | 9 +++------ 6 files changed, 62 insertions(+), 36 deletions(-) diff --git a/atst/domain/task_orders.py b/atst/domain/task_orders.py index 1fa2ac4d..68e53885 100644 --- a/atst/domain/task_orders.py +++ b/atst/domain/task_orders.py @@ -2,8 +2,10 @@ from sqlalchemy.orm.exc import NoResultFound from atst.database import db from atst.models.task_order import TaskOrder +from atst.domain.workspaces import Workspaces from .exceptions import NotFoundError + class TaskOrderError(Exception): pass @@ -93,16 +95,25 @@ class TaskOrders(object): return True - OFFICERS = ["contracting_officer", "contracting_officer_representative", "security_officer"] + OFFICERS = [ + "contracting_officer", + "contracting_officer_representative", + "security_officer", + ] @classmethod - def add_officer(cls, task_order, user, role): + def add_officer(cls, user, task_order, role, officer_data): if role in TaskOrders.OFFICERS: - setattr(task_order, role, user) + member = Workspaces.create_member( + user, task_order.workspace, {**officer_data, "workspace_role": role} + ) + setattr(task_order, role, member.user) db.session.add(task_order) db.session.commit() - return task_order + return member.user else: - raise TaskOrderError("{} is not an officer role on task orders".format(role)) + raise TaskOrderError( + "{} is not an officer role on task orders".format(role) + ) diff --git a/atst/routes/task_orders/new.py b/atst/routes/task_orders/new.py index ea01786f..fb77cff6 100644 --- a/atst/routes/task_orders/new.py +++ b/atst/routes/task_orders/new.py @@ -10,6 +10,7 @@ from flask import ( from . import task_orders_bp from atst.domain.task_orders import TaskOrders from atst.domain.workspaces import Workspaces +from atst.domain.workspace_roles import WorkspaceRoles import atst.forms.task_order as task_order_form from atst.services.invitation import Invitation as InvitationService @@ -167,17 +168,18 @@ class UpdateTaskOrderWorkflow(ShowTaskOrderWorkflow): field: getattr(self.task_order, prefix + "_" + field) for field in ["first_name", "last_name", "email", "dod_id"] } + officer = TaskOrders.add_officer( + self.user, self.task_order, officer_type["role"], officer_data + ) + ws_officer_member = WorkspaceRoles.get(self.workspace.id, officer.id) invite_service = InvitationService( self.user, - self.workspace, - {**officer_data, "workspace_role": officer_type["role"]}, + ws_officer_member, + officer_data["email"], subject=officer_type["subject"], email_template=officer_type["template"], ) - invite = invite_service.invite() - TaskOrders.add_officer( - self.task_order, invite.user, officer_type["role"] - ) + invite_service.invite() def update(self): self._update_task_order() diff --git a/atst/routes/workspaces/members.py b/atst/routes/workspaces/members.py index 7b463d03..7c590153 100644 --- a/atst/routes/workspaces/members.py +++ b/atst/routes/workspaces/members.py @@ -70,7 +70,10 @@ def create_member(workspace_id): if form.validate(): try: - invite_service = InvitationService(g.current_user, workspace, form.data) + member = Workspaces.create_member(g.current_user, workspace, form.data) + invite_service = InvitationService( + g.current_user, member, form.data.get("email") + ) invite_service.invite() flash("new_workspace_member", new_member=new_member, workspace=workspace) diff --git a/atst/services/invitation.py b/atst/services/invitation.py index f8023078..34012711 100644 --- a/atst/services/invitation.py +++ b/atst/services/invitation.py @@ -1,6 +1,5 @@ from flask import render_template -from atst.domain.workspaces import Workspaces from atst.domain.invitations import Invitations from atst.queue import queue @@ -9,33 +8,28 @@ class Invitation: def __init__( self, inviter, - workspace, - user_info, + member, + email, subject="{} has invited you to a JEDI Cloud Workspace", email_template="emails/invitation.txt", ): self.inviter = inviter - self.workspace = workspace - self.user_info = user_info + self.member = member + self.email = email self.subject = subject self.email_template = email_template def invite(self): - member = self._create_member() - email = self.user_info.get("email") - invite = self._create_invite(member, email) - self._send_invite_email(invite.token, email) + invite = self._create_invite() + self._send_invite_email(invite.token) return invite - def _create_member(self): - return Workspaces.create_member(self.inviter, self.workspace, self.user_info) + def _create_invite(self): + return Invitations.create(self.inviter, self.member, self.email) - def _create_invite(self, member, email): - return Invitations.create(self.inviter, member, email) - - def _send_invite_email(self, token, email): + def _send_invite_email(self, token): body = render_template( self.email_template, owner=self.inviter.full_name, token=token ) - queue.send_mail([email], self.subject.format(self.inviter), body) + queue.send_mail([self.email], self.subject.format(self.inviter), body) diff --git a/tests/domain/test_task_orders.py b/tests/domain/test_task_orders.py index 67d0b040..f558207b 100644 --- a/tests/domain/test_task_orders.py +++ b/tests/domain/test_task_orders.py @@ -1,8 +1,8 @@ import pytest -from atst.domain.task_orders import TaskOrders +from atst.domain.task_orders import TaskOrders, TaskOrderError -from tests.factories import TaskOrderFactory +from tests.factories import TaskOrderFactory, UserFactory def test_is_section_complete(): @@ -29,3 +29,22 @@ def test_all_sections_complete(): assert not TaskOrders.all_sections_complete(task_order) task_order.scope = "str12345" assert TaskOrders.all_sections_complete(task_order) + + +def test_add_officer(): + task_order = TaskOrderFactory.create() + ko = UserFactory.create() + owner = task_order.workspace.owner + TaskOrders.add_officer(owner, task_order, "contracting_officer", ko.to_dictionary()) + + assert task_order.contracting_officer == ko + workspace_users = [ws_role.user for ws_role in task_order.workspace.members] + assert ko in workspace_users + + +def test_add_officer_with_nonexistent_role(): + task_order = TaskOrderFactory.create() + ko = UserFactory.create() + owner = task_order.workspace.owner + with pytest.raises(TaskOrderError): + TaskOrders.add_officer(owner, task_order, "pilot", ko.to_dictionary()) diff --git a/tests/services/test_invitation.py b/tests/services/test_invitation.py index ef15db02..cbb5dc2f 100644 --- a/tests/services/test_invitation.py +++ b/tests/services/test_invitation.py @@ -1,4 +1,4 @@ -from tests.factories import UserFactory, WorkspaceFactory +from tests.factories import UserFactory, WorkspaceFactory, WorkspaceRoleFactory from atst.services.invitation import Invitation @@ -7,11 +7,8 @@ def test_invite_member(queue): inviter = UserFactory.create() new_member = UserFactory.create() workspace = WorkspaceFactory.create(owner=inviter) - invite_service = Invitation( - inviter, - workspace, - {**new_member.to_dictionary(), "workspace_role": "developer"}, - ) + ws_member = WorkspaceRoleFactory.create(user=new_member, workspace=workspace) + invite_service = Invitation(inviter, ws_member, new_member.email) new_invitation = invite_service.invite() assert new_invitation == new_member.invitations[0] assert len(queue.get_queue()) == 1 From 65a88da63bc95a4ab2ebd54fb0dcc2d2b57c343a Mon Sep 17 00:00:00 2001 From: dandds Date: Fri, 4 Jan 2019 13:30:41 -0500 Subject: [PATCH 08/17] remove unnecessary officer properties on workspace --- atst/models/workspace.py | 18 ------------------ atst/routes/task_orders/new.py | 2 +- 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/atst/models/workspace.py b/atst/models/workspace.py index 126b6fd8..d02acd19 100644 --- a/atst/models/workspace.py +++ b/atst/models/workspace.py @@ -63,21 +63,3 @@ class Workspace(Base, mixins.TimestampsMixin, mixins.AuditableMixin): return "".format( self.name, self.request_id, self.user_count, self.id ) - - def _find_by_role(self, role): - try: - return [member for member in self.members if member.role.name == role] - except StopIteration: - return None - - @property - def contracting_officer(self): - return self._find_by_role("contracting_officer") - - @property - def contracting_officer_representative(self): - return self._find_by_role("contracting_officer_representative") - - @property - def security_officer(self): - return self._find_by_role("security_officer") diff --git a/atst/routes/task_orders/new.py b/atst/routes/task_orders/new.py index fb77cff6..9534fe12 100644 --- a/atst/routes/task_orders/new.py +++ b/atst/routes/task_orders/new.py @@ -161,7 +161,7 @@ class UpdateTaskOrderWorkflow(ShowTaskOrderWorkflow): if ( hasattr(self.form, field) and self.form[field].data - and not getattr(self.workspace, officer_type["role"]) + and not getattr(self.task_order, officer_type["role"]) ): prefix = officer_type["prefix"] officer_data = { From 29e615de35a943bc49d016e8e8c41b98f887da01 Mon Sep 17 00:00:00 2001 From: dandds Date: Fri, 4 Jan 2019 13:34:18 -0500 Subject: [PATCH 09/17] add test for not inviting officer on task order --- .../routes/task_orders/test_new_task_order.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/routes/task_orders/test_new_task_order.py b/tests/routes/task_orders/test_new_task_order.py index eded7da0..fa7f30f4 100644 --- a/tests/routes/task_orders/test_new_task_order.py +++ b/tests/routes/task_orders/test_new_task_order.py @@ -161,6 +161,27 @@ def test_invite_officers_to_task_order(queue): assert task_order.security_officer.dod_id == to_data["so_dod_id"] +def test_add_officer_but_do_not_invite(queue): + user = UserFactory.create() + workspace = WorkspaceFactory.create(owner=user) + task_order = TaskOrderFactory.create(creator=user, workspace=workspace) + to_data = { + **TaskOrderFactory.dictionary(), + "ko_invite": False, + "cor_invite": False, + "so_invite": False, + } + workflow = UpdateTaskOrderWorkflow( + to_data, user, screen=3, task_order_id=task_order.id + ) + workflow.update() + workspace = task_order.workspace + # owner is only workspace member + assert len(workspace.members) == 1 + # no invitations are enqueued + assert len(queue.get_queue()) == 0 + + def test_update_does_not_resend_invitation(): user = UserFactory.create() contracting_officer = UserFactory.create() From a361cbd10940b4dfd36874e457060d3ed4d93273 Mon Sep 17 00:00:00 2001 From: dandds Date: Mon, 7 Jan 2019 11:45:03 -0500 Subject: [PATCH 10/17] fix inviter name in workspace member invitation email --- atst/services/invitation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/atst/services/invitation.py b/atst/services/invitation.py index 34012711..f1badf2b 100644 --- a/atst/services/invitation.py +++ b/atst/services/invitation.py @@ -32,4 +32,4 @@ class Invitation: body = render_template( self.email_template, owner=self.inviter.full_name, token=token ) - queue.send_mail([self.email], self.subject.format(self.inviter), body) + queue.send_mail([self.email], self.subject.format(self.inviter.full_name), body) From 7c5f0fa27db605844aef6cabfcdd0036bad91090 Mon Sep 17 00:00:00 2001 From: dandds Date: Mon, 7 Jan 2019 11:47:24 -0500 Subject: [PATCH 11/17] fix typo in security officer role --- atst/domain/roles.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/atst/domain/roles.py b/atst/domain/roles.py index b11d2552..ff31a36a 100644 --- a/atst/domain/roles.py +++ b/atst/domain/roles.py @@ -155,7 +155,7 @@ WORKSPACE_ROLES = [ { "name": "security_officer", "description": "Can edit security questions for a task order.", - "display_name": "Contracting Officer", + "display_name": "Security Officer", "permissions": [Permissions.VIEW_WORKSPACE, Permissions.SO_SIGN_TASK_ORDER], }, ] From c1f3f6bb62b924c71aef9810b89ecda6f752f1c3 Mon Sep 17 00:00:00 2001 From: dandds Date: Mon, 7 Jan 2019 12:14:42 -0500 Subject: [PATCH 12/17] validate dod id for task order officers --- atst/forms/task_order.py | 17 ++++++++++++++--- templates/components/user_info.html | 2 +- tests/routes/workspaces/test_invitations.py | 2 ++ 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/atst/forms/task_order.py b/atst/forms/task_order.py index 64a3d84a..fb10774e 100644 --- a/atst/forms/task_order.py +++ b/atst/forms/task_order.py @@ -9,6 +9,9 @@ from wtforms.fields import ( ) from wtforms.fields.html5 import DateField from wtforms.widgets import ListWidget, CheckboxInput +from wtforms.validators import Required, Length + +from atst.forms.validators import IsNumber from .forms import CacheableForm from .data import ( @@ -87,15 +90,23 @@ class OversightForm(CacheableForm): ko_first_name = StringField("First Name") ko_last_name = StringField("Last Name") ko_email = StringField("Email") - ko_dod_id = StringField("DOD ID") + ko_dod_id = StringField( + "DOD ID", validators=[Required(), Length(min=10), IsNumber()] + ) + cor_first_name = StringField("First Name") cor_last_name = StringField("Last Name") cor_email = StringField("Email") - cor_dod_id = StringField("DOD ID") + cor_dod_id = StringField( + "DOD ID", validators=[Required(), Length(min=10), IsNumber()] + ) + so_first_name = StringField("First Name") so_last_name = StringField("Last Name") so_email = StringField("Email") - so_dod_id = StringField("DOD ID") + so_dod_id = StringField( + "DOD ID", validators=[Required(), Length(min=10), IsNumber()] + ) ko_invite = BooleanField( "Invite KO to Task Order Builder", diff --git a/templates/components/user_info.html b/templates/components/user_info.html index 7c6378ca..244c7e0d 100644 --- a/templates/components/user_info.html +++ b/templates/components/user_info.html @@ -17,7 +17,7 @@
- {{ TextInput(dod_id, placeholder='1234567890') }} + {{ TextInput(dod_id, placeholder='1234567890', validation='dodId') }}
{% endmacro %} diff --git a/tests/routes/workspaces/test_invitations.py b/tests/routes/workspaces/test_invitations.py index 1fcc4145..0d0b4419 100644 --- a/tests/routes/workspaces/test_invitations.py +++ b/tests/routes/workspaces/test_invitations.py @@ -225,6 +225,8 @@ def test_task_order_officer_accepts_invite(monkeypatch, client, user_session): "ko_last_name": user_info["last_name"], "ko_email": user_info["email"], "ko_dod_id": user_info["dod_id"], + "so_dod_id": task_order.so_dod_id, + "cor_dod_id": task_order.cor_dod_id, "ko_invite": True, }, ) From fc9194d517e0ee109398dbae4a145e80bf54f2d6 Mon Sep 17 00:00:00 2001 From: dandds Date: Mon, 7 Jan 2019 12:23:36 -0500 Subject: [PATCH 13/17] do not display task order officers as regular workspace roles --- atst/forms/data.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/atst/forms/data.py b/atst/forms/data.py index b02cfb64..ebcd3b20 100644 --- a/atst/forms/data.py +++ b/atst/forms/data.py @@ -105,9 +105,16 @@ COMPLETION_DATE_RANGES = [ ("Above 12 months", "Above 12 months"), ] +NONDISPLAY_ROLES = [ + "contracting_officer", + "contracting_officer_representative", + "security_officer", +] + WORKSPACE_ROLES = [ (role["name"], {"name": role["display_name"], "description": role["description"]}) for role in WORKSPACE_ROLE_DEFINITIONS + if role["name"] not in NONDISPLAY_ROLES ] ENVIRONMENT_ROLES = [ From d41f233f5af8a6d348edb5d25d80ecf5b03ca422 Mon Sep 17 00:00:00 2001 From: dandds Date: Mon, 7 Jan 2019 12:29:28 -0500 Subject: [PATCH 14/17] fix task order officer migration --- .../71cbe76c3b87_add_officers_to_task_order.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/alembic/versions/71cbe76c3b87_add_officers_to_task_order.py b/alembic/versions/71cbe76c3b87_add_officers_to_task_order.py index a90ca5ba..5cb0e496 100644 --- a/alembic/versions/71cbe76c3b87_add_officers_to_task_order.py +++ b/alembic/versions/71cbe76c3b87_add_officers_to_task_order.py @@ -21,17 +21,17 @@ def upgrade(): 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']) + op.create_foreign_key('task_orders_users_so_id', 'task_orders', 'users', ['so_id'], ['id']) + op.create_foreign_key('task_orders_users_cor_id', 'task_orders', 'users', ['cor_id'], ['id']) + op.create_foreign_key('task_orders_users_ko_id', '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_constraint('task_orders_users_so_id', 'task_orders', type_='foreignkey') + op.drop_constraint('task_orders_users_cor_id', 'task_orders', type_='foreignkey') + op.drop_constraint('task_orders_users_ko_id', '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') From 95e7adfcf3a04b364a682bdbd009f1cd0ffcc101 Mon Sep 17 00:00:00 2001 From: dandds Date: Tue, 8 Jan 2019 14:36:32 -0500 Subject: [PATCH 15/17] single workspace role for task order officers --- atst/domain/roles.py | 20 +++---------- atst/domain/task_orders.py | 30 ++++++++++++++----- atst/forms/data.py | 8 +---- atst/models/permissions.py | 3 -- tests/domain/test_task_orders.py | 12 ++++++++ .../routes/task_orders/test_new_task_order.py | 4 +-- 6 files changed, 41 insertions(+), 36 deletions(-) diff --git a/atst/domain/roles.py b/atst/domain/roles.py index ff31a36a..91137cbc 100644 --- a/atst/domain/roles.py +++ b/atst/domain/roles.py @@ -141,22 +141,10 @@ WORKSPACE_ROLES = [ ], }, { - "name": "contracting_officer", - "description": "Can approve funding for a task order. Has view and edit permissions for task orders in a workspace.", - "display_name": "Contracting Officer", - "permissions": [Permissions.VIEW_WORKSPACE, Permissions.KO_SIGN_TASK_ORDER], - }, - { - "name": "contracting_officer_representative", - "description": "Assists in submitting task order documents. Can update Task Order.", - "display_name": "Contracting Officer Representative", - "permissions": [Permissions.VIEW_WORKSPACE], - }, - { - "name": "security_officer", - "description": "Can edit security questions for a task order.", - "display_name": "Security Officer", - "permissions": [Permissions.VIEW_WORKSPACE, Permissions.SO_SIGN_TASK_ORDER], + "name": "officer", + "description": "Officer involved with setting up a Task Order", + "display_name": "Task Order Officer", + "permissions": [], }, ] diff --git a/atst/domain/task_orders.py b/atst/domain/task_orders.py index 68e53885..ce600fba 100644 --- a/atst/domain/task_orders.py +++ b/atst/domain/task_orders.py @@ -102,18 +102,34 @@ class TaskOrders(object): ] @classmethod - def add_officer(cls, user, task_order, role, officer_data): - if role in TaskOrders.OFFICERS: - member = Workspaces.create_member( - user, task_order.workspace, {**officer_data, "workspace_role": role} + def add_officer(cls, user, task_order, officer_type, officer_data): + if officer_type in TaskOrders.OFFICERS: + workspace = task_order.workspace + + existing_member = next( + ( + member + for member in workspace.members + if member.user.dod_id is officer_data["dod_id"] + ), + None, ) - setattr(task_order, role, member.user) + + if existing_member: + user = existing_member.user + else: + member = Workspaces.create_member( + user, workspace, {**officer_data, "workspace_role": "officer"} + ) + user = member.user + + setattr(task_order, officer_type, user) db.session.add(task_order) db.session.commit() - return member.user + return user else: raise TaskOrderError( - "{} is not an officer role on task orders".format(role) + "{} is not an officer role on task orders".format(officer_type) ) diff --git a/atst/forms/data.py b/atst/forms/data.py index ebcd3b20..957dd911 100644 --- a/atst/forms/data.py +++ b/atst/forms/data.py @@ -105,16 +105,10 @@ COMPLETION_DATE_RANGES = [ ("Above 12 months", "Above 12 months"), ] -NONDISPLAY_ROLES = [ - "contracting_officer", - "contracting_officer_representative", - "security_officer", -] - WORKSPACE_ROLES = [ (role["name"], {"name": role["display_name"], "description": role["description"]}) for role in WORKSPACE_ROLE_DEFINITIONS - if role["name"] not in NONDISPLAY_ROLES + if role["name"] is not "officer" ] ENVIRONMENT_ROLES = [ diff --git a/atst/models/permissions.py b/atst/models/permissions.py index 112517a3..f7adc406 100644 --- a/atst/models/permissions.py +++ b/atst/models/permissions.py @@ -42,6 +42,3 @@ class Permissions(object): ADD_TAG_TO_WORKSPACE = "add_tag_to_workspace" REMOVE_TAG_FROM_WORKSPACE = "remove_tag_from_workspace" - - KO_SIGN_TASK_ORDER = "ko_sign_task_order" - SO_SIGN_TASK_ORDER = "so_sign_task_order" diff --git a/tests/domain/test_task_orders.py b/tests/domain/test_task_orders.py index f558207b..8966d59b 100644 --- a/tests/domain/test_task_orders.py +++ b/tests/domain/test_task_orders.py @@ -48,3 +48,15 @@ def test_add_officer_with_nonexistent_role(): owner = task_order.workspace.owner with pytest.raises(TaskOrderError): TaskOrders.add_officer(owner, task_order, "pilot", ko.to_dictionary()) + + +def test_add_officer_who_is_already_workspace_member(): + task_order = TaskOrderFactory.create() + owner = task_order.workspace.owner + TaskOrders.add_officer( + owner, task_order, "contracting_officer", owner.to_dictionary() + ) + + assert task_order.contracting_officer == owner + member = task_order.workspace.members[0] + assert member.user == owner and member.role_name == "owner" diff --git a/tests/routes/task_orders/test_new_task_order.py b/tests/routes/task_orders/test_new_task_order.py index fa7f30f4..462fabea 100644 --- a/tests/routes/task_orders/test_new_task_order.py +++ b/tests/routes/task_orders/test_new_task_order.py @@ -150,9 +150,7 @@ def test_invite_officers_to_task_order(queue): 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 + assert roles.count("officer") == 3 # email invitations are enqueued assert len(queue.get_queue()) == 3 # task order has relationship to user for each officer role From ccd75bcaca171425741d2c7e9faaa4effa31852c Mon Sep 17 00:00:00 2001 From: dandds Date: Tue, 8 Jan 2019 15:38:40 -0500 Subject: [PATCH 16/17] small fixes in TaskOrders.add_officer --- atst/domain/task_orders.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/atst/domain/task_orders.py b/atst/domain/task_orders.py index ce600fba..9acd1107 100644 --- a/atst/domain/task_orders.py +++ b/atst/domain/task_orders.py @@ -110,25 +110,25 @@ class TaskOrders(object): ( member for member in workspace.members - if member.user.dod_id is officer_data["dod_id"] + if member.user.dod_id == officer_data["dod_id"] ), None, ) if existing_member: - user = existing_member.user + workspace_user = existing_member.user else: member = Workspaces.create_member( user, workspace, {**officer_data, "workspace_role": "officer"} ) - user = member.user + workspace_user = member.user - setattr(task_order, officer_type, user) + setattr(task_order, officer_type, workspace_user) db.session.add(task_order) db.session.commit() - return user + return workspace_user else: raise TaskOrderError( "{} is not an officer role on task orders".format(officer_type) From b4799b54eb50e5bc47fd0d9846ff13bfa5efdcc1 Mon Sep 17 00:00:00 2001 From: dandds Date: Tue, 8 Jan 2019 16:51:27 -0500 Subject: [PATCH 17/17] fix migration chain --- alembic/versions/71cbe76c3b87_add_officers_to_task_order.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/alembic/versions/71cbe76c3b87_add_officers_to_task_order.py b/alembic/versions/71cbe76c3b87_add_officers_to_task_order.py index 5cb0e496..187f2649 100644 --- a/alembic/versions/71cbe76c3b87_add_officers_to_task_order.py +++ b/alembic/versions/71cbe76c3b87_add_officers_to_task_order.py @@ -1,7 +1,7 @@ """add officers to task order Revision ID: 71cbe76c3b87 -Revises: 6172ac7b8b26 +Revises: 91bd9482ce23 Create Date: 2019-01-04 10:16:50.062349 """ @@ -11,7 +11,7 @@ from sqlalchemy.dialects import postgresql # revision identifiers, used by Alembic. revision = '71cbe76c3b87' -down_revision = '6172ac7b8b26' +down_revision = '91bd9482ce23' branch_labels = None depends_on = None