Merge pull request #616 from dod-ccpo/blank-officer-email
Send officer invitations after TO form complete
This commit is contained in:
commit
40526b4a3a
32
alembic/versions/c98adf9bb431_record_invitation_status.py
Normal file
32
alembic/versions/c98adf9bb431_record_invitation_status.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
"""record invitation status
|
||||||
|
|
||||||
|
Revision ID: c98adf9bb431
|
||||||
|
Revises: 1f690989e38e
|
||||||
|
Create Date: 2019-02-06 09:02:28.617202
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'c98adf9bb431'
|
||||||
|
down_revision = '1f690989e38e'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('task_orders', sa.Column('cor_invite', sa.Boolean(), nullable=True))
|
||||||
|
op.add_column('task_orders', sa.Column('ko_invite', sa.Boolean(), nullable=True))
|
||||||
|
op.add_column('task_orders', sa.Column('so_invite', sa.Boolean(), nullable=True))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('task_orders', 'so_invite')
|
||||||
|
op.drop_column('task_orders', 'ko_invite')
|
||||||
|
op.drop_column('task_orders', 'cor_invite')
|
||||||
|
# ### end Alembic commands ###
|
@ -2,7 +2,7 @@ from enum import Enum
|
|||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
||||||
import pendulum
|
import pendulum
|
||||||
from sqlalchemy import Column, Numeric, String, ForeignKey, Date, Integer
|
from sqlalchemy import Boolean, Column, Numeric, String, ForeignKey, Date, Integer
|
||||||
from sqlalchemy.ext.hybrid import hybrid_property
|
from sqlalchemy.ext.hybrid import hybrid_property
|
||||||
from sqlalchemy.types import ARRAY
|
from sqlalchemy.types import ARRAY
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
@ -62,16 +62,19 @@ class TaskOrder(Base, mixins.TimestampsMixin):
|
|||||||
ko_email = Column(String) # Email
|
ko_email = Column(String) # Email
|
||||||
ko_phone_number = Column(String) # Phone Number
|
ko_phone_number = Column(String) # Phone Number
|
||||||
ko_dod_id = Column(String) # DOD ID
|
ko_dod_id = Column(String) # DOD ID
|
||||||
|
ko_invite = Column(Boolean)
|
||||||
cor_first_name = Column(String) # First Name
|
cor_first_name = Column(String) # First Name
|
||||||
cor_last_name = Column(String) # Last Name
|
cor_last_name = Column(String) # Last Name
|
||||||
cor_email = Column(String) # Email
|
cor_email = Column(String) # Email
|
||||||
cor_phone_number = Column(String) # Phone Number
|
cor_phone_number = Column(String) # Phone Number
|
||||||
cor_dod_id = Column(String) # DOD ID
|
cor_dod_id = Column(String) # DOD ID
|
||||||
|
cor_invite = Column(Boolean)
|
||||||
so_first_name = Column(String) # First Name
|
so_first_name = Column(String) # First Name
|
||||||
so_last_name = Column(String) # Last Name
|
so_last_name = Column(String) # Last Name
|
||||||
so_email = Column(String) # Email
|
so_email = Column(String) # Email
|
||||||
so_phone_number = Column(String) # Phone Number
|
so_phone_number = Column(String) # Phone Number
|
||||||
so_dod_id = Column(String) # DOD ID
|
so_dod_id = Column(String) # DOD ID
|
||||||
|
so_invite = Column(Boolean)
|
||||||
pdf_attachment_id = Column(ForeignKey("attachments.id"))
|
pdf_attachment_id = Column(ForeignKey("attachments.id"))
|
||||||
_pdf = relationship("Attachment", foreign_keys=[pdf_attachment_id])
|
_pdf = relationship("Attachment", foreign_keys=[pdf_attachment_id])
|
||||||
number = Column(String, unique=True) # Task Order Number
|
number = Column(String, unique=True) # Task Order Number
|
||||||
@ -156,6 +159,44 @@ class TaskOrder(Base, mixins.TimestampsMixin):
|
|||||||
def is_pending(self):
|
def is_pending(self):
|
||||||
return self.status == Status.PENDING
|
return self.status == Status.PENDING
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ko_invitable(self):
|
||||||
|
"""
|
||||||
|
The MO has indicated that the KO should be invited but we have not sent
|
||||||
|
an invite and attached the KO user
|
||||||
|
"""
|
||||||
|
return self.ko_invite and not self.contracting_officer
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cor_invitable(self):
|
||||||
|
"""
|
||||||
|
The MO has indicated that the COR should be invited but we have not sent
|
||||||
|
an invite and attached the COR user
|
||||||
|
"""
|
||||||
|
return self.cor_invite and not self.contracting_officer_representative
|
||||||
|
|
||||||
|
@property
|
||||||
|
def so_invitable(self):
|
||||||
|
"""
|
||||||
|
The MO has indicated that the SO should be invited but we have not sent
|
||||||
|
an invite and attached the SO user
|
||||||
|
"""
|
||||||
|
return self.so_invite and not self.security_officer
|
||||||
|
|
||||||
|
_OFFICER_PREFIXES = {
|
||||||
|
"contracting_officer": "ko",
|
||||||
|
"contracting_officer_representative": "cor",
|
||||||
|
"security_officer": "so",
|
||||||
|
}
|
||||||
|
_OFFICER_PROPERTIES = ["first_name", "last_name", "phone_number", "email", "dod_id"]
|
||||||
|
|
||||||
|
def officer_dictionary(self, officer_type):
|
||||||
|
prefix = self._OFFICER_PREFIXES[officer_type]
|
||||||
|
return {
|
||||||
|
field: getattr(self, "{}_{}".format(prefix, field))
|
||||||
|
for field in self._OFFICER_PROPERTIES
|
||||||
|
}
|
||||||
|
|
||||||
def to_dictionary(self):
|
def to_dictionary(self):
|
||||||
return {
|
return {
|
||||||
"portfolio_name": self.portfolio_name,
|
"portfolio_name": self.portfolio_name,
|
||||||
|
@ -3,11 +3,57 @@ from flask import redirect, url_for, g
|
|||||||
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.utils.flash import formatted_flash as flash
|
from atst.utils.flash import formatted_flash as flash
|
||||||
|
from atst.domain.portfolio_roles import PortfolioRoles
|
||||||
|
from atst.services.invitation import Invitation as InvitationService
|
||||||
|
|
||||||
|
|
||||||
|
OFFICER_INVITATIONS = [
|
||||||
|
{
|
||||||
|
"field": "ko_invite",
|
||||||
|
"role": "contracting_officer",
|
||||||
|
"subject": "Review a task order",
|
||||||
|
"template": "emails/invitation.txt",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "cor_invite",
|
||||||
|
"role": "contracting_officer_representative",
|
||||||
|
"subject": "Help with a task order",
|
||||||
|
"template": "emails/invitation.txt",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "so_invite",
|
||||||
|
"role": "security_officer",
|
||||||
|
"subject": "Review security for a task order",
|
||||||
|
"template": "emails/invitation.txt",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def update_officer_invitations(user, task_order):
|
||||||
|
for officer_type in OFFICER_INVITATIONS:
|
||||||
|
field = officer_type["field"]
|
||||||
|
if getattr(task_order, field) and not getattr(task_order, officer_type["role"]):
|
||||||
|
officer_data = task_order.officer_dictionary(officer_type["role"])
|
||||||
|
officer = TaskOrders.add_officer(
|
||||||
|
user, task_order, officer_type["role"], officer_data
|
||||||
|
)
|
||||||
|
pf_officer_member = PortfolioRoles.get(task_order.portfolio.id, officer.id)
|
||||||
|
invite_service = InvitationService(
|
||||||
|
user,
|
||||||
|
pf_officer_member,
|
||||||
|
officer_data["email"],
|
||||||
|
subject=officer_type["subject"],
|
||||||
|
email_template=officer_type["template"],
|
||||||
|
)
|
||||||
|
invite_service.invite()
|
||||||
|
|
||||||
|
|
||||||
@task_orders_bp.route("/task_orders/invite/<task_order_id>", methods=["POST"])
|
@task_orders_bp.route("/task_orders/invite/<task_order_id>", methods=["POST"])
|
||||||
def invite(task_order_id):
|
def invite(task_order_id):
|
||||||
task_order = TaskOrders.get(g.current_user, task_order_id)
|
task_order = TaskOrders.get(g.current_user, task_order_id)
|
||||||
|
if TaskOrders.all_sections_complete(task_order):
|
||||||
|
update_officer_invitations(g.current_user, task_order)
|
||||||
|
|
||||||
portfolio = task_order.portfolio
|
portfolio = task_order.portfolio
|
||||||
flash("task_order_congrats", portfolio=portfolio)
|
flash("task_order_congrats", portfolio=portfolio)
|
||||||
return redirect(
|
return redirect(
|
||||||
@ -17,3 +63,8 @@ def invite(task_order_id):
|
|||||||
task_order_id=task_order.id,
|
task_order_id=task_order.id,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
flash("task_order_incomplete")
|
||||||
|
return redirect(
|
||||||
|
url_for("task_orders.new", screen=4, task_order_id=task_order.id)
|
||||||
|
)
|
||||||
|
@ -12,9 +12,7 @@ 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.portfolios import Portfolios
|
from atst.domain.portfolios import Portfolios
|
||||||
from atst.domain.portfolio_roles import PortfolioRoles
|
|
||||||
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 = [
|
||||||
@ -173,7 +171,7 @@ class UpdateTaskOrderWorkflow(ShowTaskOrderWorkflow):
|
|||||||
def validate(self):
|
def validate(self):
|
||||||
return self.form.validate()
|
return self.form.validate()
|
||||||
|
|
||||||
def _update_task_order(self):
|
def update(self):
|
||||||
if self.task_order:
|
if self.task_order:
|
||||||
if "portfolio_name" in self.form.data:
|
if "portfolio_name" in self.form.data:
|
||||||
new_name = self.form.data["portfolio_name"]
|
new_name = self.form.data["portfolio_name"]
|
||||||
@ -189,65 +187,6 @@ class UpdateTaskOrderWorkflow(ShowTaskOrderWorkflow):
|
|||||||
self._task_order = TaskOrders.create(portfolio=pf, creator=self.user)
|
self._task_order = TaskOrders.create(portfolio=pf, creator=self.user)
|
||||||
TaskOrders.update(self.user, self.task_order, **self.task_order_form_data)
|
TaskOrders.update(self.user, self.task_order, **self.task_order_form_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",
|
|
||||||
"phone_number",
|
|
||||||
"dod_id",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
officer = TaskOrders.add_officer(
|
|
||||||
self.user, self.task_order, officer_type["role"], officer_data
|
|
||||||
)
|
|
||||||
pf_officer_member = PortfolioRoles.get(self.portfolio.id, officer.id)
|
|
||||||
invite_service = InvitationService(
|
|
||||||
self.user,
|
|
||||||
pf_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
|
||||||
|
|
||||||
|
|
||||||
|
@ -128,6 +128,13 @@ MESSAGES = {
|
|||||||
""",
|
""",
|
||||||
"category": "success",
|
"category": "success",
|
||||||
},
|
},
|
||||||
|
"task_order_incomplete": {
|
||||||
|
"title_template": "Task Order Incomplete",
|
||||||
|
"message_template": """
|
||||||
|
You must complete your Task Order form before submitting.
|
||||||
|
""",
|
||||||
|
"category": "error",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -70,4 +70,8 @@
|
|||||||
&.icon--medium {
|
&.icon--medium {
|
||||||
@include icon-size(12);
|
@include icon-size(12);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.icon--gold {
|
||||||
|
@include icon-color($color-gold-dark);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -230,14 +230,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.task-order-invite-message {
|
.task-order-invite-message {
|
||||||
|
font-weight: $font-bold;
|
||||||
|
|
||||||
&.not-sent {
|
&.not-sent {
|
||||||
color: $color-red;
|
color: $color-red;
|
||||||
font-weight: $font-bold;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.sent {
|
&.sent {
|
||||||
color: $color-green;
|
color: $color-green;
|
||||||
font-weight: $font-bold;
|
}
|
||||||
|
|
||||||
|
&.pending {
|
||||||
|
color: $color-gold-dark;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,15 +1,17 @@
|
|||||||
{% macro ReviewOfficerInfo(heading, first_name, last_name, email, phone_number, dod_id, officer) %}
|
{% macro ReviewOfficerInfo(heading, officer_data, has_officer, invite_pending) %}
|
||||||
<div class="col col--grow">
|
<div class="col col--grow">
|
||||||
<h4 class='task-order-form__heading'>{{ heading | translate }}</h4>
|
<h4 class='task-order-form__heading'>{{ heading | translate }}</h4>
|
||||||
{{ first_name }} {{ last_name }}<br>
|
{{ officer_data.first_name }} {{ officer_data.last_name }}<br>
|
||||||
{{ email }}<br>
|
{{ officer_data.email }}<br>
|
||||||
{% if phone_number %}
|
{% if officer_data.phone_number %}
|
||||||
{{ phone_number | usPhone }}
|
{{ officer_data.phone_number | usPhone }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<br>
|
<br>
|
||||||
{{ "task_orders.new.review.dod_id" | translate }} {{ dod_id}}<br>
|
{{ "task_orders.new.review.dod_id" | translate }} {{ officer_data.dod_id}}<br>
|
||||||
{% if officer %}
|
{% if has_officer %}
|
||||||
{{ Icon('ok', classes='icon--green') }} <span class="task-order-invite-message sent">{{ "task_orders.new.review.invited"| translate }}</<span>
|
{{ Icon('ok', classes='icon--green') }} <span class="task-order-invite-message sent">{{ "task_orders.new.review.invited"| translate }}</<span>
|
||||||
|
{% elif invite_pending %}
|
||||||
|
{{ Icon('alert', classes='icon--gold') }} <span class="task-order-invite-message pending">{{ "task_orders.new.review.pending_to"| translate }}</<span>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ Icon('alert', classes='icon--red') }} <span class="task-order-invite-message not-sent">{{ "task_orders.new.review.not_invited"| translate }}</span>
|
{{ Icon('alert', classes='icon--red') }} <span class="task-order-invite-message not-sent">{{ "task_orders.new.review.not_invited"| translate }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -17,9 +19,24 @@
|
|||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
{{ ReviewOfficerInfo("task_orders.new.review.ko", task_order.ko_first_name, task_order.ko_last_name, task_order.ko_email, task_order.ko_phone_number, task_order.ko_dod_id, task_order.contracting_officer) }}
|
{{ ReviewOfficerInfo(
|
||||||
{{ ReviewOfficerInfo("task_orders.new.review.cor", task_order.cor_first_name, task_order.cor_last_name, task_order.cor_email, task_order.cor_phone_number, task_order.cor_dod_id, task_order.contracting_officer_representative) }}
|
"task_orders.new.review.ko",
|
||||||
|
task_order.officer_dictionary("contracting_officer"),
|
||||||
|
task_order.contracting_officer,
|
||||||
|
task_order.ko_invitable
|
||||||
|
) }}
|
||||||
|
{{ ReviewOfficerInfo(
|
||||||
|
"task_orders.new.review.cor",
|
||||||
|
task_order.officer_dictionary("contracting_officer_representative"),
|
||||||
|
task_order.contracting_officer_representative,
|
||||||
|
task_order.cor_invitable
|
||||||
|
) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
{{ ReviewOfficerInfo("task_orders.new.review.so", task_order.so_first_name, task_order.so_last_name, task_order.so_email, task_order.so_phone_number, task_order.so_dod_id, task_order.security_officer) }}
|
{{ ReviewOfficerInfo(
|
||||||
|
"task_orders.new.review.so",
|
||||||
|
task_order.officer_dictionary("security_officer"),
|
||||||
|
task_order.security_officer,
|
||||||
|
task_order.so_invitable
|
||||||
|
) }}
|
||||||
</div>
|
</div>
|
||||||
|
@ -212,27 +212,20 @@ def test_existing_member_invite_resent_to_email_submitted_in_form(
|
|||||||
|
|
||||||
def test_contracting_officer_accepts_invite(monkeypatch, client, user_session):
|
def test_contracting_officer_accepts_invite(monkeypatch, client, user_session):
|
||||||
portfolio = PortfolioFactory.create()
|
portfolio = PortfolioFactory.create()
|
||||||
task_order = TaskOrderFactory.create(portfolio=portfolio)
|
|
||||||
user_info = UserFactory.dictionary()
|
user_info = UserFactory.dictionary()
|
||||||
|
task_order = TaskOrderFactory.create(
|
||||||
|
portfolio=portfolio,
|
||||||
|
ko_first_name=user_info["first_name"],
|
||||||
|
ko_last_name=user_info["last_name"],
|
||||||
|
ko_email=user_info["email"],
|
||||||
|
ko_phone_number=user_info["phone_number"],
|
||||||
|
ko_dod_id=user_info["dod_id"],
|
||||||
|
ko_invite=True,
|
||||||
|
)
|
||||||
|
|
||||||
# create contracting officer
|
# create contracting officer
|
||||||
user_session(portfolio.owner)
|
user_session(portfolio.owner)
|
||||||
client.post(
|
client.post(url_for("task_orders.invite", task_order_id=task_order.id))
|
||||||
url_for("task_orders.new", screen=3, task_order_id=task_order.id),
|
|
||||||
data={
|
|
||||||
"portfolio_role": "contracting_officer",
|
|
||||||
"ko_first_name": user_info["first_name"],
|
|
||||||
"ko_last_name": user_info["last_name"],
|
|
||||||
"ko_email": user_info["email"],
|
|
||||||
"ko_phone_number": user_info["phone_number"],
|
|
||||||
"ko_dod_id": user_info["dod_id"],
|
|
||||||
"cor_phone_number": user_info["phone_number"],
|
|
||||||
"so_phone_number": user_info["phone_number"],
|
|
||||||
"so_dod_id": task_order.so_dod_id,
|
|
||||||
"cor_dod_id": task_order.cor_dod_id,
|
|
||||||
"ko_invite": True,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
# contracting officer accepts invitation
|
# contracting officer accepts invitation
|
||||||
user = Users.get_by_dod_id(user_info["dod_id"])
|
user = Users.get_by_dod_id(user_info["dod_id"])
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from flask import url_for
|
from flask import url_for
|
||||||
|
|
||||||
from tests.factories import PortfolioFactory, TaskOrderFactory
|
from tests.factories import PortfolioFactory, TaskOrderFactory, UserFactory
|
||||||
|
|
||||||
|
|
||||||
def test_invite(client, user_session):
|
def test_invite(client, user_session):
|
||||||
@ -15,3 +15,79 @@ def test_invite(client, user_session):
|
|||||||
"portfolios.view_task_order", portfolio_id=to.portfolio_id, task_order_id=to.id
|
"portfolios.view_task_order", portfolio_id=to.portfolio_id, task_order_id=to.id
|
||||||
)
|
)
|
||||||
assert redirect in response.headers["Location"]
|
assert redirect in response.headers["Location"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_invite_officers_to_task_order(client, user_session, queue):
|
||||||
|
task_order = TaskOrderFactory.create(
|
||||||
|
ko_invite=True, cor_invite=True, so_invite=True
|
||||||
|
)
|
||||||
|
portfolio = task_order.portfolio
|
||||||
|
|
||||||
|
user_session(portfolio.owner)
|
||||||
|
client.post(url_for("task_orders.invite", task_order_id=task_order.id))
|
||||||
|
|
||||||
|
# owner and three officers are portfolio members
|
||||||
|
assert len(portfolio.members) == 4
|
||||||
|
roles = [member.role.name for member in portfolio.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 == task_order.ko_dod_id
|
||||||
|
assert task_order.contracting_officer_representative.dod_id == task_order.cor_dod_id
|
||||||
|
assert task_order.security_officer.dod_id == task_order.so_dod_id
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_officer_but_do_not_invite(client, user_session, queue):
|
||||||
|
task_order = TaskOrderFactory.create(
|
||||||
|
ko_invite=False, cor_invite=False, so_invite=False
|
||||||
|
)
|
||||||
|
portfolio = task_order.portfolio
|
||||||
|
|
||||||
|
user_session(portfolio.owner)
|
||||||
|
client.post(url_for("task_orders.invite", task_order_id=task_order.id))
|
||||||
|
|
||||||
|
portfolio = task_order.portfolio
|
||||||
|
# owner is only portfolio member
|
||||||
|
assert len(portfolio.members) == 1
|
||||||
|
# no invitations are enqueued
|
||||||
|
assert len(queue.get_queue()) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_does_not_resend_officer_invitation(client, user_session):
|
||||||
|
user = UserFactory.create()
|
||||||
|
contracting_officer = UserFactory.create()
|
||||||
|
portfolio = PortfolioFactory.create(owner=user)
|
||||||
|
task_order = TaskOrderFactory.create(
|
||||||
|
creator=user,
|
||||||
|
portfolio=portfolio,
|
||||||
|
ko_first_name=contracting_officer.first_name,
|
||||||
|
ko_last_name=contracting_officer.last_name,
|
||||||
|
ko_dod_id=contracting_officer.dod_id,
|
||||||
|
ko_invite=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
user_session(user)
|
||||||
|
for i in range(2):
|
||||||
|
client.post(url_for("task_orders.invite", task_order_id=task_order.id))
|
||||||
|
assert len(contracting_officer.invitations) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_does_not_invite_if_task_order_incomplete(client, user_session, queue):
|
||||||
|
task_order = TaskOrderFactory.create(
|
||||||
|
scope=None, ko_invite=True, cor_invite=True, so_invite=True
|
||||||
|
)
|
||||||
|
portfolio = task_order.portfolio
|
||||||
|
|
||||||
|
user_session(portfolio.owner)
|
||||||
|
response = client.post(url_for("task_orders.invite", task_order_id=task_order.id))
|
||||||
|
|
||||||
|
# redirected to review screen
|
||||||
|
assert response.headers["Location"] == url_for(
|
||||||
|
"task_orders.new", screen=4, task_order_id=task_order.id, _external=True
|
||||||
|
)
|
||||||
|
# only owner is portfolio member
|
||||||
|
assert len(portfolio.members) == 1
|
||||||
|
# no email invitations are enqueued
|
||||||
|
assert len(queue.get_queue()) == 0
|
||||||
|
@ -235,69 +235,6 @@ def test_cor_data_set_to_user_data_if_am_cor_is_checked(task_order):
|
|||||||
assert task_order.cor_dod_id == task_order.creator.dod_id
|
assert task_order.cor_dod_id == task_order.creator.dod_id
|
||||||
|
|
||||||
|
|
||||||
def test_invite_officers_to_task_order(task_order, queue):
|
|
||||||
to_data = {
|
|
||||||
**TaskOrderFactory.dictionary(),
|
|
||||||
"ko_invite": True,
|
|
||||||
"cor_invite": True,
|
|
||||||
"so_invite": True,
|
|
||||||
}
|
|
||||||
workflow = UpdateTaskOrderWorkflow(
|
|
||||||
task_order.creator, to_data, screen=3, task_order_id=task_order.id
|
|
||||||
)
|
|
||||||
workflow.update()
|
|
||||||
portfolio = task_order.portfolio
|
|
||||||
# owner and three officers are portfolio members
|
|
||||||
assert len(portfolio.members) == 4
|
|
||||||
roles = [member.role.name for member in portfolio.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(task_order, queue):
|
|
||||||
to_data = {
|
|
||||||
**TaskOrderFactory.dictionary(),
|
|
||||||
"ko_invite": False,
|
|
||||||
"cor_invite": False,
|
|
||||||
"so_invite": False,
|
|
||||||
}
|
|
||||||
workflow = UpdateTaskOrderWorkflow(
|
|
||||||
task_order.creator, to_data, screen=3, task_order_id=task_order.id
|
|
||||||
)
|
|
||||||
workflow.update()
|
|
||||||
portfolio = task_order.portfolio
|
|
||||||
# owner is only portfolio member
|
|
||||||
assert len(portfolio.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()
|
|
||||||
portfolio = PortfolioFactory.create(owner=user)
|
|
||||||
task_order = TaskOrderFactory.create(
|
|
||||||
creator=user,
|
|
||||||
portfolio=portfolio,
|
|
||||||
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(
|
|
||||||
user, to_data, screen=3, task_order_id=task_order.id
|
|
||||||
)
|
|
||||||
for i in range(2):
|
|
||||||
workflow.update()
|
|
||||||
assert len(contracting_officer.invitations) == 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_review_task_order_form(client, user_session, task_order):
|
def test_review_task_order_form(client, user_session, task_order):
|
||||||
user_session(task_order.creator)
|
user_session(task_order.creator)
|
||||||
|
|
||||||
|
@ -458,6 +458,7 @@ task_orders:
|
|||||||
invited: Invited
|
invited: Invited
|
||||||
not_invited: Not Yet Invited
|
not_invited: Not Yet Invited
|
||||||
not_uploaded: Not Uploaded
|
not_uploaded: Not Uploaded
|
||||||
|
pending_to: Pending TO Completion
|
||||||
invitations:
|
invitations:
|
||||||
dod_id_label: DoD ID
|
dod_id_label: DoD ID
|
||||||
contracting_officer:
|
contracting_officer:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user