Merge pull request #515 from dod-ccpo/task-order-invites-#162554089
Task order invites #162554089
This commit is contained in:
commit
6429043fca
38
alembic/versions/71cbe76c3b87_add_officers_to_task_order.py
Normal file
38
alembic/versions/71cbe76c3b87_add_officers_to_task_order.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
"""add officers to task order
|
||||||
|
|
||||||
|
Revision ID: 71cbe76c3b87
|
||||||
|
Revises: 91bd9482ce23
|
||||||
|
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 = '91bd9482ce23'
|
||||||
|
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('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('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')
|
||||||
|
# ### end Alembic commands ###
|
@ -140,6 +140,12 @@ WORKSPACE_ROLES = [
|
|||||||
Permissions.VIEW_WORKSPACE,
|
Permissions.VIEW_WORKSPACE,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "officer",
|
||||||
|
"description": "Officer involved with setting up a Task Order",
|
||||||
|
"display_name": "Task Order Officer",
|
||||||
|
"permissions": [],
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,9 +2,14 @@ from sqlalchemy.orm.exc import NoResultFound
|
|||||||
|
|
||||||
from atst.database import db
|
from atst.database import db
|
||||||
from atst.models.task_order import TaskOrder
|
from atst.models.task_order import TaskOrder
|
||||||
|
from atst.domain.workspaces import Workspaces
|
||||||
from .exceptions import NotFoundError
|
from .exceptions import NotFoundError
|
||||||
|
|
||||||
|
|
||||||
|
class TaskOrderError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class TaskOrders(object):
|
class TaskOrders(object):
|
||||||
SECTIONS = {
|
SECTIONS = {
|
||||||
"app_info": [
|
"app_info": [
|
||||||
@ -89,3 +94,42 @@ class TaskOrders(object):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
OFFICERS = [
|
||||||
|
"contracting_officer",
|
||||||
|
"contracting_officer_representative",
|
||||||
|
"security_officer",
|
||||||
|
]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
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 == officer_data["dod_id"]
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
if existing_member:
|
||||||
|
workspace_user = existing_member.user
|
||||||
|
else:
|
||||||
|
member = Workspaces.create_member(
|
||||||
|
user, workspace, {**officer_data, "workspace_role": "officer"}
|
||||||
|
)
|
||||||
|
workspace_user = member.user
|
||||||
|
|
||||||
|
setattr(task_order, officer_type, workspace_user)
|
||||||
|
|
||||||
|
db.session.add(task_order)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return workspace_user
|
||||||
|
else:
|
||||||
|
raise TaskOrderError(
|
||||||
|
"{} is not an officer role on task orders".format(officer_type)
|
||||||
|
)
|
||||||
|
@ -108,6 +108,7 @@ COMPLETION_DATE_RANGES = [
|
|||||||
WORKSPACE_ROLES = [
|
WORKSPACE_ROLES = [
|
||||||
(role["name"], {"name": role["display_name"], "description": role["description"]})
|
(role["name"], {"name": role["display_name"], "description": role["description"]})
|
||||||
for role in WORKSPACE_ROLE_DEFINITIONS
|
for role in WORKSPACE_ROLE_DEFINITIONS
|
||||||
|
if role["name"] is not "officer"
|
||||||
]
|
]
|
||||||
|
|
||||||
ENVIRONMENT_ROLES = [
|
ENVIRONMENT_ROLES = [
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from wtforms.fields import (
|
from wtforms.fields import (
|
||||||
|
BooleanField,
|
||||||
IntegerField,
|
IntegerField,
|
||||||
RadioField,
|
RadioField,
|
||||||
SelectField,
|
SelectField,
|
||||||
@ -8,6 +9,9 @@ from wtforms.fields import (
|
|||||||
)
|
)
|
||||||
from wtforms.fields.html5 import DateField
|
from wtforms.fields.html5 import DateField
|
||||||
from wtforms.widgets import ListWidget, CheckboxInput
|
from wtforms.widgets import ListWidget, CheckboxInput
|
||||||
|
from wtforms.validators import Required, Length
|
||||||
|
|
||||||
|
from atst.forms.validators import IsNumber
|
||||||
|
|
||||||
from .forms import CacheableForm
|
from .forms import CacheableForm
|
||||||
from .data import (
|
from .data import (
|
||||||
@ -86,15 +90,50 @@ class OversightForm(CacheableForm):
|
|||||||
ko_first_name = StringField("First Name")
|
ko_first_name = StringField("First Name")
|
||||||
ko_last_name = StringField("Last Name")
|
ko_last_name = StringField("Last Name")
|
||||||
ko_email = StringField("Email")
|
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_first_name = StringField("First Name")
|
||||||
cor_last_name = StringField("Last Name")
|
cor_last_name = StringField("Last Name")
|
||||||
cor_email = StringField("Email")
|
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_first_name = StringField("First Name")
|
||||||
so_last_name = StringField("Last Name")
|
so_last_name = StringField("Last Name")
|
||||||
so_email = StringField("Email")
|
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",
|
||||||
|
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.
|
||||||
|
<i>You may choose to skip this for now and invite them later.</i>
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
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. <i>You may choose to skip this for
|
||||||
|
now and invite them later.</i>
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
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. <i>You may choose to skip this for now
|
||||||
|
and invite them later.</i>
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ReviewForm(CacheableForm):
|
class ReviewForm(CacheableForm):
|
||||||
|
@ -14,7 +14,18 @@ class TaskOrder(Base, mixins.TimestampsMixin):
|
|||||||
workspace = relationship("Workspace")
|
workspace = relationship("Workspace")
|
||||||
|
|
||||||
user_id = Column(ForeignKey("users.id"))
|
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
|
scope = Column(String) # Cloud Project Scope
|
||||||
defense_component = Column(String) # Department of Defense Component
|
defense_component = Column(String) # Department of Defense Component
|
||||||
|
@ -10,7 +10,9 @@ from flask import (
|
|||||||
from . import task_orders_bp
|
from . import task_orders_bp
|
||||||
from atst.domain.task_orders import TaskOrders
|
from atst.domain.task_orders import TaskOrders
|
||||||
from atst.domain.workspaces import Workspaces
|
from atst.domain.workspaces import Workspaces
|
||||||
|
from atst.domain.workspace_roles import WorkspaceRoles
|
||||||
import atst.forms.task_order as task_order_form
|
import atst.forms.task_order as task_order_form
|
||||||
|
from atst.services.invitation import Invitation as InvitationService
|
||||||
|
|
||||||
|
|
||||||
TASK_ORDER_SECTIONS = [
|
TASK_ORDER_SECTIONS = [
|
||||||
@ -111,10 +113,15 @@ class UpdateTaskOrderWorkflow(ShowTaskOrderWorkflow):
|
|||||||
def form(self):
|
def form(self):
|
||||||
return self._section["form"](self.form_data)
|
return self._section["form"](self.form_data)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def workspace(self):
|
||||||
|
if self.task_order:
|
||||||
|
return self.task_order.workspace
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
return self.form.validate()
|
return self.form.validate()
|
||||||
|
|
||||||
def update(self):
|
def _update_task_order(self):
|
||||||
if self.task_order:
|
if self.task_order:
|
||||||
TaskOrders.update(self.task_order, **self.form.data)
|
TaskOrders.update(self.task_order, **self.form.data)
|
||||||
else:
|
else:
|
||||||
@ -124,6 +131,59 @@ class UpdateTaskOrderWorkflow(ShowTaskOrderWorkflow):
|
|||||||
self._task_order = TaskOrders.create(workspace=ws, creator=self.user)
|
self._task_order = TaskOrders.create(workspace=ws, creator=self.user)
|
||||||
TaskOrders.update(self.task_order, **to_data)
|
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_officer_invitations(self):
|
||||||
|
for officer_type in self.OFFICER_INVITATIONS:
|
||||||
|
field = officer_type["field"]
|
||||||
|
if (
|
||||||
|
hasattr(self.form, field)
|
||||||
|
and self.form[field].data
|
||||||
|
and not getattr(self.task_order, officer_type["role"])
|
||||||
|
):
|
||||||
|
prefix = officer_type["prefix"]
|
||||||
|
officer_data = {
|
||||||
|
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,
|
||||||
|
ws_officer_member,
|
||||||
|
officer_data["email"],
|
||||||
|
subject=officer_type["subject"],
|
||||||
|
email_template=officer_type["template"],
|
||||||
|
)
|
||||||
|
invite_service.invite()
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
self._update_task_order()
|
||||||
|
self._update_officer_invitations()
|
||||||
return self.task_order
|
return self.task_order
|
||||||
|
|
||||||
|
|
||||||
|
@ -20,6 +20,25 @@ def send_invite_email(owner_name, token, new_member_email):
|
|||||||
def accept_invitation(token):
|
def accept_invitation(token):
|
||||||
invite = Invitations.accept(g.current_user, 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(
|
return redirect(
|
||||||
url_for("workspaces.show_workspace", workspace_id=invite.workspace.id)
|
url_for("workspaces.show_workspace", workspace_id=invite.workspace.id)
|
||||||
)
|
)
|
||||||
|
@ -3,13 +3,13 @@ import re
|
|||||||
from flask import render_template, request as http_request, g, redirect, url_for
|
from flask import render_template, request as http_request, g, redirect, url_for
|
||||||
|
|
||||||
from . import workspaces_bp
|
from . import workspaces_bp
|
||||||
from atst.routes.workspaces.invitations import send_invite_email
|
|
||||||
from atst.domain.exceptions import AlreadyExistsError
|
from atst.domain.exceptions import AlreadyExistsError
|
||||||
from atst.domain.projects import Projects
|
from atst.domain.projects import Projects
|
||||||
from atst.domain.workspaces import Workspaces
|
from atst.domain.workspaces import Workspaces
|
||||||
from atst.domain.workspace_roles import WorkspaceRoles, MEMBER_STATUS_CHOICES
|
from atst.domain.workspace_roles import WorkspaceRoles, MEMBER_STATUS_CHOICES
|
||||||
from atst.domain.environments import Environments
|
from atst.domain.environments import Environments
|
||||||
from atst.domain.environment_roles import EnvironmentRoles
|
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.new_member import NewMemberForm
|
||||||
from atst.forms.edit_member import EditMemberForm
|
from atst.forms.edit_member import EditMemberForm
|
||||||
from atst.forms.data import (
|
from atst.forms.data import (
|
||||||
@ -19,7 +19,6 @@ from atst.forms.data import (
|
|||||||
)
|
)
|
||||||
from atst.domain.authz import Authorization
|
from atst.domain.authz import Authorization
|
||||||
from atst.models.permissions import Permissions
|
from atst.models.permissions import Permissions
|
||||||
from atst.domain.invitations import Invitations
|
|
||||||
|
|
||||||
from atst.utils.flash import formatted_flash as flash
|
from atst.utils.flash import formatted_flash as flash
|
||||||
|
|
||||||
@ -68,13 +67,14 @@ def new_member(workspace_id):
|
|||||||
def create_member(workspace_id):
|
def create_member(workspace_id):
|
||||||
workspace = Workspaces.get(g.current_user, workspace_id)
|
workspace = Workspaces.get(g.current_user, workspace_id)
|
||||||
form = NewMemberForm(http_request.form)
|
form = NewMemberForm(http_request.form)
|
||||||
user = g.current_user
|
|
||||||
|
|
||||||
if form.validate():
|
if form.validate():
|
||||||
try:
|
try:
|
||||||
new_member = Workspaces.create_member(user, workspace, form.data)
|
member = Workspaces.create_member(g.current_user, workspace, form.data)
|
||||||
invite = Invitations.create(user, new_member, form.data["email"])
|
invite_service = InvitationService(
|
||||||
send_invite_email(g.current_user.full_name, invite.token, invite.email)
|
g.current_user, member, form.data.get("email")
|
||||||
|
)
|
||||||
|
invite_service.invite()
|
||||||
|
|
||||||
flash("new_workspace_member", new_member=new_member, workspace=workspace)
|
flash("new_workspace_member", new_member=new_member, workspace=workspace)
|
||||||
|
|
||||||
|
35
atst/services/invitation.py
Normal file
35
atst/services/invitation.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
from flask import render_template
|
||||||
|
|
||||||
|
from atst.domain.invitations import Invitations
|
||||||
|
from atst.queue import queue
|
||||||
|
|
||||||
|
|
||||||
|
class Invitation:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
inviter,
|
||||||
|
member,
|
||||||
|
email,
|
||||||
|
subject="{} has invited you to a JEDI Cloud Workspace",
|
||||||
|
email_template="emails/invitation.txt",
|
||||||
|
):
|
||||||
|
self.inviter = inviter
|
||||||
|
self.member = member
|
||||||
|
self.email = email
|
||||||
|
self.subject = subject
|
||||||
|
self.email_template = email_template
|
||||||
|
|
||||||
|
def invite(self):
|
||||||
|
invite = self._create_invite()
|
||||||
|
self._send_invite_email(invite.token)
|
||||||
|
|
||||||
|
return invite
|
||||||
|
|
||||||
|
def _create_invite(self):
|
||||||
|
return Invitations.create(self.inviter, self.member, self.email)
|
||||||
|
|
||||||
|
def _send_invite_email(self, token):
|
||||||
|
body = render_template(
|
||||||
|
self.email_template, owner=self.inviter.full_name, token=token
|
||||||
|
)
|
||||||
|
queue.send_mail([self.email], self.subject.format(self.inviter.full_name), body)
|
@ -17,7 +17,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class='form-col form-col--half'>
|
<div class='form-col form-col--half'>
|
||||||
{{ TextInput(dod_id, placeholder='1234567890') }}
|
{{ TextInput(dod_id, placeholder='1234567890', validation='dodId') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
{% extends 'task_orders/_new.html' %}
|
{% extends 'task_orders/_new.html' %}
|
||||||
|
|
||||||
{% from "components/user_info.html" import UserInfo %}
|
{% from "components/user_info.html" import UserInfo %}
|
||||||
|
{% from "components/checkbox_input.html" import CheckboxInput %}
|
||||||
|
|
||||||
{% block heading %}
|
{% block heading %}
|
||||||
Oversight
|
Oversight
|
||||||
@ -12,13 +13,15 @@
|
|||||||
|
|
||||||
<!-- Oversight Section -->
|
<!-- Oversight Section -->
|
||||||
<h3>Contracting Officer (KO) Information</h3>
|
<h3>Contracting Officer (KO) Information</h3>
|
||||||
|
|
||||||
{{ UserInfo(form.ko_first_name, form.ko_last_name, form.ko_email, form.ko_dod_id) }}
|
{{ UserInfo(form.ko_first_name, form.ko_last_name, form.ko_email, form.ko_dod_id) }}
|
||||||
|
{{ CheckboxInput(form.ko_invite) }}
|
||||||
|
|
||||||
<h3>Contractive Officer Representative (COR) Information</h3>
|
<h3>Contractive Officer Representative (COR) Information</h3>
|
||||||
{{ UserInfo(form.cor_first_name, form.cor_last_name, form.cor_email, form.cor_dod_id) }}
|
{{ UserInfo(form.cor_first_name, form.cor_last_name, form.cor_email, form.cor_dod_id) }}
|
||||||
|
{{ CheckboxInput(form.cor_invite) }}
|
||||||
|
|
||||||
<h3>Security Officer Information</h3>
|
<h3>Security Officer Information</h3>
|
||||||
{{ UserInfo(form.so_first_name, form.so_last_name, form.so_email, form.so_dod_id) }}
|
{{ UserInfo(form.so_first_name, form.so_last_name, form.so_email, form.so_dod_id) }}
|
||||||
|
{{ CheckboxInput(form.so_invite) }}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import pytest
|
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():
|
def test_is_section_complete():
|
||||||
@ -29,3 +29,34 @@ def test_all_sections_complete():
|
|||||||
assert not TaskOrders.all_sections_complete(task_order)
|
assert not TaskOrders.all_sections_complete(task_order)
|
||||||
task_order.scope = "str12345"
|
task_order.scope = "str12345"
|
||||||
assert TaskOrders.all_sections_complete(task_order)
|
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())
|
||||||
|
|
||||||
|
|
||||||
|
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"
|
||||||
|
@ -4,7 +4,7 @@ from flask import url_for
|
|||||||
from atst.domain.task_orders import TaskOrders
|
from atst.domain.task_orders import TaskOrders
|
||||||
from atst.routes.task_orders.new import ShowTaskOrderWorkflow, UpdateTaskOrderWorkflow
|
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):
|
def test_new_task_order(client, user_session):
|
||||||
@ -129,3 +129,72 @@ def test_update_task_order_with_existing_task_order():
|
|||||||
assert workflow.task_order.start_date != to_data["start_date"]
|
assert workflow.task_order.start_date != to_data["start_date"]
|
||||||
workflow.update()
|
workflow.update()
|
||||||
assert workflow.task_order.start_date.strftime("%m/%d/%Y") == to_data["start_date"]
|
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
|
||||||
|
# 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 roles.count("officer") == 3
|
||||||
|
# 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_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()
|
||||||
|
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
|
||||||
|
@ -6,6 +6,7 @@ from tests.factories import (
|
|||||||
WorkspaceFactory,
|
WorkspaceFactory,
|
||||||
WorkspaceRoleFactory,
|
WorkspaceRoleFactory,
|
||||||
InvitationFactory,
|
InvitationFactory,
|
||||||
|
TaskOrderFactory,
|
||||||
)
|
)
|
||||||
from atst.domain.workspaces import Workspaces
|
from atst.domain.workspaces import Workspaces
|
||||||
from atst.models.workspace_role import Status as WorkspaceRoleStatus
|
from atst.models.workspace_role import Status as WorkspaceRoleStatus
|
||||||
@ -207,3 +208,41 @@ def test_existing_member_invite_resent_to_email_submitted_in_form(
|
|||||||
assert user.email != "example@example.com"
|
assert user.email != "example@example.com"
|
||||||
assert send_mail_job.func.__func__.__name__ == "_send_mail"
|
assert send_mail_job.func.__func__.__name__ == "_send_mail"
|
||||||
assert send_mail_job.args[0] == ["example@example.com"]
|
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"],
|
||||||
|
"so_dod_id": task_order.so_dod_id,
|
||||||
|
"cor_dod_id": task_order.cor_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
|
||||||
|
14
tests/services/test_invitation.py
Normal file
14
tests/services/test_invitation.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
from tests.factories import UserFactory, WorkspaceFactory, WorkspaceRoleFactory
|
||||||
|
|
||||||
|
from atst.services.invitation import Invitation
|
||||||
|
|
||||||
|
|
||||||
|
def test_invite_member(queue):
|
||||||
|
inviter = UserFactory.create()
|
||||||
|
new_member = UserFactory.create()
|
||||||
|
workspace = WorkspaceFactory.create(owner=inviter)
|
||||||
|
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
|
Loading…
x
Reference in New Issue
Block a user