diff --git a/atst/domain/authz.py b/atst/domain/authz.py index f2bcc0de..7075a48b 100644 --- a/atst/domain/authz.py +++ b/atst/domain/authz.py @@ -36,6 +36,27 @@ class Authorization(object): def is_ccpo(cls, user): 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 def check_is_ko(cls, user, task_order): if task_order.contracting_officer != user: diff --git a/atst/domain/task_orders.py b/atst/domain/task_orders.py index 672eb749..00dd0218 100644 --- a/atst/domain/task_orders.py +++ b/atst/domain/task_orders.py @@ -120,6 +120,10 @@ class TaskOrders(object): return True + @classmethod + def is_signed_by_ko(cls, task_order): + return task_order.signer_dod_id is not None + @classmethod def mission_owner_sections(cls): section_list = TaskOrders.SECTIONS diff --git a/atst/routes/portfolios/task_orders.py b/atst/routes/portfolios/task_orders.py index 7fa280c9..27a8fdaa 100644 --- a/atst/routes/portfolios/task_orders.py +++ b/atst/routes/portfolios/task_orders.py @@ -65,10 +65,14 @@ def view_task_order(portfolio_id, task_order_id): dd_254_complete = DD254s.is_complete(task_order.dd_254) return render_template( "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, task_order=task_order, to_form_complete=to_form_complete, - dd_254_complete=dd_254_complete, 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) 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( "/portfolios/task_orders/review.html", 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 = 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(): TaskOrders.update(user=g.current_user, task_order=task_order, **form.data) - return redirect( - url_for("task_orders.signature_requested", task_order_id=task_order_id) - ) + if Authorization.is_ko(g.current_user, task_order): + return redirect( + 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: return render_template( "/portfolios/task_orders/review.html", diff --git a/atst/routes/task_orders/signing.py b/atst/routes/task_orders/signing.py index e6251de3..548d8fa3 100644 --- a/atst/routes/task_orders/signing.py +++ b/atst/routes/task_orders/signing.py @@ -14,7 +14,7 @@ def find_unsigned_ko_to(task_order_id): task_order = TaskOrders.get(g.current_user, task_order_id) 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") return task_order diff --git a/templates/portfolios/task_orders/show.html b/templates/portfolios/task_orders/show.html index 105ed2c7..c5468a24 100644 --- a/templates/portfolios/task_orders/show.html +++ b/templates/portfolios/task_orders/show.html @@ -124,27 +124,37 @@

{{ "task_orders.view.whats_next" | translate }}

- {% call Step( - description="task_orders.view.steps.draft" | translate({ - "contact": officer_name(task_order.creator) - })| safe, - button_url=url_for("task_orders.new", screen=1, task_order_id=task_order.id), - button_text='Edit', - complete=to_form_complete) %} - {% endcall %} - {% set is_so = user == task_order.security_officer %} - {{ Step( - description="task_orders.view.steps.security" | translate({ - "security_officer": officer_name(task_order.security_officer) - }) | 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) }} + {{ + 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({ + "contact": officer_name(task_order.creator) + })| safe, + ) + }} + {{ + Step( + button_text=is_so and ("common.edit" | translate), + 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({ + "security_officer": officer_name(task_order.security_officer) + }) | safe, + ) + }} {% call Step( description="task_orders.view.steps.record" | translate({ "contracting_officer": officer_name(task_order.contracting_officer), "contracting_officer_representative": officer_name(task_order.contracting_officer_representative) }) | 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) %}
@@ -154,14 +164,20 @@
{% endcall %} - {% set is_ko = user == task_order.contracting_officer %} - {{ Step( - description="task_orders.view.steps.sign" | translate({ - "contracting_officer": officer_name(task_order.contracting_officer) - }) | 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) }} + {{ + 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({ + "contracting_officer": officer_name(task_order.contracting_officer) + }) | safe, + ) + }}
diff --git a/tests/domain/test_authz.py b/tests/domain/test_authz.py new file mode 100644 index 00000000..a92fe2f1 --- /dev/null +++ b/tests/domain/test_authz.py @@ -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) diff --git a/tests/domain/test_task_orders.py b/tests/domain/test_task_orders.py index 401d2db9..2ea1a569 100644 --- a/tests/domain/test_task_orders.py +++ b/tests/domain/test_task_orders.py @@ -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(): dict_keys = [k for k in TaskOrders.SECTIONS.keys()] section = dict_keys[0] diff --git a/tests/routes/portfolios/test_task_orders.py b/tests/routes/portfolios/test_task_orders.py index c6ecc97d..f6bf3c2c 100644 --- a/tests/routes/portfolios/test_task_orders.py +++ b/tests/routes/portfolios/test_task_orders.py @@ -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): portfolio = PortfolioFactory.create() ko = UserFactory.create() + cor = UserFactory.create() + PortfolioRoleFactory.create( role=Roles.get("officer"), portfolio=portfolio, user=ko, status=PortfolioStatus.ACTIVE, ) - task_order = TaskOrderFactory.create(portfolio=portfolio, contracting_officer=ko) - user_session(ko) - response = client.get( - url_for( - "portfolios.ko_review", - portfolio_id=portfolio.id, - task_order_id=task_order.id, - ) + 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) + response = client.get(request_url) 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): portfolio = PortfolioFactory.create() @@ -282,17 +309,66 @@ def test_cor_redirected_to_build_page(client, user_session): 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() + + 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() + PortfolioRoleFactory.create( role=Roles.get("officer"), portfolio=portfolio, user=ko, status=PortfolioStatus.ACTIVE, ) + task_order = TaskOrderFactory.create(portfolio=portfolio, contracting_officer=ko) user_session(ko) + form_data = { "start_date": "02/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, ) - assert task_order.pdf assert response.headers["Location"] == url_for( "task_orders.signature_requested", task_order_id=task_order.id, _external=True diff --git a/translations.yaml b/translations.yaml index 5b2ba8f2..57aec61a 100644 --- a/translations.yaml +++ b/translations.yaml @@ -20,6 +20,7 @@ base_public: title_tag: JEDI Cloud common: back: Back + edit: Edit manage: manage save_and_continue: Save & Continue sign: Sign