wip
This commit is contained in:
		| @@ -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) | ||||
|   | ||||
| @@ -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( | ||||
|   | ||||
| @@ -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, | ||||
|         ) | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -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() | ||||
|  | ||||
|   | ||||
| @@ -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": """ | ||||
|   | ||||
| @@ -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 %} | ||||
|   | ||||
| @@ -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()) | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user