This commit is contained in:
George Drummond 2019-02-28 10:00:18 -05:00
parent 1998bf6600
commit d57b96cf05
No known key found for this signature in database
GPG Key ID: 296DD6077123BF17
10 changed files with 398 additions and 123 deletions

View File

@ -108,6 +108,11 @@ class Invitations(object):
invite = Invitations._get(token) invite = Invitations._get(token)
return Invitations._update_status(invite, InvitationStatus.REVOKED) return Invitations._update_status(invite, InvitationStatus.REVOKED)
@classmethod
def lookup_by_portfolio_and_user(cls, portfolio, user):
portfolio_role = PortfolioRoles.get(portfolio.id, user.id)
return portfolio_role.latest_invitation
@classmethod @classmethod
def resend(cls, user, portfolio_id, token): def resend(cls, user, portfolio_id, token):
portfolio = Portfolios.get(user, portfolio_id) portfolio = Portfolios.get(user, portfolio_id)

View File

@ -14,6 +14,10 @@ class TaskOrderError(Exception):
pass pass
class InvalidOfficerError(Exception):
pass
class TaskOrders(object): class TaskOrders(object):
SECTIONS = { SECTIONS = {
"app_info": [ "app_info": [
@ -145,6 +149,15 @@ class TaskOrders(object):
"security_officer", "security_officer",
] ]
@classmethod
def remove_officer(cls, task_order, officer_type):
if officer_type in TaskOrders.OFFICERS:
setattr(task_order, officer_type, None)
db.session.add(task_order)
db.session.commit()
else:
raise (InvalidOfficerError)
@classmethod @classmethod
def add_officer(cls, user, task_order, officer_type, officer_data): def add_officer(cls, user, task_order, officer_type, officer_data):
Authorization.check_portfolio_permission( Authorization.check_portfolio_permission(

View File

@ -4,15 +4,22 @@ from flask import g, redirect, render_template, url_for, request as http_request
from . import portfolios_bp from . import portfolios_bp
from atst.database import db from atst.database import db
from atst.domain.task_orders import TaskOrders, DD254s
from atst.domain.exceptions import NotFoundError, NoAccessError
from atst.domain.portfolios import Portfolios
from atst.domain.authz import Authorization from atst.domain.authz import Authorization
from atst.domain.exceptions import NotFoundError, NoAccessError
from atst.domain.invitations import Invitations
from atst.domain.portfolios import Portfolios
from atst.domain.task_orders import TaskOrders, DD254s
from atst.utils.localization import translate
from atst.forms.dd_254 import DD254Form
from atst.forms.ko_review import KOReviewForm
from atst.forms.officers import EditTaskOrderOfficersForm from atst.forms.officers import EditTaskOrderOfficersForm
from atst.models.task_order import Status as TaskOrderStatus from atst.models.task_order import Status as TaskOrderStatus
from atst.forms.ko_review import KOReviewForm from atst.utils.flash import formatted_flash as flash
from atst.forms.dd_254 import DD254Form from atst.services.invitation import (
from atst.services.invitation import update_officer_invitations update_officer_invitations,
OFFICER_INVITATIONS,
Invitation as InvitationService,
)
@portfolios_bp.route("/portfolios/<portfolio_id>/task_orders") @portfolios_bp.route("/portfolios/<portfolio_id>/task_orders")
@ -96,6 +103,66 @@ def ko_review(portfolio_id, task_order_id):
raise NoAccessError("task_order") raise NoAccessError("task_order")
@portfolios_bp.route(
"/portfolios/<portfolio_id>/task_order/<task_order_id>/resend_invite",
methods=["POST"],
)
def resend_invite(portfolio_id, task_order_id, form=None):
form_data = {**http_request.form}
invite_type = form_data["invite_type"][0]
if invite_type not in dict.keys(OFFICER_INVITATIONS):
raise NotFoundError("invite_type")
invite_type_info = OFFICER_INVITATIONS[invite_type]
task_order = TaskOrders.get(g.current_user, task_order_id)
portfolio = Portfolios.get(g.current_user, portfolio_id)
#
# TODO: Add in authorization check
#
officer = getattr(task_order, invite_type_info["role"])
if not officer:
raise NotFoundError("officer")
invitation = Invitations.lookup_by_portfolio_and_user(portfolio, officer)
if not invitation:
raise NotFoundError("invitation")
Invitations.resend(g.current_user, portfolio.id, invitation.token)
invite_service = InvitationService(
g.current_user,
invitation.portfolio_role,
invitation.email,
subject=invite_type_info["subject"],
email_template=invite_type_info["template"],
)
invite_service.invite()
flash(
"invitation_resent",
officer_type=translate(
"common.officer_helpers.underscore_to_friendly.{}".format(
invite_type_info["role"]
)
),
)
return redirect(
url_for(
"portfolios.task_order_invitations",
portfolio_id=portfolio.id,
task_order_id=task_order.id,
)
)
@portfolios_bp.route( @portfolios_bp.route(
"/portfolios/<portfolio_id>/task_order/<task_order_id>/review", methods=["POST"] "/portfolios/<portfolio_id>/task_order/<task_order_id>/review", methods=["POST"]
) )
@ -173,11 +240,14 @@ def edit_task_order_invitations(portfolio_id, task_order_id):
) )
) )
else: else:
return render_template( return (
render_template(
"portfolios/task_orders/invitations.html", "portfolios/task_orders/invitations.html",
portfolio=portfolio, portfolio=portfolio,
task_order=task_order, task_order=task_order,
form=form, form=form,
),
400,
) )

