Merge pull request #676 from dod-ccpo/to-edit-sign-permissions
Break KO signing into two steps
This commit is contained in:
commit
c4383c8859
@ -36,6 +36,27 @@ class Authorization(object):
|
|||||||
def is_ccpo(cls, user):
|
def is_ccpo(cls, user):
|
||||||
return user.atat_role.name == "ccpo"
|
return user.atat_role.name == "ccpo"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def is_ko(cls, user, task_order):
|
||||||
|
return user == task_order.contracting_officer
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def is_cor(cls, user, task_order):
|
||||||
|
return user == task_order.contracting_officer_representative
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def is_so(cls, user, task_order):
|
||||||
|
return user == task_order.security_officer
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def check_is_ko_or_cor(cls, user, task_order):
|
||||||
|
if Authorization.is_ko(user, task_order) or Authorization.is_cor(
|
||||||
|
user, task_order
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
raise UnauthorizedError(user, "not KO or COR")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def check_is_ko(cls, user, task_order):
|
def check_is_ko(cls, user, task_order):
|
||||||
if task_order.contracting_officer != user:
|
if task_order.contracting_officer != user:
|
||||||
|
@ -120,6 +120,10 @@ class TaskOrders(object):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def is_signed_by_ko(cls, task_order):
|
||||||
|
return task_order.signer_dod_id is not None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def mission_owner_sections(cls):
|
def mission_owner_sections(cls):
|
||||||
section_list = TaskOrders.SECTIONS
|
section_list = TaskOrders.SECTIONS
|
||||||
|
@ -65,10 +65,14 @@ def view_task_order(portfolio_id, task_order_id):
|
|||||||
dd_254_complete = DD254s.is_complete(task_order.dd_254)
|
dd_254_complete = DD254s.is_complete(task_order.dd_254)
|
||||||
return render_template(
|
return render_template(
|
||||||
"portfolios/task_orders/show.html",
|
"portfolios/task_orders/show.html",
|
||||||
|
dd_254_complete=dd_254_complete,
|
||||||
|
is_cor=Authorization.is_cor(g.current_user, task_order),
|
||||||
|
is_ko=Authorization.is_ko(g.current_user, task_order),
|
||||||
|
is_so=Authorization.is_so(g.current_user, task_order),
|
||||||
|
is_to_signed=TaskOrders.is_signed_by_ko(task_order),
|
||||||
portfolio=portfolio,
|
portfolio=portfolio,
|
||||||
task_order=task_order,
|
task_order=task_order,
|
||||||
to_form_complete=to_form_complete,
|
to_form_complete=to_form_complete,
|
||||||
dd_254_complete=dd_254_complete,
|
|
||||||
user=g.current_user,
|
user=g.current_user,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -78,7 +82,8 @@ def ko_review(portfolio_id, task_order_id):
|
|||||||
task_order = TaskOrders.get(g.current_user, task_order_id)
|
task_order = TaskOrders.get(g.current_user, task_order_id)
|
||||||
portfolio = Portfolios.get(g.current_user, portfolio_id)
|
portfolio = Portfolios.get(g.current_user, portfolio_id)
|
||||||
|
|
||||||
Authorization.check_is_ko(g.current_user, task_order)
|
Authorization.check_is_ko_or_cor(g.current_user, task_order)
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"/portfolios/task_orders/review.html",
|
"/portfolios/task_orders/review.html",
|
||||||
portfolio=portfolio,
|
portfolio=portfolio,
|
||||||
@ -95,12 +100,22 @@ def submit_ko_review(portfolio_id, task_order_id, form=None):
|
|||||||
form_data = {**http_request.form, **http_request.files}
|
form_data = {**http_request.form, **http_request.files}
|
||||||
form = KOReviewForm(form_data)
|
form = KOReviewForm(form_data)
|
||||||
|
|
||||||
Authorization.check_is_ko(g.current_user, task_order)
|
Authorization.check_is_ko_or_cor(g.current_user, task_order)
|
||||||
|
|
||||||
if form.validate():
|
if form.validate():
|
||||||
TaskOrders.update(user=g.current_user, task_order=task_order, **form.data)
|
TaskOrders.update(user=g.current_user, task_order=task_order, **form.data)
|
||||||
|
if Authorization.is_ko(g.current_user, task_order):
|
||||||
return redirect(
|
return redirect(
|
||||||
url_for("task_orders.signature_requested", task_order_id=task_order_id)
|
url_for("task_orders.signature_requested", task_order_id=task_order_id)
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
return redirect(
|
||||||
|
url_for(
|
||||||
|
"portfolios.view_task_order",
|
||||||
|
task_order_id=task_order_id,
|
||||||
|
portfolio_id=portfolio_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
return render_template(
|
return render_template(
|
||||||
"/portfolios/task_orders/review.html",
|
"/portfolios/task_orders/review.html",
|
||||||
|
@ -14,7 +14,7 @@ def find_unsigned_ko_to(task_order_id):
|
|||||||
task_order = TaskOrders.get(g.current_user, task_order_id)
|
task_order = TaskOrders.get(g.current_user, task_order_id)
|
||||||
Authorization.check_is_ko(g.current_user, task_order)
|
Authorization.check_is_ko(g.current_user, task_order)
|
||||||
|
|
||||||
if task_order.signer_dod_id is not None:
|
if TaskOrders.is_signed_by_ko(task_order):
|
||||||
raise NotFoundError("task_order")
|
raise NotFoundError("task_order")
|
||||||
|
|
||||||
return task_order
|
return task_order
|
||||||
|
@ -124,27 +124,37 @@
|
|||||||
<div id="next-steps" class="task-order-next-steps">
|
<div id="next-steps" class="task-order-next-steps">
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<h3 class="task-order-next-steps__panel-head panel__content">{{ "task_orders.view.whats_next" | translate }}</h3>
|
<h3 class="task-order-next-steps__panel-head panel__content">{{ "task_orders.view.whats_next" | translate }}</h3>
|
||||||
{% call Step(
|
{{
|
||||||
|
Step(
|
||||||
|
button_text='Edit',
|
||||||
|
button_url=url_for("task_orders.new", screen=1, task_order_id=task_order.id),
|
||||||
|
complete=to_form_complete,
|
||||||
description="task_orders.view.steps.draft" | translate({
|
description="task_orders.view.steps.draft" | translate({
|
||||||
"contact": officer_name(task_order.creator)
|
"contact": officer_name(task_order.creator)
|
||||||
})| safe,
|
})| safe,
|
||||||
button_url=url_for("task_orders.new", screen=1, task_order_id=task_order.id),
|
)
|
||||||
button_text='Edit',
|
}}
|
||||||
complete=to_form_complete) %}
|
{{
|
||||||
{% endcall %}
|
Step(
|
||||||
{% set is_so = user == task_order.security_officer %}
|
button_text=is_so and ("common.edit" | translate),
|
||||||
{{ Step(
|
button_url=is_so and url_for("portfolios.so_review", portfolio_id=portfolio.id, task_order_id=task_order.id),
|
||||||
|
complete=dd_254_complete,
|
||||||
description="task_orders.view.steps.security" | translate({
|
description="task_orders.view.steps.security" | translate({
|
||||||
"security_officer": officer_name(task_order.security_officer)
|
"security_officer": officer_name(task_order.security_officer)
|
||||||
}) | safe,
|
}) | safe,
|
||||||
button_url=is_so and url_for("portfolios.so_review", portfolio_id=portfolio.id, task_order_id=task_order.id),
|
)
|
||||||
button_text=is_so and 'Edit',
|
}}
|
||||||
complete=dd_254_complete) }}
|
|
||||||
{% call Step(
|
{% call Step(
|
||||||
description="task_orders.view.steps.record" | translate({
|
description="task_orders.view.steps.record" | translate({
|
||||||
"contracting_officer": officer_name(task_order.contracting_officer),
|
"contracting_officer": officer_name(task_order.contracting_officer),
|
||||||
"contracting_officer_representative": officer_name(task_order.contracting_officer_representative)
|
"contracting_officer_representative": officer_name(task_order.contracting_officer_representative)
|
||||||
}) | safe,
|
}) | safe,
|
||||||
|
button_url=url_for(
|
||||||
|
"portfolios.ko_review",
|
||||||
|
portfolio_id=portfolio.id,
|
||||||
|
task_order_id=task_order.id,
|
||||||
|
),
|
||||||
|
button_text=(is_cor or is_ko) and ("common.edit" | translate),
|
||||||
complete=False) %}
|
complete=False) %}
|
||||||
<div class='alert alert--warning'>
|
<div class='alert alert--warning'>
|
||||||
<div class='alert__content'>
|
<div class='alert__content'>
|
||||||
@ -154,14 +164,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
{% set is_ko = user == task_order.contracting_officer %}
|
{{
|
||||||
{{ Step(
|
Step(
|
||||||
|
button_text=is_ko and not is_to_signed and ("common.sign" | translate),
|
||||||
|
button_url=is_ko and url_for(
|
||||||
|
"portfolios.ko_review",
|
||||||
|
portfolio_id=portfolio.id,
|
||||||
|
task_order_id=task_order.id,
|
||||||
|
),
|
||||||
|
complete=is_to_signed,
|
||||||
description="task_orders.view.steps.sign" | translate({
|
description="task_orders.view.steps.sign" | translate({
|
||||||
"contracting_officer": officer_name(task_order.contracting_officer)
|
"contracting_officer": officer_name(task_order.contracting_officer)
|
||||||
}) | safe,
|
}) | safe,
|
||||||
button_url=is_ko and url_for("portfolios.ko_review", portfolio_id=portfolio.id, task_order_id=task_order.id),
|
)
|
||||||
button_text=is_ko and 'Sign',
|
}}
|
||||||
complete=False) }}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="task-order-sidebar col">
|
<div class="task-order-sidebar col">
|
||||||
|
42
tests/domain/test_authz.py
Normal file
42
tests/domain/test_authz.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from tests.factories import TaskOrderFactory, UserFactory
|
||||||
|
from atst.domain.authz import Authorization
|
||||||
|
from atst.domain.exceptions import UnauthorizedError
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def invalid_user():
|
||||||
|
return UserFactory.create()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def task_order():
|
||||||
|
return TaskOrderFactory.create()
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_ko(task_order, invalid_user):
|
||||||
|
assert not Authorization.is_ko(invalid_user, task_order)
|
||||||
|
assert Authorization.is_ko(task_order.contracting_officer, task_order)
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_cor(task_order, invalid_user):
|
||||||
|
assert not Authorization.is_cor(invalid_user, task_order)
|
||||||
|
assert Authorization.is_cor(
|
||||||
|
task_order.contracting_officer_representative, task_order
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_so(task_order, invalid_user):
|
||||||
|
assert Authorization.is_so(task_order.security_officer, task_order)
|
||||||
|
assert not Authorization.is_so(invalid_user, task_order)
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_is_ko_or_cor(task_order, invalid_user):
|
||||||
|
assert Authorization.check_is_ko_or_cor(
|
||||||
|
task_order.contracting_officer_representative, task_order
|
||||||
|
)
|
||||||
|
assert Authorization.check_is_ko_or_cor(task_order.contracting_officer, task_order)
|
||||||
|
|
||||||
|
with pytest.raises(UnauthorizedError):
|
||||||
|
Authorization.check_is_ko_or_cor(invalid_user, task_order)
|
@ -13,6 +13,17 @@ from tests.factories import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_signed_by_ko():
|
||||||
|
user = UserFactory.create()
|
||||||
|
task_order = TaskOrderFactory.create(contracting_officer=user)
|
||||||
|
|
||||||
|
assert not TaskOrders.is_signed_by_ko(task_order)
|
||||||
|
|
||||||
|
TaskOrders.update(user, task_order, signer_dod_id=user.dod_id)
|
||||||
|
|
||||||
|
assert TaskOrders.is_signed_by_ko(task_order)
|
||||||
|
|
||||||
|
|
||||||
def test_section_completion_status():
|
def test_section_completion_status():
|
||||||
dict_keys = [k for k in TaskOrders.SECTIONS.keys()]
|
dict_keys = [k for k in TaskOrders.SECTIONS.keys()]
|
||||||
section = dict_keys[0]
|
section = dict_keys[0]
|
||||||
|
@ -234,23 +234,50 @@ def test_cant_view_task_order_invitations_when_not_complete(client, user_session
|
|||||||
def test_ko_can_view_ko_review_page(client, user_session):
|
def test_ko_can_view_ko_review_page(client, user_session):
|
||||||
portfolio = PortfolioFactory.create()
|
portfolio = PortfolioFactory.create()
|
||||||
ko = UserFactory.create()
|
ko = UserFactory.create()
|
||||||
|
cor = UserFactory.create()
|
||||||
|
|
||||||
PortfolioRoleFactory.create(
|
PortfolioRoleFactory.create(
|
||||||
role=Roles.get("officer"),
|
role=Roles.get("officer"),
|
||||||
portfolio=portfolio,
|
portfolio=portfolio,
|
||||||
user=ko,
|
user=ko,
|
||||||
status=PortfolioStatus.ACTIVE,
|
status=PortfolioStatus.ACTIVE,
|
||||||
)
|
)
|
||||||
task_order = TaskOrderFactory.create(portfolio=portfolio, contracting_officer=ko)
|
PortfolioRoleFactory.create(
|
||||||
|
role=Roles.get("officer"),
|
||||||
|
portfolio=portfolio,
|
||||||
|
user=cor,
|
||||||
|
status=PortfolioStatus.ACTIVE,
|
||||||
|
)
|
||||||
|
task_order = TaskOrderFactory.create(
|
||||||
|
portfolio=portfolio,
|
||||||
|
contracting_officer=ko,
|
||||||
|
contracting_officer_representative=cor,
|
||||||
|
)
|
||||||
|
request_url = url_for(
|
||||||
|
"portfolios.ko_review", portfolio_id=portfolio.id, task_order_id=task_order.id
|
||||||
|
)
|
||||||
|
|
||||||
|
#
|
||||||
|
# KO returns 200
|
||||||
|
#
|
||||||
user_session(ko)
|
user_session(ko)
|
||||||
response = client.get(
|
response = client.get(request_url)
|
||||||
url_for(
|
|
||||||
"portfolios.ko_review",
|
|
||||||
portfolio_id=portfolio.id,
|
|
||||||
task_order_id=task_order.id,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
#
|
||||||
|
# COR returns 200
|
||||||
|
#
|
||||||
|
user_session(cor)
|
||||||
|
response = client.get(request_url)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
#
|
||||||
|
# Random user raises UnauthorizedError
|
||||||
|
#
|
||||||
|
user_session(UserFactory.create())
|
||||||
|
response = client.get(request_url)
|
||||||
|
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 = PortfolioFactory.create()
|
portfolio = PortfolioFactory.create()
|
||||||
@ -282,17 +309,66 @@ 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(client, user_session, pdf_upload):
|
def test_submit_completed_ko_review_page_as_cor(client, user_session, pdf_upload):
|
||||||
portfolio = PortfolioFactory.create()
|
portfolio = PortfolioFactory.create()
|
||||||
|
|
||||||
|
cor = UserFactory.create()
|
||||||
|
|
||||||
|
PortfolioRoleFactory.create(
|
||||||
|
role=Roles.get("officer"),
|
||||||
|
portfolio=portfolio,
|
||||||
|
user=cor,
|
||||||
|
status=PortfolioStatus.ACTIVE,
|
||||||
|
)
|
||||||
|
|
||||||
|
task_order = TaskOrderFactory.create(
|
||||||
|
portfolio=portfolio, contracting_officer_representative=cor
|
||||||
|
)
|
||||||
|
|
||||||
|
form_data = {
|
||||||
|
"start_date": "02/10/2019",
|
||||||
|
"end_date": "03/10/2019",
|
||||||
|
"number": "1938745981",
|
||||||
|
"loa": "0813458013405",
|
||||||
|
"custom_clauses": "hi im a custom clause",
|
||||||
|
"pdf": pdf_upload,
|
||||||
|
}
|
||||||
|
|
||||||
|
user_session(cor)
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
url_for(
|
||||||
|
"portfolios.ko_review",
|
||||||
|
portfolio_id=portfolio.id,
|
||||||
|
task_order_id=task_order.id,
|
||||||
|
),
|
||||||
|
data=form_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert task_order.pdf
|
||||||
|
assert response.headers["Location"] == url_for(
|
||||||
|
"portfolios.view_task_order",
|
||||||
|
task_order_id=task_order.id,
|
||||||
|
portfolio_id=portfolio.id,
|
||||||
|
_external=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_submit_completed_ko_review_page_as_ko(client, user_session, pdf_upload):
|
||||||
|
portfolio = PortfolioFactory.create()
|
||||||
|
|
||||||
ko = UserFactory.create()
|
ko = UserFactory.create()
|
||||||
|
|
||||||
PortfolioRoleFactory.create(
|
PortfolioRoleFactory.create(
|
||||||
role=Roles.get("officer"),
|
role=Roles.get("officer"),
|
||||||
portfolio=portfolio,
|
portfolio=portfolio,
|
||||||
user=ko,
|
user=ko,
|
||||||
status=PortfolioStatus.ACTIVE,
|
status=PortfolioStatus.ACTIVE,
|
||||||
)
|
)
|
||||||
|
|
||||||
task_order = TaskOrderFactory.create(portfolio=portfolio, contracting_officer=ko)
|
task_order = TaskOrderFactory.create(portfolio=portfolio, contracting_officer=ko)
|
||||||
user_session(ko)
|
user_session(ko)
|
||||||
|
|
||||||
form_data = {
|
form_data = {
|
||||||
"start_date": "02/10/2019",
|
"start_date": "02/10/2019",
|
||||||
"end_date": "03/10/2019",
|
"end_date": "03/10/2019",
|
||||||
@ -310,7 +386,6 @@ def test_submit_completed_ko_review_page(client, user_session, pdf_upload):
|
|||||||
),
|
),
|
||||||
data=form_data,
|
data=form_data,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert task_order.pdf
|
assert task_order.pdf
|
||||||
assert response.headers["Location"] == url_for(
|
assert response.headers["Location"] == url_for(
|
||||||
"task_orders.signature_requested", task_order_id=task_order.id, _external=True
|
"task_orders.signature_requested", task_order_id=task_order.id, _external=True
|
||||||
|
@ -20,6 +20,7 @@ base_public:
|
|||||||
title_tag: JEDI Cloud
|
title_tag: JEDI Cloud
|
||||||
common:
|
common:
|
||||||
back: Back
|
back: Back
|
||||||
|
edit: Edit
|
||||||
manage: manage
|
manage: manage
|
||||||
save_and_continue: Save & Continue
|
save_and_continue: Save & Continue
|
||||||
sign: Sign
|
sign: Sign
|
||||||
|
Loading…
x
Reference in New Issue
Block a user