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)
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

@ -14,6 +14,10 @@ class TaskOrderError(Exception):
pass
class InvalidOfficerError(Exception):
pass
class TaskOrders(object):
SECTIONS = {
"app_info": [
@ -145,6 +149,15 @@ class TaskOrders(object):
"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
def add_officer(cls, user, task_order, officer_type, officer_data):
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 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.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 +103,66 @@ 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):
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/<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:
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

@ -6,6 +6,7 @@
{% from "components/icon.html" import Icon %}
{% from "components/text_input.html" import TextInput %}
{% macro Link(text, icon_name, onClick=None, url='#', classes='') %}
<a href="{{ url }}" {% if onClick %}v-on:click="{{ onClick }}"{% endif %} class="icon-link {{ classes }}">
{{ Icon(icon_name) }}
@ -14,51 +15,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) %}
@ -66,10 +70,18 @@
<h2 class="officer__title">{{ ("task_orders.invitations." + officer_type + ".title") | translate }}</h2>
<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>
<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"] %}
{% set email = task_order[prefix + "_email"] %}
@ -137,24 +149,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

@ -1,6 +1,11 @@
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.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():
finished = DD254Factory.create()
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.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,111 @@ 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,
_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
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