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)
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
def resend(cls, user, portfolio_id, token):
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 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.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.models.task_order import Status as TaskOrderStatus
from atst.forms.ko_review import KOReviewForm
from atst.forms.dd_254 import DD254Form
from atst.services.invitation import update_officer_invitations
from atst.models.invitation import Status as InvitationStatus
from atst.utils.flash import formatted_flash as flash
from atst.services.invitation import (
update_officer_invitations,
OFFICER_INVITATIONS,
Invitation as InvitationService,
)
@portfolios_bp.route("/portfolios/<portfolio_id>/task_orders")
@ -96,6 +104,61 @@ def ko_review(portfolio_id, task_order_id):
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/<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:
return render_template(
"portfolios/task_orders/invitations.html",
portfolio=portfolio,
task_order=task_order,
form=form,
return (
render_template(
"portfolios/task_orders/invitations.html",
portfolio=portfolio,
task_order=task_order,
form=form,
),
400,
)

View File

@ -5,43 +5,43 @@ from atst.queue import queue
from atst.domain.task_orders import TaskOrders
from atst.domain.portfolio_roles import PortfolioRoles
OFFICER_INVITATIONS = [
{
"field": "ko_invite",
OFFICER_INVITATIONS = {
"ko_invite": {
"role": "contracting_officer",
"subject": "Review a task order",
"template": "emails/invitation.txt",
},
{
"field": "cor_invite",
"cor_invite": {
"role": "contracting_officer_representative",
"subject": "Help with a task order",
"template": "emails/invitation.txt",
},
{
"field": "so_invite",
"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"])
for invite_type in dict.keys(OFFICER_INVITATIONS):
invite_opts = OFFICER_INVITATIONS[invite_type]
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(
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)
invite_service = Invitation(
user,
pf_officer_member,
officer_data["email"],
subject=officer_type["subject"],
email_template=officer_type["template"],
subject=invite_opts["subject"],
email_template=invite_opts["template"],
)
invite_service.invite()

View File

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

View File

@ -1,3 +1,8 @@
// 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: {
action: String,
btn_text: String,
btn_icon: String,
btn_class: String,
cancel_btn_text: String,
confirm_btn_text: String,
confirm_msg: String,
csrf_token: String,
name: String,
},
template: `
@ -26,7 +29,10 @@ export default {
</button>
</div>
</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>
`,
}

View File

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

View File

@ -510,6 +510,10 @@
margin: 0 $gap;
}
.v-popover {
display: inline-block;
}
.remove {
color: $color-red;
.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
btn_text='{{ btn_text }}'
btn_icon='{{ btn_icon }}'
btn_class='{{ btn_class }}'
action='{{ action }}'
csrf_token='{{ csrf_token() }}'
confirm_msg='{{ confirm_msg }}'

View File

@ -5,6 +5,8 @@
{% from "components/checkbox_input.html" import CheckboxInput %}
{% from "components/icon.html" import Icon %}
{% from "components/text_input.html" import TextInput %}
{% from "components/confirmation_button.html" import ConfirmationButton %}
{% macro Link(text, icon_name, onClick=None, url='#', classes='') %}
<a href="{{ url }}" {% if onClick %}v-on:click="{{ onClick }}"{% endif %} class="icon-link {{ classes }}">
@ -14,51 +16,54 @@
{% endmacro %}
{% macro EditOfficerInfo(form, officer_type, invited) -%}
<template v-if="editing">
<div class='officer__form'>
<div class="edit-officer">
<h4>{{ ("task_orders.invitations." + officer_type + ".edit_title") | translate}}</h4>
</div>
<div class='form-row'>
<div class='form-col form-col--half'>
{{ TextInput(form.first_name) }}
<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">
<div class='officer__form'>
<div class="edit-officer">
<h4>{{ ("task_orders.invitations." + officer_type + ".edit_title") | translate}}</h4>
</div>
<div class='form-row'>
<div class='form-col form-col--half'>
{{ TextInput(form.first_name) }}
</div>
<div class='form-col form-col--half'>
{{ TextInput(form.last_name) }}
</div>
</div>
<div class='form-row'>
<div class='form-col form-col--half'>
{{ TextInput(form.email, placeholder='name@mail.mil', validation='email') }}
</div>
<div class='form-col form-col--half'>
{{ TextInput(form.phone_number, placeholder='(123) 456-7890', validation='usPhone') }}
</div>
</div>
<div class='form-row officer__form--dodId'>
<div class="form-col">
{% if not invited %}
<div class='form-row'>
{{ CheckboxInput(form.invite, label=(("forms.officers." + officer_type + "_invite") | translate)) }}
</div>
{% endif %}
<div class='form-row'>
{{ TextInput(form.dod_id, tooltip="task_orders.new.oversight.dod_id_tooltip" | translate, tooltip_title='Why', validation='dodId', disabled=invited)}}
<div class='form-col form-col--half'>
{{ TextInput(form.last_name) }}
</div>
</div>
<div class='form-row'>
<div class='form-col form-col--half'>
{{ TextInput(form.email, placeholder='name@mail.mil', validation='email') }}
</div>
<div class='form-col form-col--half'>
{{ TextInput(form.phone_number, placeholder='(123) 456-7890', validation='usPhone') }}
</div>
</div>
<div class='form-row officer__form--dodId'>
<div class="form-col">
{% if not invited %}
<div class='form-row'>
{{ CheckboxInput(form.invite, label=(("forms.officers." + officer_type + "_invite") | translate)) }}
</div>
{% endif %}
<div class='form-row'>
{{ TextInput(form.dod_id, tooltip="task_orders.new.oversight.dod_id_tooltip" | translate, tooltip_title='Why', validation='dodId', disabled=invited)}}
</div>
</div>
</div>
<div class='alert__actions officer__form--actions'>
<a href="#{{ officer_type }}" v-on:click="cancel" class="icon-link">
{{ Icon("x") }}
<span>Cancel</span>
</a>
<input type='submit' class='usa-button usa-button-primary' value='Save Changes' />
</div>
</div>
<div class='alert__actions officer__form--actions'>
<a href="#{{ officer_type }}" v-on:click="cancel" class="icon-link">
{{ Icon("x") }}
<span>Cancel</span>
</a>
<input type='submit' class='usa-button usa-button-primary' value='Save Changes' />
</div>
</div>
</template>
</template>
</form>
{% endmacro %}
{% 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>
<div>
{% set prefix = { "contracting_officer": "ko", "contracting_officer_representative": "cor", "security_officer": "so" }[officer_type] %}
{% set first_name = task_order[prefix + "_first_name"] %}
{% set last_name = task_order[prefix + "_last_name"] %}
@ -77,7 +81,6 @@
{% set dod_id = task_order[prefix + "_dod_id"] %}
{% set invited = False %}
{% if task_order[officer_type] %}
{% set invited = True %}
<div class="officer__info">
@ -94,7 +97,22 @@
</div>
<div class="officer__actions">
{{ 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") }}
</div>
{% elif first_name and last_name %}
@ -137,24 +155,21 @@
{% endmacro %}
{% block portfolio_content %}
<div class="task-order-invitations">
{% 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__heading">
<h1 class="task-order-invitations__heading subheading">
<div class="h2">Edit Task Order</div>
Oversight
</h1>
</div>
{% for officer in ["contracting_officer", "contracting_officer_representative", "security_officer"] %}
{{ OfficerInfo(task_order, officer, form[officer]) }}
{% endfor %}
<div class="panel">
<div class="panel__heading">
<h1 class="task-order-invitations__heading subheading">
<div class="h2">Edit Task Order</div>
Oversight
</h1>
</div>
</form>
{% for officer in ["contracting_officer", "contracting_officer_representative", "security_officer"] %}
{{ OfficerInfo(task_order, officer, form[officer]) }}
{% endfor %}
</div>
</div>
{% endblock %}

View File

@ -7,6 +7,7 @@ from atst.domain.invitations import (
InvitationError,
WrongUserError,
ExpiredError,
NotFoundError,
)
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]
assert "email" 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():
portfolio = PortfolioFactory.create()
user = UserFactory.create()
ws_role = PortfolioRoleFactory.create(
portfolio_role = PortfolioRoleFactory.create(
portfolio=portfolio, user=user, status=PortfolioRoleStatus.PENDING
)
invite = InvitationFactory.create(
expiration_time=datetime.datetime.now() - datetime.timedelta(minutes=60),
portfolio_role=ws_role,
portfolio_role=portfolio_role,
)
assert not invite.is_revokable
@ -28,18 +28,20 @@ def test_expired_invite_is_not_revokable():
def test_unexpired_invite_is_revokable():
portfolio = PortfolioFactory.create()
user = UserFactory.create()
ws_role = PortfolioRoleFactory.create(
portfolio_role = PortfolioRoleFactory.create(
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
def test_invite_is_not_revokable_if_invite_is_not_pending():
portfolio = PortfolioFactory.create()
user = UserFactory.create()
ws_role = PortfolioRoleFactory.create(
portfolio_role = PortfolioRoleFactory.create(
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

View File

@ -5,11 +5,14 @@ from datetime import timedelta, date
from atst.domain.roles import Roles
from atst.domain.task_orders import TaskOrders
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.queue import queue
from atst.domain.invitations import Invitations
from tests.factories import (
PortfolioFactory,
InvitationFactory,
PortfolioRoleFactory,
TaskOrderFactory,
UserFactory,
@ -20,9 +23,18 @@ from tests.factories import (
from tests.utils import captured_templates
@pytest.fixture
def portfolio():
return PortfolioFactory.create()
@pytest.fixture
def user():
return UserFactory.create()
class TestPortfolioFunding:
def test_portfolio_with_no_task_orders(self, app, user_session):
portfolio = PortfolioFactory.create()
def test_portfolio_with_no_task_orders(self, app, user_session, portfolio):
user_session(portfolio.owner)
with captured_templates(app) as templates:
@ -38,8 +50,7 @@ class TestPortfolioFunding:
assert context["active_task_orders"] == []
assert context["expired_task_orders"] == []
def test_funded_portfolio(self, app, user_session):
portfolio = PortfolioFactory.create()
def test_funded_portfolio(self, app, user_session, portfolio):
user_session(portfolio.owner)
pending_to = TaskOrderFactory.create(portfolio=portfolio)
@ -71,8 +82,7 @@ class TestPortfolioFunding:
assert context["funding_end_date"] is end_date
assert context["total_balance"] == active_to1.budget + active_to2.budget
def test_expiring_and_funded_portfolio(self, app, user_session):
portfolio = PortfolioFactory.create()
def test_expiring_and_funded_portfolio(self, app, user_session, portfolio):
user_session(portfolio.owner)
expiring_to = TaskOrderFactory.create(
@ -98,8 +108,7 @@ class TestPortfolioFunding:
assert context["funding_end_date"] is active_to.end_date
assert context["funded"] == True
def test_expiring_and_unfunded_portfolio(self, app, user_session):
portfolio = PortfolioFactory.create()
def test_expiring_and_unfunded_portfolio(self, app, user_session, portfolio):
user_session(portfolio.owner)
expiring_to = TaskOrderFactory.create(
@ -154,6 +163,16 @@ class TestTaskOrderInvitations:
assert updated_task_order.so_first_name == "Boba"
assert updated_task_order.so_last_name == "Fett"
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):
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_phone_number == "0123456789"
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):
queue_length = len(queue.get_queue())
@ -196,19 +225,18 @@ class TestTaskOrderInvitations:
updated_task_order = TaskOrders.get(self.portfolio.owner, self.task_order.id)
assert updated_task_order.so_first_name != "Boba"
assert len(queue.get_queue()) == queue_length
assert response.status_code == 400
def test_ko_can_view_task_order(client, user_session):
portfolio = PortfolioFactory.create()
ko = UserFactory.create()
def test_ko_can_view_task_order(client, user_session, portfolio, user):
PortfolioRoleFactory.create(
role=Roles.get("officer"),
role=Roles.get("owner"),
portfolio=portfolio,
user=ko,
user=user,
status=PortfolioStatus.ACTIVE,
)
task_order = TaskOrderFactory.create(portfolio=portfolio, contracting_officer=ko)
user_session(ko)
task_order = TaskOrderFactory.create(portfolio=portfolio, contracting_officer=user)
user_session(user)
response = client.get(
url_for(
@ -220,7 +248,7 @@ def test_ko_can_view_task_order(client, user_session):
assert response.status_code == 200
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(
url_for(
"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()
def test_can_view_task_order_invitations_when_complete(client, user_session):
portfolio = PortfolioFactory.create()
def test_can_view_task_order_invitations_when_complete(client, user_session, portfolio):
user_session(portfolio.owner)
task_order = TaskOrderFactory.create(portfolio=portfolio)
response = client.get(
@ -246,8 +273,9 @@ def test_can_view_task_order_invitations_when_complete(client, user_session):
assert response.status_code == 200
def test_cant_view_task_order_invitations_when_not_complete(client, user_session):
portfolio = PortfolioFactory.create()
def test_cant_view_task_order_invitations_when_not_complete(
client, user_session, portfolio
):
user_session(portfolio.owner)
task_order = TaskOrderFactory.create(portfolio=portfolio, clin_01=None)
response = client.get(
@ -324,8 +352,7 @@ def test_cor_cant_view_review_until_to_completed(client, user_session):
assert response.status_code == 404
def test_mo_redirected_to_build_page(client, user_session):
portfolio = PortfolioFactory.create()
def test_mo_redirected_to_build_page(client, user_session, portfolio):
user_session(portfolio.owner)
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
def test_cor_redirected_to_build_page(client, user_session):
portfolio = PortfolioFactory.create()
def test_cor_redirected_to_build_page(client, user_session, portfolio):
cor = UserFactory.create()
PortfolioRoleFactory.create(
role=Roles.get("officer"),
@ -354,20 +380,18 @@ def test_cor_redirected_to_build_page(client, user_session):
assert response.status_code == 200
def test_submit_completed_ko_review_page_as_cor(client, user_session, pdf_upload):
portfolio = PortfolioFactory.create()
cor = UserFactory.create()
def test_submit_completed_ko_review_page_as_cor(
client, user_session, pdf_upload, portfolio, user
):
PortfolioRoleFactory.create(
role=Roles.get("officer"),
portfolio=portfolio,
user=cor,
user=user,
status=PortfolioStatus.ACTIVE,
)
task_order = TaskOrderFactory.create(
portfolio=portfolio, contracting_officer_representative=cor
portfolio=portfolio, contracting_officer_representative=user
)
form_data = {
@ -379,7 +403,7 @@ def test_submit_completed_ko_review_page_as_cor(client, user_session, pdf_upload
"pdf": pdf_upload,
}
user_session(cor)
user_session(user)
response = client.post(
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):
portfolio = PortfolioFactory.create()
def test_submit_completed_ko_review_page_as_ko(
client, user_session, pdf_upload, portfolio
):
ko = UserFactory.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
def test_so_review_page(app, client, user_session):
portfolio = PortfolioFactory.create()
def test_so_review_page(app, client, user_session, portfolio):
so = UserFactory.create()
PortfolioRoleFactory.create(
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):
portfolio = PortfolioFactory.create()
def test_submit_so_review(app, client, user_session, portfolio):
so = UserFactory.create()
PortfolioRoleFactory.create(
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.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
save_and_continue: Save & Continue
sign: Sign
officer_helpers:
underscore_to_friendly:
contracting_officer: Contracting Officer
security_officer: Security Officer
contracting_officer_representative: Contracting Officer Representative
components:
modal:
close: Close