diff --git a/atst/routes/portfolios/__init__.py b/atst/routes/portfolios/__init__.py index e851bdab..ca5f2dca 100644 --- a/atst/routes/portfolios/__init__.py +++ b/atst/routes/portfolios/__init__.py @@ -6,7 +6,6 @@ portfolios_bp = Blueprint("portfolios", __name__) from . import index from . import members from . import invitations -from . import task_orders from atst.domain.exceptions import UnauthorizedError from atst.domain.portfolios import Portfolios from atst.domain.authz import Authorization diff --git a/atst/routes/portfolios/invitations.py b/atst/routes/portfolios/invitations.py index a8870667..e02c4375 100644 --- a/atst/routes/portfolios/invitations.py +++ b/atst/routes/portfolios/invitations.py @@ -21,19 +21,10 @@ def send_invite_email(owner_name, token, new_member_email): def accept_invitation(token): invite = Invitations.accept(g.current_user, token) - # TODO: this will eventually redirect to different places depending on - # whether the user is an officer for the TO and what kind of officer they - # are. It will also have to manage cases like: - # - the logged-in user has multiple roles on the TO (e.g., KO and COR) - # - the logged-in user has officer roles on multiple unsigned TOs for task_order in invite.portfolio.task_orders: if g.current_user in task_order.officers: return redirect( - url_for( - "portfolios.view_task_order", - portfolio_id=task_order.portfolio_id, - task_order_id=task_order.id, - ) + url_for("task_orders.view_task_order", task_order_id=task_order.id) ) return redirect( diff --git a/atst/routes/portfolios/task_orders.py b/atst/routes/portfolios/task_orders.py deleted file mode 100644 index 35b3eb32..00000000 --- a/atst/routes/portfolios/task_orders.py +++ /dev/null @@ -1,321 +0,0 @@ -from collections import defaultdict - -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.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.utils.flash import formatted_flash as flash -from atst.services.invitation import ( - update_officer_invitations, - OFFICER_INVITATIONS, - Invitation as InvitationService, -) -from atst.domain.authz.decorator import user_can_access_decorator as user_can -from atst.models.permissions import Permissions - - -@portfolios_bp.route("/portfolios//task_orders") -@user_can(Permissions.VIEW_PORTFOLIO_FUNDING, message="view portfolio funding") -def portfolio_funding(portfolio_id): - portfolio = Portfolios.get(g.current_user, portfolio_id) - task_orders_by_status = defaultdict(list) - serialize_task_order = lambda task_order: { - key: getattr(task_order, key) - for key in [ - "id", - "budget", - "time_created", - "start_date", - "end_date", - "display_status", - "days_to_expiration", - "balance", - ] - } - - for task_order in portfolio.task_orders: - serialized_task_order = serialize_task_order(task_order) - serialized_task_order["url"] = url_for( - "portfolios.view_task_order", - portfolio_id=portfolio.id, - task_order_id=task_order.id, - ) - task_orders_by_status[task_order.status].append(serialized_task_order) - - active_task_orders = task_orders_by_status.get(TaskOrderStatus.ACTIVE, []) - total_balance = sum([task_order["balance"] for task_order in active_task_orders]) - - return render_template( - "portfolios/task_orders/index.html", - pending_task_orders=( - task_orders_by_status.get(TaskOrderStatus.STARTED, []) - + task_orders_by_status.get(TaskOrderStatus.PENDING, []) - ), - active_task_orders=active_task_orders, - expired_task_orders=task_orders_by_status.get(TaskOrderStatus.EXPIRED, []), - total_balance=total_balance, - ) - - -@portfolios_bp.route("/portfolios//task_order/") -@user_can(Permissions.VIEW_TASK_ORDER_DETAILS, message="view task order details") -def view_task_order(portfolio_id, task_order_id): - task_order = TaskOrders.get(task_order_id, portfolio_id=portfolio_id) - to_form_complete = TaskOrders.all_sections_complete(task_order) - 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), - task_order=task_order, - to_form_complete=to_form_complete, - user=g.current_user, - ) - - -def wrap_check_is_ko_or_cor(user, task_order_id=None, portfolio_id=None, **_kwargs): - task_order = TaskOrders.get(task_order_id, portfolio_id=portfolio_id) - Authorization.check_is_ko_or_cor(user, task_order) - - return True - - -@portfolios_bp.route("/portfolios//task_order//review") -@user_can( - None, - override=wrap_check_is_ko_or_cor, - message="view contracting officer review form", -) -def ko_review(portfolio_id, task_order_id): - task_order = TaskOrders.get(task_order_id, portfolio_id=portfolio_id) - - if TaskOrders.all_sections_complete(task_order): - return render_template( - "/portfolios/task_orders/review.html", - task_order=task_order, - form=KOReviewForm(obj=task_order), - ) - else: - raise NoAccessError("task_order") - - -@portfolios_bp.route( - "/portfolios//task_order//resend_invite", - methods=["POST"], -) -@user_can( - Permissions.EDIT_TASK_ORDER_DETAILS, message="resend task order officer invites" -) -def resend_invite(portfolio_id, task_order_id): - 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(task_order_id, portfolio_id=portfolio_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: - raise NotFoundError("invitation") - - if not invitation.can_resend: - raise NoAccessError("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//task_order//review", methods=["POST"] -) -@user_can( - None, override=wrap_check_is_ko_or_cor, message="submit contracting officer review" -) -def submit_ko_review(portfolio_id, task_order_id, form=None): - task_order = TaskOrders.get(task_order_id, portfolio_id=portfolio_id) - form_data = {**http_request.form, **http_request.files} - form = KOReviewForm(form_data) - - if form.validate(): - TaskOrders.update(task_order=task_order, **form.data) - if Authorization.is_ko(g.current_user, task_order) and TaskOrders.can_ko_sign( - 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", task_order=task_order, form=form - ) - - -@portfolios_bp.route( - "/portfolios//task_order//invitations" -) -@user_can( - Permissions.EDIT_TASK_ORDER_DETAILS, message="view task order invitations page" -) -def task_order_invitations(portfolio_id, task_order_id): - task_order = TaskOrders.get(task_order_id, portfolio_id=portfolio_id) - form = EditTaskOrderOfficersForm(obj=task_order) - - if TaskOrders.all_sections_complete(task_order): - return render_template( - "portfolios/task_orders/invitations.html", - task_order=task_order, - form=form, - user=g.current_user, - ) - else: - raise NotFoundError("task_order") - - -@portfolios_bp.route( - "/portfolios//task_order//invitations", - methods=["POST"], -) -@user_can(Permissions.EDIT_TASK_ORDER_DETAILS, message="edit task order invitations") -def edit_task_order_invitations(portfolio_id, task_order_id): - task_order = TaskOrders.get(task_order_id, portfolio_id=portfolio_id) - form = EditTaskOrderOfficersForm(formdata=http_request.form, obj=task_order) - - if form.validate(): - form.populate_obj(task_order) - db.session.add(task_order) - db.session.commit() - update_officer_invitations(g.current_user, task_order) - - return redirect( - url_for( - "portfolios.task_order_invitations", - portfolio_id=portfolio_id, - task_order_id=task_order.id, - ) - ) - else: - return ( - render_template( - "portfolios/task_orders/invitations.html", - task_order=task_order, - form=form, - ), - 400, - ) - - -def so_review_form(task_order): - if task_order.dd_254: - dd_254 = task_order.dd_254 - form = DD254Form(obj=dd_254) - form.required_distribution.data = dd_254.required_distribution - return form - else: - so = task_order.officer_dictionary("security_officer") - form_data = { - "certifying_official": "{}, {}".format( - so.get("last_name", ""), so.get("first_name", "") - ), - "co_phone": so.get("phone_number", ""), - } - return DD254Form(data=form_data) - - -def wrap_check_is_so(user, task_order_id=None, portfolio_id=None, **_kwargs): - task_order = TaskOrders.get(task_order_id, portfolio_id=portfolio_id) - Authorization.check_is_so(user, task_order) - - return True - - -@portfolios_bp.route("/portfolios//task_order//dd254") -@user_can(None, override=wrap_check_is_so, message="view security officer review form") -def so_review(portfolio_id, task_order_id): - task_order = TaskOrders.get(task_order_id, portfolio_id=portfolio_id) - form = so_review_form(task_order) - - return render_template( - "portfolios/task_orders/so_review.html", form=form, task_order=task_order - ) - - -@portfolios_bp.route( - "/portfolios//task_order//dd254", methods=["POST"] -) -@user_can( - None, override=wrap_check_is_so, message="submit security officer review form" -) -def submit_so_review(portfolio_id, task_order_id): - task_order = TaskOrders.get(task_order_id, portfolio_id=portfolio_id) - form = DD254Form(http_request.form) - - if form.validate(): - TaskOrders.add_dd_254(task_order, form.data) - # TODO: will redirect to download, sign, upload page - return redirect( - url_for( - "portfolios.view_task_order", - portfolio_id=task_order.portfolio.id, - task_order_id=task_order.id, - ) - ) - else: - return render_template( - "portfolios/task_orders/so_review.html", form=form, task_order=task_order - ) diff --git a/atst/routes/task_orders/__init__.py b/atst/routes/task_orders/__init__.py index e09d7c91..ef5a1e74 100644 --- a/atst/routes/task_orders/__init__.py +++ b/atst/routes/task_orders/__init__.py @@ -2,7 +2,12 @@ from flask import Blueprint task_orders_bp = Blueprint("task_orders", __name__) -from . import new from . import index -from . import invite +from . import new +from . import invitations +from . import officer_reviews from . import signing +from . import downloads +from atst.utils.context_processors import portfolio as portfolio_context_processor + +task_orders_bp.context_processor(portfolio_context_processor) diff --git a/atst/routes/task_orders/downloads.py b/atst/routes/task_orders/downloads.py new file mode 100644 index 00000000..c23f5250 --- /dev/null +++ b/atst/routes/task_orders/downloads.py @@ -0,0 +1,56 @@ +from io import BytesIO +from flask import Response, current_app as app + +from . import task_orders_bp +from atst.domain.task_orders import TaskOrders +from atst.domain.exceptions import NotFoundError +from atst.utils.docx import Docx +from atst.domain.authz.decorator import user_can_access_decorator as user_can +from atst.models.permissions import Permissions + + +@task_orders_bp.route("/task_orders//download_summary") +@user_can(Permissions.VIEW_TASK_ORDER_DETAILS, message="download task order summary") +def download_summary(task_order_id): + task_order = TaskOrders.get(task_order_id) + byte_str = BytesIO() + Docx.render(byte_str, data=task_order.to_dictionary()) + filename = "{}.docx".format(task_order.portfolio_name) + return Response( + byte_str, + headers={"Content-Disposition": "attachment; filename={}".format(filename)}, + mimetype="application/vnd.openxmlformats-officedocument.wordprocessingml.document", + ) + + +def send_file(attachment): + generator = app.csp.files.download(attachment.object_name) + return Response( + generator, + headers={ + "Content-Disposition": "attachment; filename={}".format(attachment.filename) + }, + ) + + +@task_orders_bp.route("/task_orders//csp_estimate") +@user_can( + Permissions.VIEW_TASK_ORDER_DETAILS, + message="download task order cloud service provider estimate", +) +def download_csp_estimate(task_order_id): + task_order = TaskOrders.get(task_order_id) + if task_order.csp_estimate: + return send_file(task_order.csp_estimate) + else: + raise NotFoundError("task_order CSP estimate") + + +@task_orders_bp.route("/task_orders//pdf") +@user_can(Permissions.VIEW_TASK_ORDER_DETAILS, message="download task order PDF") +def download_task_order_pdf(task_order_id): + task_order = TaskOrders.get(task_order_id) + if task_order.pdf: + return send_file(task_order.pdf) + else: + raise NotFoundError("task_order pdf") diff --git a/atst/routes/task_orders/index.py b/atst/routes/task_orders/index.py index 1f775cc8..1f749b24 100644 --- a/atst/routes/task_orders/index.py +++ b/atst/routes/task_orders/index.py @@ -1,56 +1,74 @@ -from io import BytesIO -from flask import Response, current_app as app +from collections import defaultdict + +from flask import g, render_template, url_for from . import task_orders_bp -from atst.domain.task_orders import TaskOrders -from atst.domain.exceptions import NotFoundError -from atst.utils.docx import Docx +from atst.domain.authz import Authorization from atst.domain.authz.decorator import user_can_access_decorator as user_can -from atst.models.permissions import Permissions +from atst.domain.portfolios import Portfolios +from atst.domain.task_orders import TaskOrders, DD254s +from atst.models import Permissions +from atst.models.task_order import Status as TaskOrderStatus -@task_orders_bp.route("/task_orders/download_summary/") -@user_can(Permissions.VIEW_TASK_ORDER_DETAILS, message="download task order summary") -def download_summary(task_order_id): +@task_orders_bp.route("/task_orders/") +@user_can(Permissions.VIEW_TASK_ORDER_DETAILS, message="view task order details") +def view_task_order(task_order_id): task_order = TaskOrders.get(task_order_id) - byte_str = BytesIO() - Docx.render(byte_str, data=task_order.to_dictionary()) - filename = "{}.docx".format(task_order.portfolio_name) - return Response( - byte_str, - headers={"Content-Disposition": "attachment; filename={}".format(filename)}, - mimetype="application/vnd.openxmlformats-officedocument.wordprocessingml.document", + to_form_complete = TaskOrders.all_sections_complete(task_order) + 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), + task_order=task_order, + to_form_complete=to_form_complete, + user=g.current_user, ) -def send_file(attachment): - generator = app.csp.files.download(attachment.object_name) - return Response( - generator, - headers={ - "Content-Disposition": "attachment; filename={}".format(attachment.filename) - }, +def serialize_task_order(task_order): + return { + key: getattr(task_order, key) + for key in [ + "id", + "budget", + "time_created", + "start_date", + "end_date", + "display_status", + "days_to_expiration", + "balance", + ] + } + + +@task_orders_bp.route("/portfolios//task_orders") +@user_can(Permissions.VIEW_PORTFOLIO_FUNDING, message="view portfolio funding") +def portfolio_funding(portfolio_id): + portfolio = Portfolios.get(g.current_user, portfolio_id) + task_orders_by_status = defaultdict(list) + + for task_order in portfolio.task_orders: + serialized_task_order = serialize_task_order(task_order) + serialized_task_order["url"] = url_for( + "task_orders.view_task_order", task_order_id=task_order.id + ) + task_orders_by_status[task_order.status].append(serialized_task_order) + + active_task_orders = task_orders_by_status.get(TaskOrderStatus.ACTIVE, []) + total_balance = sum([task_order["balance"] for task_order in active_task_orders]) + + return render_template( + "portfolios/task_orders/index.html", + pending_task_orders=( + task_orders_by_status.get(TaskOrderStatus.STARTED, []) + + task_orders_by_status.get(TaskOrderStatus.PENDING, []) + ), + active_task_orders=active_task_orders, + expired_task_orders=task_orders_by_status.get(TaskOrderStatus.EXPIRED, []), + total_balance=total_balance, ) - - -@task_orders_bp.route("/task_orders/csp_estimate/") -@user_can( - Permissions.VIEW_TASK_ORDER_DETAILS, - message="download task order cloud service provider estimate", -) -def download_csp_estimate(task_order_id): - task_order = TaskOrders.get(task_order_id) - if task_order.csp_estimate: - return send_file(task_order.csp_estimate) - else: - raise NotFoundError("task_order CSP estimate") - - -@task_orders_bp.route("/task_orders/pdf/") -@user_can(Permissions.VIEW_TASK_ORDER_DETAILS, message="download task order PDF") -def download_task_order_pdf(task_order_id): - task_order = TaskOrders.get(task_order_id) - if task_order.pdf: - return send_file(task_order.pdf) - else: - raise NotFoundError("task_order pdf") diff --git a/atst/routes/task_orders/invitations.py b/atst/routes/task_orders/invitations.py new file mode 100644 index 00000000..024c0097 --- /dev/null +++ b/atst/routes/task_orders/invitations.py @@ -0,0 +1,132 @@ +from flask import g, redirect, render_template, url_for, request as http_request + +from . import task_orders_bp +from atst.domain.task_orders import TaskOrders +from atst.utils.flash import formatted_flash as flash +from atst.domain.authz.decorator import user_can_access_decorator as user_can +from atst.models.permissions import Permissions +from atst.database import db +from atst.domain.exceptions import NotFoundError, NoAccessError +from atst.domain.invitations import Invitations +from atst.domain.portfolios import Portfolios +from atst.utils.localization import translate +from atst.forms.officers import EditTaskOrderOfficersForm +from atst.services.invitation import ( + update_officer_invitations, + OFFICER_INVITATIONS, + Invitation as InvitationService, +) + + +@task_orders_bp.route("/task_orders//invite", methods=["POST"]) +@user_can(Permissions.EDIT_TASK_ORDER_DETAILS, message="invite task order officers") +def invite(task_order_id): + task_order = TaskOrders.get(task_order_id) + if TaskOrders.all_sections_complete(task_order): + update_officer_invitations(g.current_user, task_order) + + portfolio = task_order.portfolio + flash("task_order_congrats", portfolio=portfolio) + return redirect( + url_for("task_orders.view_task_order", task_order_id=task_order.id) + ) + else: + flash("task_order_incomplete") + return redirect( + url_for("task_orders.new", screen=4, task_order_id=task_order.id) + ) + + +@task_orders_bp.route("/task_orders//resend_invite", methods=["POST"]) +@user_can( + Permissions.EDIT_TASK_ORDER_DETAILS, message="resend task order officer invites" +) +def resend_invite(task_order_id): + 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(task_order_id) + portfolio = Portfolios.get(g.current_user, task_order.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: + raise NotFoundError("invitation") + + if not invitation.can_resend: + raise NoAccessError("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("task_orders.invitations", task_order_id=task_order_id)) + + +@task_orders_bp.route("/task_orders//invitations") +@user_can( + Permissions.EDIT_TASK_ORDER_DETAILS, message="view task order invitations page" +) +def invitations(task_order_id): + task_order = TaskOrders.get(task_order_id) + form = EditTaskOrderOfficersForm(obj=task_order) + + if TaskOrders.all_sections_complete(task_order): + return render_template( + "portfolios/task_orders/invitations.html", + task_order=task_order, + form=form, + user=g.current_user, + ) + else: + raise NotFoundError("task_order") + + +@task_orders_bp.route("/task_orders//invitations/edit", methods=["POST"]) +@user_can(Permissions.EDIT_TASK_ORDER_DETAILS, message="edit task order invitations") +def invitations_edit(task_order_id): + task_order = TaskOrders.get(task_order_id) + form = EditTaskOrderOfficersForm(formdata=http_request.form, obj=task_order) + + if form.validate(): + form.populate_obj(task_order) + db.session.add(task_order) + db.session.commit() + update_officer_invitations(g.current_user, task_order) + + return redirect(url_for("task_orders.invitations", task_order_id=task_order.id)) + else: + return ( + render_template( + "portfolios/task_orders/invitations.html", + task_order=task_order, + form=form, + ), + 400, + ) diff --git a/atst/routes/task_orders/invite.py b/atst/routes/task_orders/invite.py deleted file mode 100644 index 4021adda..00000000 --- a/atst/routes/task_orders/invite.py +++ /dev/null @@ -1,31 +0,0 @@ -from flask import redirect, url_for, g - -from . import task_orders_bp -from atst.domain.task_orders import TaskOrders -from atst.utils.flash import formatted_flash as flash -from atst.services.invitation import update_officer_invitations -from atst.domain.authz.decorator import user_can_access_decorator as user_can -from atst.models.permissions import Permissions - - -@task_orders_bp.route("/task_orders/invite/", methods=["POST"]) -@user_can(Permissions.EDIT_TASK_ORDER_DETAILS, message="invite task order officers") -def invite(task_order_id): - task_order = TaskOrders.get(task_order_id) - if TaskOrders.all_sections_complete(task_order): - update_officer_invitations(g.current_user, task_order) - - portfolio = task_order.portfolio - flash("task_order_congrats", portfolio=portfolio) - return redirect( - url_for( - "portfolios.view_task_order", - portfolio_id=task_order.portfolio_id, - task_order_id=task_order.id, - ) - ) - else: - flash("task_order_incomplete") - return redirect( - url_for("task_orders.new", screen=4, task_order_id=task_order.id) - ) diff --git a/atst/routes/task_orders/new.py b/atst/routes/task_orders/new.py index 2854e966..9002f8e5 100644 --- a/atst/routes/task_orders/new.py +++ b/atst/routes/task_orders/new.py @@ -305,9 +305,7 @@ def new(screen, task_order_id=None, portfolio_id=None): if http_request.args.get("ko_edit"): template_args["ko_edit"] = True template_args["next"] = url_for( - "portfolios.ko_review", - portfolio_id=workflow.task_order.portfolio.id, - task_order_id=task_order_id, + "task_orders.ko_review", task_order_id=task_order_id ) url_args["next"] = template_args["next"] diff --git a/atst/routes/task_orders/officer_reviews.py b/atst/routes/task_orders/officer_reviews.py new file mode 100644 index 00000000..aaaf8c68 --- /dev/null +++ b/atst/routes/task_orders/officer_reviews.py @@ -0,0 +1,117 @@ +from flask import g, redirect, render_template, url_for, request as http_request + +from . import task_orders_bp +from atst.domain.authz import Authorization +from atst.domain.exceptions import NoAccessError +from atst.domain.task_orders import TaskOrders +from atst.forms.dd_254 import DD254Form +from atst.forms.ko_review import KOReviewForm +from atst.domain.authz.decorator import user_can_access_decorator as user_can + + +def wrap_check_is_ko_or_cor(user, task_order_id=None, **_kwargs): + task_order = TaskOrders.get(task_order_id) + Authorization.check_is_ko_or_cor(user, task_order) + + return True + + +@task_orders_bp.route("/task_orders//review") +@user_can( + None, + override=wrap_check_is_ko_or_cor, + message="view contracting officer review form", +) +def ko_review(task_order_id): + task_order = TaskOrders.get(task_order_id) + + if TaskOrders.all_sections_complete(task_order): + return render_template( + "/portfolios/task_orders/review.html", + task_order=task_order, + form=KOReviewForm(obj=task_order), + ) + else: + raise NoAccessError("task_order") + + +@task_orders_bp.route("/task_orders//review", methods=["POST"]) +@user_can( + None, override=wrap_check_is_ko_or_cor, message="submit contracting officer review" +) +def submit_ko_review(task_order_id, form=None): + task_order = TaskOrders.get(task_order_id) + form_data = {**http_request.form, **http_request.files} + form = KOReviewForm(form_data) + + if form.validate(): + TaskOrders.update(task_order=task_order, **form.data) + if Authorization.is_ko(g.current_user, task_order) and TaskOrders.can_ko_sign( + task_order + ): + return redirect( + url_for("task_orders.signature_requested", task_order_id=task_order_id) + ) + else: + return redirect( + url_for("task_orders.view_task_order", task_order_id=task_order_id) + ) + else: + return render_template( + "/portfolios/task_orders/review.html", task_order=task_order, form=form + ) + + +def so_review_form(task_order): + if task_order.dd_254: + dd_254 = task_order.dd_254 + form = DD254Form(obj=dd_254) + form.required_distribution.data = dd_254.required_distribution + return form + else: + so = task_order.officer_dictionary("security_officer") + form_data = { + "certifying_official": "{}, {}".format( + so.get("last_name", ""), so.get("first_name", "") + ), + "co_phone": so.get("phone_number", ""), + } + return DD254Form(data=form_data) + + +def wrap_check_is_so(user, task_order_id=None, **_kwargs): + task_order = TaskOrders.get(task_order_id) + Authorization.check_is_so(user, task_order) + + return True + + +@task_orders_bp.route("/task_orders//dd254") +@user_can(None, override=wrap_check_is_so, message="view security officer review form") +def so_review(task_order_id): + task_order = TaskOrders.get(task_order_id) + form = so_review_form(task_order) + + return render_template( + "portfolios/task_orders/so_review.html", form=form, task_order=task_order + ) + + +@task_orders_bp.route("/task_orders//dd254", methods=["POST"]) +@user_can( + None, override=wrap_check_is_so, message="submit security officer review form" +) +def submit_so_review(task_order_id): + task_order = TaskOrders.get(task_order_id) + form = DD254Form(http_request.form) + + if form.validate(): + TaskOrders.add_dd_254(task_order, form.data) + # TODO: will redirect to download, sign, upload page + return redirect( + url_for("task_orders.view_task_order", task_order_id=task_order.id) + ) + else: + return render_template( + "portfolios/task_orders/so_review.html", form=form, task_order=task_order + ) diff --git a/atst/routes/task_orders/signing.py b/atst/routes/task_orders/signing.py index 5f40bea8..3b3a92e0 100644 --- a/atst/routes/task_orders/signing.py +++ b/atst/routes/task_orders/signing.py @@ -72,11 +72,7 @@ def record_signature(task_order_id): flash("task_order_signed") return redirect( - url_for( - "portfolios.view_task_order", - portfolio_id=task_order.portfolio_id, - task_order_id=task_order.id, - ) + url_for("task_orders.view_task_order", task_order_id=task_order.id) ) else: return ( diff --git a/templates/portfolios/header.html b/templates/portfolios/header.html index c62933b5..3675cb58 100644 --- a/templates/portfolios/header.html +++ b/templates/portfolios/header.html @@ -67,8 +67,8 @@ {{ Link( icon='dollar-sign', text='navigation.portfolio_navigation.breadcrumbs.funding' | translate, - url=url_for("portfolios.portfolio_funding", portfolio_id=portfolio.id), - active=request.url_rule.endpoint == "portfolios.portfolio_funding", + url=url_for("task_orders.portfolio_funding", portfolio_id=portfolio.id), + active=request.url_rule.endpoint == "task_orders.portfolio_funding", ) }} {% if user_can(permissions.VIEW_PORTFOLIO_ADMIN) %} {{ Link( diff --git a/templates/portfolios/reports/index.html b/templates/portfolios/reports/index.html index 49095331..dc45af28 100644 --- a/templates/portfolios/reports/index.html +++ b/templates/portfolios/reports/index.html @@ -76,7 +76,7 @@ - {% endif %} - + {{ Icon('cog') }} Manage Task Order diff --git a/templates/portfolios/task_orders/index.html b/templates/portfolios/task_orders/index.html index 1d92eca2..4fa70009 100644 --- a/templates/portfolios/task_orders/index.html +++ b/templates/portfolios/task_orders/index.html @@ -8,7 +8,7 @@ {% block portfolio_content %} {% macro ViewLink(task_order) %} - + View {{ Icon("caret_right", classes="icon--tiny") }} diff --git a/templates/portfolios/task_orders/invitations.html b/templates/portfolios/task_orders/invitations.html index e7b0b75a..7474097d 100644 --- a/templates/portfolios/task_orders/invitations.html +++ b/templates/portfolios/task_orders/invitations.html @@ -17,7 +17,7 @@ {% endmacro %} {% macro EditOfficerInfo(form, officer_type, invited) -%} -
+ {{ form.csrf_token }}