View File

@ -5,43 +5,43 @@ from atst.queue import queue
from atst.domain.task_orders import TaskOrders from atst.domain.task_orders import TaskOrders
from atst.domain.portfolio_roles import PortfolioRoles from atst.domain.portfolio_roles import PortfolioRoles
OFFICER_INVITATIONS = [ OFFICER_INVITATIONS = {
{ "ko_invite": {
"field": "ko_invite",
"role": "contracting_officer", "role": "contracting_officer",
"subject": "Review a task order", "subject": "Review a task order",
"template": "emails/invitation.txt", "template": "emails/invitation.txt",
}, },
{ "cor_invite": {
"field": "cor_invite",
"role": "contracting_officer_representative", "role": "contracting_officer_representative",
"subject": "Help with a task order", "subject": "Help with a task order",
"template": "emails/invitation.txt", "template": "emails/invitation.txt",
}, },
{ "so_invite": {
"field": "so_invite",
"role": "security_officer", "role": "security_officer",
"subject": "Review security for a task order", "subject": "Review security for a task order",
"template": "emails/invitation.txt", "template": "emails/invitation.txt",
}, },
] }
def update_officer_invitations(user, task_order): def update_officer_invitations(user, task_order):
for officer_type in OFFICER_INVITATIONS: for invite_type in dict.keys(OFFICER_INVITATIONS):
field = officer_type["field"] invite_opts = OFFICER_INVITATIONS[invite_type]
if getattr(task_order, field) and not getattr(task_order, officer_type["role"]):
officer_data = task_order.officer_dictionary(officer_type["role"]) if getattr(task_order, invite_type) and not getattr(
task_order, invite_opts["role"]
):
officer_data = task_order.officer_dictionary(invite_opts["role"])
officer = TaskOrders.add_officer( officer = TaskOrders.add_officer(
user, task_order, officer_type["role"], officer_data user, task_order, invite_opts["role"], officer_data
) )
pf_officer_member = PortfolioRoles.get(task_order.portfolio.id, officer.id) pf_officer_member = PortfolioRoles.get(task_order.portfolio.id, officer.id)
invite_service = Invitation( invite_service = Invitation(
user, user,
pf_officer_member, pf_officer_member,
officer_data["email"], officer_data["email"],
subject=officer_type["subject"], subject=invite_opts["subject"],
email_template=officer_type["template"], email_template=invite_opts["template"],
) )
invite_service.invite() invite_service.invite()

View File

