Merge pull request #694 from dod-ccpo/resend-invitation-html

Resend Officer Invites from Manage Invitations Page
This commit is contained in:
George Drummond 2019-03-13 16:30:03 -04:00 committed by GitHub
commit 4b9e27daf7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 438 additions and 132 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

@ -4,15 +4,23 @@ 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.models.invitation import Status as InvitationStatus
from atst.forms.dd_254 import DD254Form from atst.utils.flash import formatted_flash as flash
from atst.services.invitation import update_officer_invitations from atst.services.invitation import (
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 +104,61 @@ 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):
invite_type = http_request.args.get("invite_type")
if invite_type not in 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)
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 or (invitation.status is not InvitationStatus.PENDING):
raise NotFoundError("invitation")
Invitations.revoke(token=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 +236,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

@ -1,3 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ConfirmationPopover matches snapshot 1`] = `<v-popover-stub placement="top-start" delay="0" offset="0" trigger="click" container="body" popperoptions="[object Object]" popoverclass="vue-popover-theme" popoverbaseclass="tooltip popover" popoverinnerclass="tooltip-inner popover-inner" popoverwrapperclass="wrapper" popoverarrowclass="tooltip-arrow popover-arrow" autohide="true" handleresize="true"><template></template> <button type="button" class="tooltip-target">Do something dangerous</button></v-popover-stub>`; exports[`ConfirmationPopover matches snapshot 1`] = `
<v-popover-stub placement="top-start" delay="0" offset="0" trigger="click" container="body" popperoptions="[object Object]" popoverclass="vue-popover-theme" popoverbaseclass="tooltip popover" popoverinnerclass="tooltip-inner popover-inner" popoverwrapperclass="wrapper" popoverarrowclass="tooltip-arrow popover-arrow" autohide="true" handleresize="true"><template></template> <button type="button" class="tooltip-target">
<div></div>
Do something dangerous
</button></v-popover-stub>
`;

View File

@ -4,10 +4,13 @@ export default {
props: { props: {
action: String, action: String,
btn_text: String, btn_text: String,
btn_icon: String,
btn_class: String,
cancel_btn_text: String, cancel_btn_text: String,
confirm_btn_text: String, confirm_btn_text: String,
confirm_msg: String, confirm_msg: String,
csrf_token: String, csrf_token: String,
name: String,
}, },
template: ` template: `
@ -26,7 +29,10 @@ export default {
</button> </button>
</div> </div>
</template> </template>
<button class="tooltip-target" type="button">{{ btn_text }}</button> <button class="tooltip-target" v-bind:class="[btn_class]" type="button">
<div v-html="btn_icon" />
{{ btn_text }}
</button>
</v-popover> </v-popover>
`, `,
} }

View File

@ -1,6 +1,7 @@
import FormMixin from '../../mixins/form' import FormMixin from '../../mixins/form'
import checkboxinput from '../checkbox_input' import checkboxinput from '../checkbox_input'
import textinput from '../text_input' import textinput from '../text_input'
import ConfirmationPopover from '../confirmation_popover'
export default { export default {
name: 'edit-officer-form', name: 'edit-officer-form',
@ -10,6 +11,7 @@ export default {
components: { components: {
checkboxinput, checkboxinput,
textinput, textinput,
ConfirmationPopover,
}, },
props: { props: {

View File

@ -510,6 +510,10 @@
margin: 0 $gap; margin: 0 $gap;
} }
.v-popover {
display: inline-block;
}
.remove { .remove {
color: $color-red; color: $color-red;
.icon { .icon {

View File

@ -1,6 +1,16 @@
{% macro ConfirmationButton(btn_text, action, confirm_msg="Are you sure?", confirm_btn="Confirm", cancel_btn="Cancel") -%} {% macro ConfirmationButton(
btn_text,
action,
btn_icon=None,
btn_class=None,
confirm_msg="Are you sure?",
confirm_btn="Confirm",
cancel_btn="Cancel")
-%}
<confirmation-popover <confirmation-popover
btn_text='{{ btn_text }}' btn_text='{{ btn_text }}'
btn_icon='{{ btn_icon }}'
btn_class='{{ btn_class }}'
action='{{ action }}' action='{{ action }}'
csrf_token='{{ csrf_token() }}' csrf_token='{{ csrf_token() }}'
confirm_msg='{{ confirm_msg }}' confirm_msg='{{ confirm_msg }}'

View File

@ -5,6 +5,8 @@
{% from "components/checkbox_input.html" import CheckboxInput %} {% from "components/checkbox_input.html" import CheckboxInput %}
{% 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 %}
{% from "components/confirmation_button.html" import ConfirmationButton %}
{% 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 }}">
@ -14,6 +16,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 +63,7 @@
</div> </div>
</div> </div>
</template> </template>
</form>
{% endmacro %} {% endmacro %}
{% macro OfficerInfo(task_order, officer_type, form) %} {% macro OfficerInfo(task_order, officer_type, form) %}
@ -68,7 +73,6 @@
<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 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"] %}
@ -77,7 +81,6 @@
{% set dod_id = task_order[prefix + "_dod_id"] %} {% set dod_id = task_order[prefix + "_dod_id"] %}
{% set invited = False %} {% set invited = False %}
{% if task_order[officer_type] %} {% if task_order[officer_type] %}
{% set invited = True %} {% set invited = True %}
<div class="officer__info"> <div class="officer__info">
@ -94,7 +97,22 @@
</div> </div>
<div class="officer__actions"> <div class="officer__actions">
{{ Link("Update", "edit", onClick="edit") }} {{ Link("Update", "edit", onClick="edit") }}
{{ Link("Resend Invitation", "avatar") }} {% set invite_type = [prefix + "_invite"] %}
{{
ConfirmationButton(
btn_text="Resend Invitation",
action=url_for(
"portfolios.resend_invite",
portfolio_id=portfolio.id,
task_order_id=task_order.id,
invite_type=invite_type,
),
btn_icon=Icon('avatar'),
btn_class="icon-link",
)
}}
{{ Link("Remove", "trash", classes="remove") }} {{ Link("Remove", "trash", classes="remove") }}
</div> </div>
{% elif first_name and last_name %} {% elif first_name and last_name %}
@ -137,12 +155,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 +171,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

@ -15,12 +15,12 @@ from tests.factories import (
def test_expired_invite_is_not_revokable(): def test_expired_invite_is_not_revokable():
portfolio = PortfolioFactory.create() portfolio = PortfolioFactory.create()
user = UserFactory.create() user = UserFactory.create()
ws_role = PortfolioRoleFactory.create( portfolio_role = PortfolioRoleFactory.create(
portfolio=portfolio, user=user, status=PortfolioRoleStatus.PENDING portfolio=portfolio, user=user, status=PortfolioRoleStatus.PENDING
) )
invite = InvitationFactory.create( invite = InvitationFactory.create(
expiration_time=datetime.datetime.now() - datetime.timedelta(minutes=60), expiration_time=datetime.datetime.now() - datetime.timedelta(minutes=60),
portfolio_role=ws_role, portfolio_role=portfolio_role,
) )
assert not invite.is_revokable assert not invite.is_revokable
@ -28,18 +28,20 @@ def test_expired_invite_is_not_revokable():
def test_unexpired_invite_is_revokable(): def test_unexpired_invite_is_revokable():
portfolio = PortfolioFactory.create() portfolio = PortfolioFactory.create()
user = UserFactory.create() user = UserFactory.create()
ws_role = PortfolioRoleFactory.create( portfolio_role = PortfolioRoleFactory.create(
portfolio=portfolio, user=user, status=PortfolioRoleStatus.PENDING portfolio=portfolio, user=user, status=PortfolioRoleStatus.PENDING
) )
invite = InvitationFactory.create(portfolio_role=ws_role) invite = InvitationFactory.create(portfolio_role=portfolio_role)
assert invite.is_revokable assert invite.is_revokable
def test_invite_is_not_revokable_if_invite_is_not_pending(): def test_invite_is_not_revokable_if_invite_is_not_pending():
portfolio = PortfolioFactory.create() portfolio = PortfolioFactory.create()
user = UserFactory.create() user = UserFactory.create()
ws_role = PortfolioRoleFactory.create( portfolio_role = PortfolioRoleFactory.create(
portfolio=portfolio, user=user, status=PortfolioRoleStatus.PENDING portfolio=portfolio, user=user, status=PortfolioRoleStatus.PENDING
) )
invite = InvitationFactory.create(portfolio_role=ws_role, status=Status.ACCEPTED) invite = InvitationFactory.create(
portfolio_role=portfolio_role, status=Status.ACCEPTED
)
assert not invite.is_revokable assert not invite.is_revokable

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,149 @@ 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,
invite_type="ko_invite",
_external=True,
)
)
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
def test_resend_invite_when_not_pending(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 = InvitationFactory.create(
inviter=user,
portfolio_role=portfolio_role,
email=user.email,
status=InvitationStatus.ACCEPTED,
)
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.ACCEPTED
assert response.status_code == 404
assert len(queue.get_queue()) == queue_length

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