@ -2,6 +2,11 @@ from flask import flash, render_template_string
from atst.utils.localization import translate from atst.utils.localization import translate
MESSAGES = { MESSAGES = {
"invitation_resent": {
"title_template": "The {{ officer_type }} invite has been resent",
"message_template": "Invitation has been resent",
"category": "success",
},
"task_order_draft": { "task_order_draft": {
"title_template": translate("task_orders.form.draft_alert_title"), "title_template": translate("task_orders.form.draft_alert_title"),
"message_template": """ "message_template": """

View File

@ -6,6 +6,7 @@
{% from "components/icon.html" import Icon %} {% from "components/icon.html" import Icon %}
{% from "components/text_input.html" import TextInput %} {% from "components/text_input.html" import TextInput %}
{% macro Link(text, icon_name, onClick=None, url='#', classes='') %} {% macro Link(text, icon_name, onClick=None, url='#', classes='') %}
<a href="{{ url }}" {% if onClick %}v-on:click="{{ onClick }}"{% endif %} class="icon-link {{ classes }}"> <a href="{{ url }}" {% if onClick %}v-on:click="{{ onClick }}"{% endif %} class="icon-link {{ classes }}">
{{ Icon(icon_name) }} {{ Icon(icon_name) }}
@ -14,6 +15,8 @@
{% endmacro %} {% endmacro %}
{% macro EditOfficerInfo(form, officer_type, invited) -%} {% macro EditOfficerInfo(form, officer_type, invited) -%}
<form method='POST' action="{{ url_for("portfolios.edit_task_order_invitations", portfolio_id=portfolio.id, task_order_id=task_order.id) }}" autocomplete="off">
{{ form.csrf_token }}
<template v-if="editing"> <template v-if="editing">
<div class='officer__form'> <div class='officer__form'>
<div class="edit-officer"> <div class="edit-officer">
@ -59,6 +62,7 @@
</div> </div>
</div> </div>
</template> </template>
</form>
{% endmacro %} {% endmacro %}
{% macro OfficerInfo(task_order, officer_type, form) %} {% macro OfficerInfo(task_order, officer_type, form) %}
@ -66,10 +70,18 @@
<h2 class="officer__title">{{ ("task_orders.invitations." + officer_type + ".title") | translate }}</h2> <h2 class="officer__title">{{ ("task_orders.invitations." + officer_type + ".title") | translate }}</h2>
<p class="officer__description">{{ ("task_orders.invitations." + officer_type + ".description") | translate }}</p> <p class="officer__description">{{ ("task_orders.invitations." + officer_type + ".description") | translate }}</p>
{% set prefix = { "contracting_officer": "ko", "contracting_officer_representative": "cor", "security_officer": "so" }[officer_type] %}
<form method='POST' action="{{ url_for("portfolios.resend_invite", portfolio_id=portfolio.id, task_order_id=task_order.id) }}">
{{ form.csrf_token }}
<input name="invite_type" value="{{ prefix }}_invite" type="hidden" />
<button>
Resend
</button>
</form>
<edit-officer-form v-bind:has-errors='{{ ((form.errors|length) > 0)|tojson }}' v-bind:has-changes='{{ form.has_changes() | tojson }}' inline-template> <edit-officer-form v-bind:has-errors='{{ ((form.errors|length) > 0)|tojson }}' v-bind:has-changes='{{ form.has_changes() | tojson }}' inline-template>
<div> <div>
{% set prefix = { "contracting_officer": "ko", "contracting_officer_representative": "cor", "security_officer": "so" }[officer_type] %}
{% set first_name = task_order[prefix + "_first_name"] %} {% set first_name = task_order[prefix + "_first_name"] %}
{% set last_name = task_order[prefix + "_last_name"] %} {% set last_name = task_order[prefix + "_last_name"] %}
{% set email = task_order[prefix + "_email"] %} {% set email = task_order[prefix + "_email"] %}
@ -137,12 +149,10 @@
{% endmacro %} {% endmacro %}
{% block portfolio_content %} {% block portfolio_content %}
<div class="task-order-invitations"> <div class="task-order-invitations">
{% include "fragments/flash.html" %} {% include "fragments/flash.html" %}
<form method='POST' action="{{ url_for("portfolios.edit_task_order_invitations", portfolio_id=portfolio.id, task_order_id=task_order.id) }}" autocomplete="off">
{{ form.csrf_token }}
<div class="panel"> <div class="panel">
<div class="panel__heading"> <div class="panel__heading">
<h1 class="task-order-invitations__heading subheading"> <h1 class="task-order-invitations__heading subheading">
@ -155,6 +165,5 @@
{{ OfficerInfo(task_order, officer, form[officer]) }} {{ OfficerInfo(task_order, officer, form[officer]) }}
{% endfor %} {% endfor %}
</div> </div>
</form>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -7,6 +7,7 @@ from atst.domain.invitations import (
InvitationError, InvitationError,
WrongUserError, WrongUserError,
ExpiredError, ExpiredError,
NotFoundError,
) )
from atst.models.invitation import Status from atst.models.invitation import Status
@ -144,3 +145,15 @@ def test_audit_event_for_accepted_invite():
accepted_event = AuditLog.get_by_resource(invite.id)[0] accepted_event = AuditLog.get_by_resource(invite.id)[0]
assert "email" in accepted_event.event_details assert "email" in accepted_event.event_details
assert "dod_id" in accepted_event.event_details assert "dod_id" in accepted_event.event_details
def test_lookup_by_user_and_portfolio():
portfolio = PortfolioFactory.create()
user = UserFactory.create()
ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio)
invite = Invitations.create(portfolio.owner, ws_role, user.email)
assert Invitations.lookup_by_portfolio_and_user(portfolio, user) == invite
with pytest.raises(NotFoundError):
Invitations.lookup_by_portfolio_and_user(portfolio, UserFactory.create())

View File

@ -1,6 +1,11 @@
import pytest import pytest
from atst.domain.task_orders import TaskOrders, TaskOrderError, DD254s from atst.domain.task_orders import (
TaskOrders,
TaskOrderError,
InvalidOfficerError,
DD254s,
)
from atst.domain.exceptions import UnauthorizedError from atst.domain.exceptions import UnauthorizedError
from atst.models.attachment import Attachment from atst.models.attachment import Attachment
@ -127,6 +132,26 @@ def test_task_order_access():
) )
def test_remove_valid_officer():
task_order = TaskOrderFactory.create()
owner = task_order.portfolio.owner
TaskOrders.add_officer(
owner, task_order, "contracting_officer", owner.to_dictionary()
)
assert task_order.contracting_officer == owner
TaskOrders.remove_officer(task_order, "contracting_officer")
assert task_order.contracting_officer is None
def test_remove_invalid_officer():
task_order = TaskOrderFactory.create()
with pytest.raises(InvalidOfficerError):
TaskOrders.remove_officer(task_order, "invalid_officer_type")
def test_dd254_complete(): def test_dd254_complete():
finished = DD254Factory.create() finished = DD254Factory.create()
unfinished = DD254Factory.create(certifying_official=None) unfinished = DD254Factory.create(certifying_official=None)

View File

@ -5,11 +5,14 @@ from datetime import timedelta, date
from atst.domain.roles import Roles from atst.domain.roles import Roles
from atst.domain.task_orders import TaskOrders from atst.domain.task_orders import TaskOrders
from atst.models.portfolio_role import Status as PortfolioStatus from atst.models.portfolio_role import Status as PortfolioStatus
from atst.models.invitation import Status as InvitationStatus
from atst.utils.localization import translate from atst.utils.localization import translate
from atst.queue import queue from atst.queue import queue
from atst.domain.invitations import Invitations
from tests.factories import ( from tests.factories import (
PortfolioFactory, PortfolioFactory,
InvitationFactory,
PortfolioRoleFactory, PortfolioRoleFactory,
TaskOrderFactory, TaskOrderFactory,
UserFactory, UserFactory,
@ -20,9 +23,18 @@ from tests.factories import (
from tests.utils import captured_templates from tests.utils import captured_templates
@pytest.fixture
def portfolio():
return PortfolioFactory.create()
@pytest.fixture
def user():
return UserFactory.create()
class TestPortfolioFunding: class TestPortfolioFunding:
def test_portfolio_with_no_task_orders(self, app, user_session): def test_portfolio_with_no_task_orders(self, app, user_session, portfolio):
portfolio = PortfolioFactory.create()
user_session(portfolio.owner) user_session(portfolio.owner)
with captured_templates(app) as templates: with captured_templates(app) as templates:
@ -38,8 +50,7 @@ class TestPortfolioFunding:
assert context["active_task_orders"] == [] assert context["active_task_orders"] == []
assert context["expired_task_orders"] == [] assert context["expired_task_orders"] == []
def test_funded_portfolio(self, app, user_session): def test_funded_portfolio(self, app, user_session, portfolio):
portfolio = PortfolioFactory.create()
user_session(portfolio.owner) user_session(portfolio.owner)
pending_to = TaskOrderFactory.create(portfolio=portfolio) pending_to = TaskOrderFactory.create(portfolio=portfolio)
@ -71,8 +82,7 @@ class TestPortfolioFunding:
assert context["funding_end_date"] is end_date assert context["funding_end_date"] is end_date
assert context["total_balance"] == active_to1.budget + active_to2.budget assert context["total_balance"] == active_to1.budget + active_to2.budget
def test_expiring_and_funded_portfolio(self, app, user_session): def test_expiring_and_funded_portfolio(self, app, user_session, portfolio):
portfolio = PortfolioFactory.create()
user_session(portfolio.owner) user_session(portfolio.owner)
expiring_to = TaskOrderFactory.create( expiring_to = TaskOrderFactory.create(
@ -98,8 +108,7 @@ class TestPortfolioFunding:
assert context["funding_end_date"] is active_to.end_date assert context["funding_end_date"] is active_to.end_date
assert context["funded"] == True assert context["funded"] == True
def test_expiring_and_unfunded_portfolio(self, app, user_session): def test_expiring_and_unfunded_portfolio(self, app, user_session, portfolio):
portfolio = PortfolioFactory.create()
user_session(portfolio.owner) user_session(portfolio.owner)
expiring_to = TaskOrderFactory.create( expiring_to = TaskOrderFactory.create(
@ -154,6 +163,16 @@ class TestTaskOrderInvitations:
assert updated_task_order.so_first_name == "Boba" assert updated_task_order.so_first_name == "Boba"
assert updated_task_order.so_last_name == "Fett" assert updated_task_order.so_last_name == "Fett"
assert len(queue.get_queue()) == queue_length assert len(queue.get_queue()) == queue_length
assert response.status_code == 302
assert (
url_for(
"portfolios.task_order_invitations",
portfolio_id=self.portfolio.id,
task_order_id=self.task_order.id,
_external=True,
)
== response.headers["Location"]
)
def test_editing_with_complete_data(self, user_session, client): def test_editing_with_complete_data(self, user_session, client):
queue_length = len(queue.get_queue()) queue_length = len(queue.get_queue())
@ -178,6 +197,16 @@ class TestTaskOrderInvitations:
assert updated_task_order.ko_email == "luke@skywalker.mil" assert updated_task_order.ko_email == "luke@skywalker.mil"
assert updated_task_order.ko_phone_number == "0123456789" assert updated_task_order.ko_phone_number == "0123456789"
assert len(queue.get_queue()) == queue_length + 1 assert len(queue.get_queue()) == queue_length + 1
assert response.status_code == 302
assert (
url_for(
"portfolios.task_order_invitations",
portfolio_id=self.portfolio.id,
task_order_id=self.task_order.id,
_external=True,
)
== response.headers["Location"]
)
def test_editing_with_invalid_data(self, user_session, client): def test_editing_with_invalid_data(self, user_session, client):
queue_length = len(queue.get_queue()) queue_length = len(queue.get_queue())
@ -196,19 +225,18 @@ class TestTaskOrderInvitations:
updated_task_order = TaskOrders.get(self.portfolio.owner, self.task_order.id) updated_task_order = TaskOrders.get(self.portfolio.owner, self.task_order.id)
assert updated_task_order.so_first_name != "Boba" assert updated_task_order.so_first_name != "Boba"
assert len(queue.get_queue()) == queue_length assert len(queue.get_queue()) == queue_length
assert response.status_code == 400
def test_ko_can_view_task_order(client, user_session): def test_ko_can_view_task_order(client, user_session, portfolio, user):
portfolio = PortfolioFactory.create()
ko = UserFactory.create()
PortfolioRoleFactory.create( PortfolioRoleFactory.create(
role=Roles.get("officer"), role=Roles.get("owner"),
portfolio=portfolio, portfolio=portfolio,
user=ko, user=user,
status=PortfolioStatus.ACTIVE, status=PortfolioStatus.ACTIVE,
) )
task_order = TaskOrderFactory.create(portfolio=portfolio, contracting_officer=ko) task_order = TaskOrderFactory.create(portfolio=portfolio, contracting_officer=user)
user_session(ko) user_session(user)
response = client.get( response = client.get(
url_for( url_for(
@ -220,7 +248,7 @@ def test_ko_can_view_task_order(client, user_session):
assert response.status_code == 200 assert response.status_code == 200
assert translate("common.manage") in response.data.decode() assert translate("common.manage") in response.data.decode()
TaskOrders.update(ko, task_order, clin_01=None) TaskOrders.update(user, task_order, clin_01=None)
response = client.get( response = client.get(
url_for( url_for(
"portfolios.view_task_order", "portfolios.view_task_order",
@ -232,8 +260,7 @@ def test_ko_can_view_task_order(client, user_session):
assert translate("common.manage") not in response.data.decode() assert translate("common.manage") not in response.data.decode()
def test_can_view_task_order_invitations_when_complete(client, user_session): def test_can_view_task_order_invitations_when_complete(client, user_session, portfolio):
portfolio = PortfolioFactory.create()
user_session(portfolio.owner) user_session(portfolio.owner)
task_order = TaskOrderFactory.create(portfolio=portfolio) task_order = TaskOrderFactory.create(portfolio=portfolio)
response = client.get( response = client.get(
@ -246,8 +273,9 @@ def test_can_view_task_order_invitations_when_complete(client, user_session):
assert response.status_code == 200 assert response.status_code == 200
def test_cant_view_task_order_invitations_when_not_complete(client, user_session): def test_cant_view_task_order_invitations_when_not_complete(
portfolio = PortfolioFactory.create() client, user_session, portfolio
):
user_session(portfolio.owner) user_session(portfolio.owner)
task_order = TaskOrderFactory.create(portfolio=portfolio, clin_01=None) task_order = TaskOrderFactory.create(portfolio=portfolio, clin_01=None)
response = client.get( response = client.get(
@ -324,8 +352,7 @@ def test_cor_cant_view_review_until_to_completed(client, user_session):
assert response.status_code == 404 assert response.status_code == 404
def test_mo_redirected_to_build_page(client, user_session): def test_mo_redirected_to_build_page(client, user_session, portfolio):
portfolio = PortfolioFactory.create()
user_session(portfolio.owner) user_session(portfolio.owner)
task_order = TaskOrderFactory.create(portfolio=portfolio) task_order = TaskOrderFactory.create(portfolio=portfolio)
@ -335,8 +362,7 @@ def test_mo_redirected_to_build_page(client, user_session):
assert response.status_code == 200 assert response.status_code == 200
def test_cor_redirected_to_build_page(client, user_session): def test_cor_redirected_to_build_page(client, user_session, portfolio):
portfolio = PortfolioFactory.create()
cor = UserFactory.create() cor = UserFactory.create()
PortfolioRoleFactory.create( PortfolioRoleFactory.create(
role=Roles.get("officer"), role=Roles.get("officer"),
@ -354,20 +380,18 @@ def test_cor_redirected_to_build_page(client, user_session):
assert response.status_code == 200 assert response.status_code == 200
def test_submit_completed_ko_review_page_as_cor(client, user_session, pdf_upload): def test_submit_completed_ko_review_page_as_cor(
portfolio = PortfolioFactory.create() client, user_session, pdf_upload, portfolio, user
):
cor = UserFactory.create()
PortfolioRoleFactory.create( PortfolioRoleFactory.create(
role=Roles.get("officer"), role=Roles.get("officer"),
portfolio=portfolio, portfolio=portfolio,
user=cor, user=user,
status=PortfolioStatus.ACTIVE, status=PortfolioStatus.ACTIVE,
) )
task_order = TaskOrderFactory.create( task_order = TaskOrderFactory.create(
portfolio=portfolio, contracting_officer_representative=cor portfolio=portfolio, contracting_officer_representative=user
) )
form_data = { form_data = {
@ -379,7 +403,7 @@ def test_submit_completed_ko_review_page_as_cor(client, user_session, pdf_upload
"pdf": pdf_upload, "pdf": pdf_upload,
} }
user_session(cor) user_session(user)
response = client.post( response = client.post(
url_for( url_for(
@ -399,9 +423,9 @@ def test_submit_completed_ko_review_page_as_cor(client, user_session, pdf_upload
) )
def test_submit_completed_ko_review_page_as_ko(client, user_session, pdf_upload): def test_submit_completed_ko_review_page_as_ko(
portfolio = PortfolioFactory.create() client, user_session, pdf_upload, portfolio
):
ko = UserFactory.create() ko = UserFactory.create()
PortfolioRoleFactory.create( PortfolioRoleFactory.create(
@ -443,8 +467,7 @@ def test_submit_completed_ko_review_page_as_ko(client, user_session, pdf_upload)
assert task_order.loas == loa_list assert task_order.loas == loa_list
def test_so_review_page(app, client, user_session): def test_so_review_page(app, client, user_session, portfolio):
portfolio = PortfolioFactory.create()
so = UserFactory.create() so = UserFactory.create()
PortfolioRoleFactory.create( PortfolioRoleFactory.create(
role=Roles.get("officer"), role=Roles.get("officer"),
@ -482,8 +505,7 @@ def test_so_review_page(app, client, user_session):
) )
def test_submit_so_review(app, client, user_session): def test_submit_so_review(app, client, user_session, portfolio):
portfolio = PortfolioFactory.create()
so = UserFactory.create() so = UserFactory.create()
PortfolioRoleFactory.create( PortfolioRoleFactory.create(
role=Roles.get("officer"), role=Roles.get("officer"),
@ -514,3 +536,111 @@ def test_submit_so_review(app, client, user_session):
assert task_order.dd_254 assert task_order.dd_254
assert task_order.dd_254.certifying_official == dd_254_data["certifying_official"] assert task_order.dd_254.certifying_official == dd_254_data["certifying_official"]
def test_resend_invite_when_invalid_invite_officer(
app, client, user_session, portfolio, user
):
queue_length = len(queue.get_queue())
task_order = TaskOrderFactory.create(
portfolio=portfolio, contracting_officer=user, ko_invite=True
)
PortfolioRoleFactory.create(
role=Roles.get("owner"),
portfolio=portfolio,
user=user,
status=PortfolioStatus.ACTIVE,
)
user_session(user)
response = client.post(
url_for(
"portfolios.resend_invite",
portfolio_id=portfolio.id,
task_order_id=task_order.id,
_external=True,
),
data={"invite_type": "invalid_invite_type"},
)
assert response.status_code == 404
assert len(queue.get_queue()) == queue_length
def test_resend_invite_when_officer_type_missing(
app, client, user_session, portfolio, user
):
queue_length = len(queue.get_queue())
task_order = TaskOrderFactory.create(
portfolio=portfolio, contracting_officer=None, ko_invite=True
)
PortfolioRoleFactory.create(
role=Roles.get("owner"),
portfolio=portfolio,
user=user,
status=PortfolioStatus.ACTIVE,
)
user_session(user)
response = client.post(
url_for(
"portfolios.resend_invite",
portfolio_id=portfolio.id,
task_order_id=task_order.id,
_external=True,
),
data={"invite_type": "contracting_officer_invite"},
)
assert response.status_code == 404
assert len(queue.get_queue()) == queue_length
def test_resend_invite_when_ko(app, client, user_session, portfolio, user):
queue_length = len(queue.get_queue())
task_order = TaskOrderFactory.create(
portfolio=portfolio, contracting_officer=user, ko_invite=True
)
portfolio_role = PortfolioRoleFactory.create(
role=Roles.get("owner"),
portfolio=portfolio,
user=user,
status=PortfolioStatus.ACTIVE,
)
original_invitation = Invitations.create(
inviter=user, portfolio_role=portfolio_role, email=user.email
)
user_session(user)
response = client.post(
url_for(
"portfolios.resend_invite",
portfolio_id=portfolio.id,
task_order_id=task_order.id,
_external=True,
),
data={"invite_type": "ko_invite"},
)
assert original_invitation.status == InvitationStatus.REVOKED
assert response.status_code == 302
assert (
url_for(
"portfolios.task_order_invitations",
portfolio_id=portfolio.id,
task_order_id=task_order.id,
_external=True,
)
== response.headers["Location"]
)
assert len(queue.get_queue()) == queue_length + 1

View File

@ -23,6 +23,11 @@ common:
manage: manage manage: manage
save_and_continue: Save & Continue save_and_continue: Save & Continue
sign: Sign sign: Sign
officer_helpers:
underscore_to_friendly:
contracting_officer: Contracting Officer
security_officer: Security Officer
contracting_officer_representative: Contracting Officer Representative
components: components:
modal: modal:
close: Close close: Close