From 038cceb34b11911bb7e35a5daa20898365b51cf7 Mon Sep 17 00:00:00 2001 From: dandds Date: Tue, 4 Sep 2018 17:28:16 -0400 Subject: [PATCH 01/11] ccpo should see request approval screen for all request --- atst/routes/requests/approval.py | 10 +++++--- atst/routes/requests/index.py | 4 ++- templates/requests/approval.html | 39 +---------------------------- tests/routes/test_requests_index.py | 19 ++++++++++++++ 4 files changed, 29 insertions(+), 43 deletions(-) diff --git a/atst/routes/requests/approval.py b/atst/routes/requests/approval.py index 8d16960a..ca4116c7 100644 --- a/atst/routes/requests/approval.py +++ b/atst/routes/requests/approval.py @@ -1,9 +1,11 @@ -from flask import render_template +from flask import render_template, g from . import requests_bp from atst.forms.data import SERVICE_BRANCHES +from atst.domain.requests import Requests -@requests_bp.route("/request_approval", methods=["GET"]) -def requests_approval(): - return render_template("request_approval.html", service_branches=SERVICE_BRANCHES) +@requests_bp.route("/requests/approval/", methods=["GET"]) +def approval(request_id): + request = Requests.get(g.current_user, request_id) + return render_template("requests/approval.html", data=request.body, service_branches=SERVICE_BRANCHES) diff --git a/atst/routes/requests/index.py b/atst/routes/requests/index.py index fe3e894e..75be2c87 100644 --- a/atst/routes/requests/index.py +++ b/atst/routes/requests/index.py @@ -70,7 +70,9 @@ class RequestsIndex(object): else "-" ) - if Requests.is_pending_financial_verification(request): + if viewing_role == "ccpo": + edit_link = url_for("requests.approval", request_id=request.id) + elif Requests.is_pending_financial_verification(request): edit_link = url_for( "requests.financial_verification", request_id=request.id ) diff --git a/templates/requests/approval.html b/templates/requests/approval.html index 8cb2580a..6f9b8dd8 100644 --- a/templates/requests/approval.html +++ b/templates/requests/approval.html @@ -18,44 +18,7 @@

Ongoing maintainence for Death Star (a moon-sized Imperial military battlestation armed with a planet-destroying superlaser). Its definitely hasn't been sabotaged from the start.

- {% with data = { - "primary_poc": { - "am_poc": False, - "dodid_poc": "1234567890", - "email_poc": "fake@email.com", - "fname_poc": "Amanda", - "lname_poc": "Adamson", - }, - "details_of_use": { - "jedi_usage": "adf", - "start_date": "2018-08-08", - "cloud_native": "yes", - "dollar_value": 500000, - "dod_component": "Air Force, Department of the", - "data_transfers": "Less than 100GB", - "expected_completion_date": "Less than 1 month", - "jedi_migration": "yes", - "num_software_systems": 1, - "number_user_sessions": 2, - "average_daily_traffic": 1, - "engineering_assessment": "yes", - "technical_support_team": "yes", - "estimated_monthly_spend": 100, - "average_daily_traffic_gb": 4, - "rationalization_software_systems": "yes", - "organization_providing_assistance": "In-house staff", - }, - "information_about_you": { - "citizenship": "United States", - "designation": "military", - "phone_number": "1234567890", - "email_request": "fake@email.mil", - "fname_request": "Amanda", - "lname_request": "Adamson", - "service_branch": "Air Force, Department of the", - "date_latest_training": "2018-08-06", - } - }, service_branches=service_branches %} + {% with data=data, service_branches=service_branches %} {% include "requests/_review.html" %} {% endwith %} diff --git a/tests/routes/test_requests_index.py b/tests/routes/test_requests_index.py index e33c7cb8..c8d6fa5d 100644 --- a/tests/routes/test_requests_index.py +++ b/tests/routes/test_requests_index.py @@ -1,3 +1,5 @@ +from flask import url_for + from atst.routes.requests.index import RequestsIndex from tests.factories import RequestFactory, UserFactory from atst.domain.requests import Requests @@ -21,3 +23,20 @@ def test_action_required_ccpo(): context = RequestsIndex(ccpo).execute() assert context["num_action_required"] == 1 + + +def test_ccpo_sees_approval_screen(): + ccpo = UserFactory.from_atat_role("ccpo") + request = RequestFactory.create() + Requests.submit(request) + ccpo_context = RequestsIndex(ccpo).execute() + assert ( + ccpo_context["requests"][0]["edit_link"] + == url_for("requests.approval", request_id=request.id) + ) + + mo_context = RequestsIndex(request.creator).execute() + assert ( + mo_context["requests"][0]["edit_link"] + != url_for("requests.approval", request_id=request.id) + ) From 50c8766a7abf50ca5f6e75eba49347232c05cb3b Mon Sep 17 00:00:00 2001 From: dandds Date: Wed, 5 Sep 2018 15:41:51 -0400 Subject: [PATCH 02/11] use macro for request review fields --- atst/filters.py | 10 ++ templates/requests/_review.html | 240 ++++++-------------------------- 2 files changed, 55 insertions(+), 195 deletions(-) diff --git a/atst/filters.py b/atst/filters.py index 6ecd6d1f..e889b300 100644 --- a/atst/filters.py +++ b/atst/filters.py @@ -50,6 +50,15 @@ def mixedContentToJson(value): return app.jinja_env.filters["tojson"](value) +def findFilter(value, filter_name, filter_args=[]): + if not filter_name: + return value + elif filter_name in app.jinja_env.filters: + return app.jinja_env.filters[filter_name](value, *filter_args) + else: + raise ValueError("filter name {} not found".format(filter_name)) + + def register_filters(app): app.jinja_env.filters["iconSvg"] = iconSvg app.jinja_env.filters["dollars"] = dollars @@ -57,3 +66,4 @@ def register_filters(app): app.jinja_env.filters["readableInteger"] = readableInteger app.jinja_env.filters["getOptionLabel"] = getOptionLabel app.jinja_env.filters["mixedContentToJson"] = mixedContentToJson + app.jinja_env.filters["findFilter"] = findFilter diff --git a/templates/requests/_review.html b/templates/requests/_review.html index 8d21d7f2..576f07fc 100644 --- a/templates/requests/_review.html +++ b/templates/requests/_review.html @@ -2,6 +2,20 @@ Response Required {%- endmacro %} +{% macro DefinitionReviewField(title, section, item_name, filter=None, filter_args=[]) -%} +
+
{{ title }}
+
+ {% if data[section] and data[section][item_name] %} + {{ data[section][item_name] | findFilter(filter, filter_args) }} + {% else %} + {{ RequiredLabel() }} + {% endif %} +
+
+{% endmacro %} + +

Details of Use @@ -14,161 +28,51 @@

-
-
DoD Component
-
- {% if data['details_of_use']['dod_component'] %} - {{ data['details_of_use']['dod_component'] | getOptionLabel(service_branches) }} - {% else %} - {{ RequiredLabel() }} - {% endif %} -
-
-
-
JEDI Usage
-
{{ data['details_of_use']['jedi_usage'] or RequiredLabel() }}
-
+ {{ DefinitionReviewField("DoD Component", "details_of_use", "dod_component", filter="getOptionLabel", filter_args=[service_branches]) }} -
-
Number of software systems
-
- {% if data['details_of_use']['num_software_systems'] %} - {{ data['details_of_use']['num_software_systems'] | readableInteger }} - {% else %} - {{ RequiredLabel() }} - {% endif %} -
-
+ {{ DefinitionReviewField("JEDI Usage", "details_of_use", "jedi_usage") }} -
-
JEDI Migration
-
{{ data['details_of_use']['jedi_migration'] or RequiredLabel() }}
-
+ {{ DefinitionReviewField("Number of software systems", "details_of_use", "num_software_systems", filter="readableInteger") }} + + {{ DefinitionReviewField("JEDI Migration", "details_of_use", "jedi_migration") }} {% if data['details_of_use']['jedi_migration'] == 'yes' %} -
-
Rationalization of Software Systems
-
{{ data['details_of_use']['rationalization_software_systems'] or RequiredLabel() }}
-
- -
-
Technical Support Team
-
{{ data['details_of_use']['technical_support_team'] or RequiredLabel() }}
-
+ {{ DefinitionReviewField("Rationalization of Software Systems", "details_of_use", "rationalization_software_systems") }} + {{ DefinitionReviewField("Technical Support Team", "details_of_use", "technical_support_team") }} {% if data['details_of_use']['technical_support_team'] == 'yes' %} -
-
Organization Providing Assistance
-
- {% if data['details_of_use']['organization_providing_assistance'] %} - {{ data['details_of_use']['organization_providing_assistance'] | getOptionLabel(assistance_org_types) }} - {% else %} - {{ RequiredLabel() }} - {% endif %} -
-
+ {{ DefinitionReviewField("Organization Providing Assistance", "details_of_use", "organization_providing_assistance", filter="getOptionLabel", filter_args=[assistance_org_types]) }} {% endif %} -
-
Engineering Assessment
-
{{ data['details_of_use']['engineering_assessment'] or RequiredLabel() }}
-
+ {{ DefinitionReviewField("Engineering Assessment", "details_of_use", "engineering_assessment") }} -
-
Data Transfers
-
- {% if data['details_of_use']['data_transfers'] %} - {{ data['details_of_use']['data_transfers'] | getOptionLabel(data_transfer_amounts) }} - {% else %} - {{ RequiredLabel() }} - {% endif %} -
-
+ {{ DefinitionReviewField("Data Transfers", "details_of_use", "data_transfers", filter="getOptionLabel", filter_args=[data_transfer_amounts]) }} -
-
Expected Completion Date
-
- {% if data['details_of_use']['expected_completion_date'] %} - {{ data['details_of_use']['expected_completion_date'] | getOptionLabel(completion_date_ranges) }} - {% else %} - {{ RequiredLabel() }} - {% endif %} -
-
+ {{ DefinitionReviewField("Expected Completion Date", "details_of_use", "expected_completion_date", filter="getOptionLabel", filter_args=[completion_date_ranges]) }} {% else %} -
-
Cloud Native
-
{{ data['details_of_use']['cloud_native'] or RequiredLabel() }}
-
+ {{ DefinitionReviewField("Cloud Native", "details_of_use", "cloud_native") }} {% endif %} -
-
Estimated Monthly Spend
-
- {% if data['details_of_use']['estimated_monthly_spend'] %} - {{ data['details_of_use']['estimated_monthly_spend'] | dollars }} - {% else %} - {{ RequiredLabel() }} - {% endif %} -
-
+ {{ DefinitionReviewField("Estimated Monthly Spend", "details_of_use", "estimated_monthly_spend", filter="dollars") }} {% if jedi_request and jedi_request.annual_spend > annual_spend_threshold %} -
-
Number of User Sessions
-
- {% if data['details_of_use']['number_user_sessions'] %} - {{ data['details_of_use']['number_user_sessions'] | readableInteger }} - {% else %} - {{ RequiredLabel() }} - {% endif %} -
-
+ {{ DefinitionReviewField("Number of User Sessions", "details_of_use", "number_user_sessions", filter="readableInteger") }} -
-
Average Daily Traffic (Number of Requests)
-
- {% if data['details_of_use']['average_daily_traffic'] %} - {{ data['details_of_use']['average_daily_traffic'] | readableInteger }} - {% else %} - {{ RequiredLabel() }} - {% endif %} -
-
+ {{ DefinitionReviewField("Average Daily Traffic (Number of Requests)", "details_of_use", "average_daily_traffic", filter="readableInteger") }} + + {{ DefinitionReviewField("Average Daily Traffic (GB)", "details_of_use", "average_daily_traffic_gb", filter="readableInteger") }} -
-
Average Daily Traffic (GB)
-
- {% if data['details_of_use']['average_daily_traffic_gb'] %} - {{ data['details_of_use']['average_daily_traffic_gb'] | readableInteger }} GB - {% else %} - {{ RequiredLabel() }} - {% endif %} -
-
{% endif %} -
-
Total Spend
-
- {% if data['details_of_use']['dollar_value'] %} - {{ data['details_of_use']['dollar_value'] | dollars }} - {% else %} - {{ RequiredLabel() }} - {% endif %} -
-
+ {{ DefinitionReviewField("Total Spend", "details_of_use", "dollar_value", filter="dollars") }} -
-
Start Date
-
{{ data['details_of_use']['start_date'] or RequiredLabel() }}
-
+ {{ DefinitionReviewField("Start Date", "details_of_use", "start_date") }}

@@ -183,63 +87,21 @@
-
-
First Name
-
{{ data['information_about_you']['fname_request'] or RequiredLabel() }}
-
+ {{ DefinitionReviewField("First Name", "information_about_you", "fname_request") }} -
-
Last Name
-
{{ data['information_about_you']['lname_request'] or RequiredLabel() }}
-
+ {{ DefinitionReviewField("Last Name", "information_about_you", "lname_request") }} -
-
Email Address
-
{{ data['information_about_you']['email_request'] or RequiredLabel() }}
-
+ {{ DefinitionReviewField("Email Address", "information_about_you", "email_request") }} -
-
Phone Number
-
- {% if data['information_about_you']['phone_number'] %} - {{ data['information_about_you']['phone_number'] | usPhone }} - {% else %} - {{ RequiredLabel() }} - {% endif %} -
-
+ {{ DefinitionReviewField("Phone Number", "information_about_you", "phone_number", filter="usPhone") }} -
-
Service Branch or Agency
-
- {% if data['information_about_you']['service_branch'] %} - {{ data['information_about_you']['service_branch'] | getOptionLabel(service_branches) }} - {% else %} - {{ RequiredLabel() }} - {% endif %} -
-
+ {{ DefinitionReviewField("Service Branch or Agency", "information_about_you", "service_branch", filter="getOptionLabel", filter_args=[service_branches]) }} -
-
Citizenship
-
{{ data['information_about_you']['citizenship'] or RequiredLabel() }}
-
+ {{ DefinitionReviewField("Citizenship", "information_about_you", "citizenship") }} -
-
Designation of Person
-
- {% if data['information_about_you']['designation'] %} - {{ data['information_about_you']['designation'] | capitalize }} - {% else %} - {{ RequiredLabel() }} - {% endif %} -
-
+ {{ DefinitionReviewField("Designation of Person", "information_about_you", "designation", filter="capitalize") }} -
-
Latest Information Assurance (IA) Training completion date
-
{{ data['information_about_you']['date_latest_training'] or RequiredLabel() }}
-
+ {{ DefinitionReviewField("Latest Information Assurance (IA) Training completion date", "information_about_you", "date_latest_training") }}

@@ -254,25 +116,13 @@
-
-
POC First Name
-
{{ data['primary_poc']['fname_poc'] or RequiredLabel() }}
-
+ {{ DefinitionReviewField("POC First Name", "primary_poc", "fname_poc") }} -
-
POC Last Name
-
{{ data['primary_poc']['lname_poc'] or RequiredLabel() }}
-
+ {{ DefinitionReviewField("POC Last Name", "primary_poc", "lname_poc") }} -
-
POC Email Address
-
{{ data['primary_poc']['email_poc'] or RequiredLabel() }}
-
+ {{ DefinitionReviewField("POC Email Address", "primary_poc", "email_poc") }} -
-
DOD ID
-
{{ data['primary_poc']['dodid_poc'] or RequiredLabel() }}
-
+ {{ DefinitionReviewField("DOD ID", "primary_poc", "dodid_poc") }}
From e8aa905a9904ecb2d30c8061258e4af31e85d8c6 Mon Sep 17 00:00:00 2001 From: dandds Date: Wed, 5 Sep 2018 17:17:30 -0400 Subject: [PATCH 03/11] display financial verification review info for ccpo --- atst/filters.py | 7 ++++ atst/forms/data.py | 8 ++++ atst/forms/financial.py | 9 +---- atst/routes/requests/approval.py | 15 +++++++- atst/routes/requests/requests_form.py | 2 + templates/requests/_review.html | 55 ++++++++++++++++++++++++++- tests/routes/test_requests_index.py | 10 ++--- 7 files changed, 90 insertions(+), 16 deletions(-) diff --git a/atst/filters.py b/atst/filters.py index e889b300..6a0eed36 100644 --- a/atst/filters.py +++ b/atst/filters.py @@ -30,6 +30,8 @@ def readableInteger(value): def getOptionLabel(value, options): + if hasattr(value, "value"): + value = value.value try: return next(tup[1] for tup in options if tup[0] == value) except StopIteration: @@ -59,6 +61,10 @@ def findFilter(value, filter_name, filter_args=[]): raise ValueError("filter name {} not found".format(filter_name)) +def renderList(value): + return app.jinja_env.filters["safe"]("
".join(value)) + + def register_filters(app): app.jinja_env.filters["iconSvg"] = iconSvg app.jinja_env.filters["dollars"] = dollars @@ -67,3 +73,4 @@ def register_filters(app): app.jinja_env.filters["getOptionLabel"] = getOptionLabel app.jinja_env.filters["mixedContentToJson"] = mixedContentToJson app.jinja_env.filters["findFilter"] = findFilter + app.jinja_env.filters["renderList"] = renderList diff --git a/atst/forms/data.py b/atst/forms/data.py index 5c7f9b7a..29b38b10 100644 --- a/atst/forms/data.py +++ b/atst/forms/data.py @@ -140,3 +140,11 @@ WORKSPACE_ROLES = [ }, ), ] + +FUNDING_TYPES = [ + ("", "- Select -"), + ("RDTE", "Research, Development, Testing & Evaluation (RDT&E)"), + ("OM", "Operations & Maintenance (O&M)"), + ("PROC", "Procurement (PROC)"), + ("OTHER", "Other"), +] diff --git a/atst/forms/financial.py b/atst/forms/financial.py index 8582486b..9f4afbb3 100644 --- a/atst/forms/financial.py +++ b/atst/forms/financial.py @@ -10,6 +10,7 @@ from atst.domain.task_orders import TaskOrders from .fields import NewlineListField, SelectField from .forms import ValidatedForm +from .data import FUNDING_TYPES PE_REGEX = re.compile( @@ -161,13 +162,7 @@ class ExtendedFinancialForm(BaseFinancialForm): funding_type = SelectField( description="What is the source of funding?", - choices=[ - ("", "- Select -"), - ("RDTE", "Research, Development, Testing & Evaluation (RDT&E)"), - ("OM", "Operations & Maintenance (O&M)"), - ("PROC", "Procurement (PROC)"), - ("OTHER", "Other"), - ], + choices=FUNDING_TYPES, validators=[Required()], render_kw={"required": False}, ) diff --git a/atst/routes/requests/approval.py b/atst/routes/requests/approval.py index ca4116c7..9df64395 100644 --- a/atst/routes/requests/approval.py +++ b/atst/routes/requests/approval.py @@ -1,11 +1,22 @@ from flask import render_template, g from . import requests_bp -from atst.forms.data import SERVICE_BRANCHES from atst.domain.requests import Requests +def task_order_dictionary(task_order): + return { + c.name: getattr(task_order, c.name) + for c in task_order.__table__.columns + if c.name not in ["id", "attachment_id"] + } + + @requests_bp.route("/requests/approval/", methods=["GET"]) def approval(request_id): request = Requests.get(g.current_user, request_id) - return render_template("requests/approval.html", data=request.body, service_branches=SERVICE_BRANCHES) + data = request.body + if request.task_order: + data["task_order"] = task_order_dictionary(request.task_order) + + return render_template("requests/approval.html", data=data, financial_review=True) diff --git a/atst/routes/requests/requests_form.py b/atst/routes/requests/requests_form.py index 1a0353bc..ca941a8c 100644 --- a/atst/routes/requests/requests_form.py +++ b/atst/routes/requests/requests_form.py @@ -9,6 +9,7 @@ from atst.forms.data import ( ASSISTANCE_ORG_TYPES, DATA_TRANSFER_AMOUNTS, COMPLETION_DATE_RANGES, + FUNDING_TYPES, ) @@ -19,6 +20,7 @@ def option_data(): "assistance_org_types": ASSISTANCE_ORG_TYPES, "data_transfer_amounts": DATA_TRANSFER_AMOUNTS, "completion_date_ranges": COMPLETION_DATE_RANGES, + "funding_types": FUNDING_TYPES, } diff --git a/templates/requests/_review.html b/templates/requests/_review.html index 576f07fc..7e7fb33d 100644 --- a/templates/requests/_review.html +++ b/templates/requests/_review.html @@ -4,7 +4,7 @@ {% macro DefinitionReviewField(title, section, item_name, filter=None, filter_args=[]) -%}
-
{{ title }}
+
{{ title | safe }}
{% if data[section] and data[section][item_name] %} {{ data[section][item_name] | findFilter(filter, filter_args) }} @@ -125,4 +125,57 @@ {{ DefinitionReviewField("DOD ID", "primary_poc", "dodid_poc") }} +{% if financial_review %} +
+

+ Financial Verification +

+
+ {{ DefinitionReviewField("Task Order Number", "task_order", "number") }} + + {{ DefinitionReviewField("What is the source of funding?", "task_order", "funding_type", filter="getOptionLabel", filter_args=[funding_types]) }} + + {% if data["task_order"] and data["task_order"]["funding_type"].value == "OTHER" %} + {{ DefinitionReviewField("If other, please specify", "task_order", "funding_type_other") }} + {% endif %} + + {{ DefinitionReviewField("
CLIN 0001
-
Unclassified IaaS and PaaS Amount
", "task_order", "clin_0001", filter="dollars") }} + + {{ DefinitionReviewField("
CLIN 0003
-
Unclassified Cloud Support Package
", "task_order", "clin_0003", filter="dollars") }} + + {{ DefinitionReviewField("
CLIN 1001
-
Unclassified IaaS and PaaS Amount
OPTION PERIOD 1
", "task_order", "clin_1001", filter="dollars") }} + + {{ DefinitionReviewField("
CLIN 1003
-
Unclassified Cloud Support Package
OPTION PERIOD 1
", "task_order", "clin_1003", filter="dollars") }} + + {{ DefinitionReviewField("
CLIN 2001
-
Unclassified IaaS and PaaS Amount
OPTION PERIOD 2
", "task_order", "clin_2001", filter="dollars") }} + + {{ DefinitionReviewField("
CLIN 2003
-
Unclassified Cloud Support Package
OPTION PERIOD 2
", "task_order", "clin_2003", filter="dollars") }} + + + + {{ DefinitionReviewField("Unique Item Identifier (UII)s related to your application(s) if you already have them", "financial_verification", "uii_ids", filter="renderList") }} + + {{ DefinitionReviewField("Program Element (PE) Number related to your request", "financial_verification", "pe_id") }} + + {{ DefinitionReviewField("Program Treasury Code", "financial_verification", "treasury_code") }} + + {{ DefinitionReviewField("Program Budget Activity (BA) Code", "financial_verification", "ba_code") }} + + {{ DefinitionReviewField("Contracting Officer First Name", "financial_verification", "fname_co") }} + + {{ DefinitionReviewField("Contracting Officer Last Name", "financial_verification", "lname_co") }} + + {{ DefinitionReviewField("Contracting Officer Email", "financial_verification", "email_co") }} + + {{ DefinitionReviewField("Contracting Officer Office", "financial_verification", "office_co") }} + + {{ DefinitionReviewField("Contracting Officer Representative (COR) First Name", "financial_verification", "fname_cor") }} + + {{ DefinitionReviewField("Contracting Officer Representative (COR) Last Name", "financial_verification", "lname_cor") }} + + {{ DefinitionReviewField("Contracting Officer Representative (COR) Email", "financial_verification", "email_cor") }} + + {{ DefinitionReviewField("Contracting Officer Representative (COR) Office", "financial_verification", "office_cor") }} +
+{% endif %} diff --git a/tests/routes/test_requests_index.py b/tests/routes/test_requests_index.py index c8d6fa5d..29300b65 100644 --- a/tests/routes/test_requests_index.py +++ b/tests/routes/test_requests_index.py @@ -30,13 +30,11 @@ def test_ccpo_sees_approval_screen(): request = RequestFactory.create() Requests.submit(request) ccpo_context = RequestsIndex(ccpo).execute() - assert ( - ccpo_context["requests"][0]["edit_link"] - == url_for("requests.approval", request_id=request.id) + assert ccpo_context["requests"][0]["edit_link"] == url_for( + "requests.approval", request_id=request.id ) mo_context = RequestsIndex(request.creator).execute() - assert ( - mo_context["requests"][0]["edit_link"] - != url_for("requests.approval", request_id=request.id) + assert mo_context["requests"][0]["edit_link"] != url_for( + "requests.approval", request_id=request.id ) From 0391348b5d0bafea25866a14f46dcaa7846339eb Mon Sep 17 00:00:00 2001 From: dandds Date: Wed, 5 Sep 2018 17:45:12 -0400 Subject: [PATCH 04/11] basic task order pdf downloads --- atst/routes/requests/approval.py | 17 ++++++++++-- atst/uploader.py | 5 ++-- templates/requests/_review.html | 8 ++++++ templates/requests/approval.html | 2 +- tests/routes/test_request_approval.py | 38 +++++++++++++++++++++++++++ tests/test_uploader.py | 16 +++++++++++ 6 files changed, 81 insertions(+), 5 deletions(-) create mode 100644 tests/routes/test_request_approval.py diff --git a/atst/routes/requests/approval.py b/atst/routes/requests/approval.py index 9df64395..e9525ec7 100644 --- a/atst/routes/requests/approval.py +++ b/atst/routes/requests/approval.py @@ -1,7 +1,9 @@ -from flask import render_template, g +from flask import render_template, g, Response +from flask import current_app as app from . import requests_bp from atst.domain.requests import Requests +from atst.domain.exceptions import NotFoundError def task_order_dictionary(task_order): @@ -19,4 +21,15 @@ def approval(request_id): if request.task_order: data["task_order"] = task_order_dictionary(request.task_order) - return render_template("requests/approval.html", data=data, financial_review=True) + return render_template("requests/approval.html", data=data, request_id=request.id, financial_review=True) + + +@requests_bp.route("/requests/task_order_download/", methods=["GET"]) +def task_order_pdf_download(request_id): + request = Requests.get(g.current_user, request_id) + if request.task_order and request.task_order.pdf: + object_name = request.task_order.pdf.object_name + generator = app.uploader.download_stream(object_name) + return Response(generator, mimetype="application/pdf") + else: + raise NotFoundError("task_order pdf") diff --git a/atst/uploader.py b/atst/uploader.py index e63ba4ce..5955913c 100644 --- a/atst/uploader.py +++ b/atst/uploader.py @@ -37,8 +37,9 @@ class Uploader: ) return (fyle.filename, object_name) - def download(self, path): - pass + def download_stream(self, object_name): + obj = self.container.get_object(object_name=object_name) + return obj.as_stream() def _get_container(self, provider, container, key, secret): if provider == "LOCAL": diff --git a/templates/requests/_review.html b/templates/requests/_review.html index 7e7fb33d..8ba3dfda 100644 --- a/templates/requests/_review.html +++ b/templates/requests/_review.html @@ -153,6 +153,14 @@ {{ DefinitionReviewField("
CLIN 2003
-
Unclassified Cloud Support Package
OPTION PERIOD 2
", "task_order", "clin_2003", filter="dollars") }} +
+
Task Order PDF
+
+ + Download the Task Order PDF + +
+
{{ DefinitionReviewField("Unique Item Identifier (UII)s related to your application(s) if you already have them", "financial_verification", "uii_ids", filter="renderList") }} diff --git a/templates/requests/approval.html b/templates/requests/approval.html index 6f9b8dd8..9d219c6d 100644 --- a/templates/requests/approval.html +++ b/templates/requests/approval.html @@ -18,7 +18,7 @@

Ongoing maintainence for Death Star (a moon-sized Imperial military battlestation armed with a planet-destroying superlaser). Its definitely hasn't been sabotaged from the start.

- {% with data=data, service_branches=service_branches %} + {% with data=data, request_id=request_id %} {% include "requests/_review.html" %} {% endwith %} diff --git a/tests/routes/test_request_approval.py b/tests/routes/test_request_approval.py new file mode 100644 index 00000000..7e89026f --- /dev/null +++ b/tests/routes/test_request_approval.py @@ -0,0 +1,38 @@ +import os +from flask import url_for + +from atst.models.attachment import Attachment +from tests.factories import RequestFactory, TaskOrderFactory, UserFactory + + +def test_approval(): + pass + + +def test_task_order_download(app, client, user_session, pdf_upload): + user = UserFactory.create() + user_session(user) + + attachment = Attachment.attach(pdf_upload) + task_order = TaskOrderFactory.create(number="abc123", pdf=attachment) + request = RequestFactory.create(task_order=task_order, creator=user) + + # ensure that real data for pdf upload has been flushed to disk + pdf_upload.seek(0) + pdf_content = pdf_upload.read() + pdf_upload.close() + full_path = os.path.join(app.config.get("STORAGE_CONTAINER"), attachment.object_name) + with open(full_path, "wb") as output_file: + output_file.write(pdf_content) + output_file.flush() + + response = client.get(url_for("requests.task_order_pdf_download", request_id=request.id)) + assert response.data == pdf_content + + +def test_task_order_download_does_not_exist(client, user_session): + user = UserFactory.create() + user_session(user) + request = RequestFactory.create(creator=user) + response = client.get(url_for("requests.task_order_pdf_download", request_id=request.id)) + assert response.status_code == 404 diff --git a/tests/test_uploader.py b/tests/test_uploader.py index 89ec1b73..131277f5 100644 --- a/tests/test_uploader.py +++ b/tests/test_uploader.py @@ -31,3 +31,19 @@ def test_upload_fails_for_non_pdfs(uploader): fs = FileStorage(fp, content_type="text/plain") with pytest.raises(UploadError): uploader.upload(fs) + + +def test_download_stream(upload_dir, uploader, pdf_upload): + # write pdf content to upload file storage and make sure it is flushed to + # disk + pdf_upload.seek(0) + pdf_content = pdf_upload.read() + pdf_upload.close() + full_path = os.path.join(upload_dir, "abc") + with open(full_path, "wb") as output_file: + output_file.write(pdf_content) + output_file.flush() + + stream = uploader.download_stream("abc") + stream_content = b"".join([b for b in stream]) + assert pdf_content == stream_content From 8f97fc4cbf03499f817313fdab4be6785b59a4b5 Mon Sep 17 00:00:00 2001 From: dandds Date: Thu, 6 Sep 2018 10:36:06 -0400 Subject: [PATCH 05/11] check user is ccpo for request approval page --- atst/domain/authz.py | 10 ++++++++ atst/routes/requests/approval.py | 10 +++++++- tests/routes/test_request_approval.py | 33 +++++++++++++++++++++++---- 3 files changed, 47 insertions(+), 6 deletions(-) diff --git a/atst/domain/authz.py b/atst/domain/authz.py index 65db9894..467d5150 100644 --- a/atst/domain/authz.py +++ b/atst/domain/authz.py @@ -25,6 +25,16 @@ class Authorization(object): return False + @classmethod + def check_can_approve_request(cls, user): + if ( + Permissions.REVIEW_AND_APPROVE_JEDI_WORKSPACE_REQUEST + in user.atat_permissions + ): + return True + else: + raise UnauthorizedError(user, "cannot review and approve requests") + @classmethod def check_workspace_permission(cls, user, workspace, permission, message): if not Authorization.has_workspace_permission(user, workspace, permission): diff --git a/atst/routes/requests/approval.py b/atst/routes/requests/approval.py index e9525ec7..fefea484 100644 --- a/atst/routes/requests/approval.py +++ b/atst/routes/requests/approval.py @@ -4,6 +4,7 @@ from flask import current_app as app from . import requests_bp from atst.domain.requests import Requests from atst.domain.exceptions import NotFoundError +from atst.domain.authz import Authorization def task_order_dictionary(task_order): @@ -17,11 +18,18 @@ def task_order_dictionary(task_order): @requests_bp.route("/requests/approval/", methods=["GET"]) def approval(request_id): request = Requests.get(g.current_user, request_id) + Authorization.check_can_approve_request(g.current_user) + data = request.body if request.task_order: data["task_order"] = task_order_dictionary(request.task_order) - return render_template("requests/approval.html", data=data, request_id=request.id, financial_review=True) + return render_template( + "requests/approval.html", + data=data, + request_id=request.id, + financial_review=True, + ) @requests_bp.route("/requests/task_order_download/", methods=["GET"]) diff --git a/tests/routes/test_request_approval.py b/tests/routes/test_request_approval.py index 7e89026f..5680e30c 100644 --- a/tests/routes/test_request_approval.py +++ b/tests/routes/test_request_approval.py @@ -2,11 +2,28 @@ import os from flask import url_for from atst.models.attachment import Attachment +from atst.domain.roles import Roles + from tests.factories import RequestFactory, TaskOrderFactory, UserFactory -def test_approval(): - pass +def test_ccpo_can_view_approval(user_session, client): + ccpo = Roles.get("ccpo") + user = UserFactory.create(atat_role=ccpo) + user_session(user) + + request = RequestFactory.create() + response = client.get(url_for("requests.approval", request_id=request.id)) + assert response.status_code == 200 + + +def test_non_ccpo_cannot_view_approval(user_session, client): + user = UserFactory.create() + user_session(user) + + request = RequestFactory.create(creator=user) + response = client.get(url_for("requests.approval", request_id=request.id)) + assert response.status_code == 404 def test_task_order_download(app, client, user_session, pdf_upload): @@ -21,12 +38,16 @@ def test_task_order_download(app, client, user_session, pdf_upload): pdf_upload.seek(0) pdf_content = pdf_upload.read() pdf_upload.close() - full_path = os.path.join(app.config.get("STORAGE_CONTAINER"), attachment.object_name) + full_path = os.path.join( + app.config.get("STORAGE_CONTAINER"), attachment.object_name + ) with open(full_path, "wb") as output_file: output_file.write(pdf_content) output_file.flush() - response = client.get(url_for("requests.task_order_pdf_download", request_id=request.id)) + response = client.get( + url_for("requests.task_order_pdf_download", request_id=request.id) + ) assert response.data == pdf_content @@ -34,5 +55,7 @@ def test_task_order_download_does_not_exist(client, user_session): user = UserFactory.create() user_session(user) request = RequestFactory.create(creator=user) - response = client.get(url_for("requests.task_order_pdf_download", request_id=request.id)) + response = client.get( + url_for("requests.task_order_pdf_download", request_id=request.id) + ) assert response.status_code == 404 From d139c6678d4913ea2b7a861ba05aeca8af567530 Mon Sep 17 00:00:00 2001 From: dandds Date: Thu, 6 Sep 2018 10:58:12 -0400 Subject: [PATCH 06/11] set content-disposition for task order downloads so filename is known --- atst/routes/requests/approval.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/atst/routes/requests/approval.py b/atst/routes/requests/approval.py index fefea484..ddf34c38 100644 --- a/atst/routes/requests/approval.py +++ b/atst/routes/requests/approval.py @@ -36,8 +36,15 @@ def approval(request_id): def task_order_pdf_download(request_id): request = Requests.get(g.current_user, request_id) if request.task_order and request.task_order.pdf: - object_name = request.task_order.pdf.object_name - generator = app.uploader.download_stream(object_name) - return Response(generator, mimetype="application/pdf") + pdf = request.task_order.pdf + generator = app.uploader.download_stream(pdf.object_name) + return Response( + generator, + headers={ + "Content-Disposition": "attachment; filename={}".format(pdf.filename) + }, + mimetype="application/pdf", + ) + else: raise NotFoundError("task_order pdf") From a6bd27d880ea10ab01fe7dfcd804ff5c5fd58a34 Mon Sep 17 00:00:00 2001 From: dandds Date: Thu, 6 Sep 2018 11:46:43 -0400 Subject: [PATCH 07/11] conditionally display pdf link on request approval page --- atst/routes/requests/approval.py | 1 + templates/requests/_review.html | 18 ++++++++---------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/atst/routes/requests/approval.py b/atst/routes/requests/approval.py index ddf34c38..f120a384 100644 --- a/atst/routes/requests/approval.py +++ b/atst/routes/requests/approval.py @@ -29,6 +29,7 @@ def approval(request_id): data=data, request_id=request.id, financial_review=True, + pdf_available=request.task_order and request.task_order.pdf, ) diff --git a/templates/requests/_review.html b/templates/requests/_review.html index 8ba3dfda..80881a71 100644 --- a/templates/requests/_review.html +++ b/templates/requests/_review.html @@ -131,6 +131,14 @@ Financial Verification + {% if pdf_available %} + + {% endif %} +
{{ DefinitionReviewField("Task Order Number", "task_order", "number") }} @@ -152,16 +160,6 @@ {{ DefinitionReviewField("
CLIN 2003
-
Unclassified Cloud Support Package
OPTION PERIOD 2
", "task_order", "clin_2003", filter="dollars") }} - -
-
Task Order PDF
-
- - Download the Task Order PDF - -
-
- {{ DefinitionReviewField("Unique Item Identifier (UII)s related to your application(s) if you already have them", "financial_verification", "uii_ids", filter="renderList") }} {{ DefinitionReviewField("Program Element (PE) Number related to your request", "financial_verification", "pe_id") }} From 8741e8ee418b411ffc525160bcdc4e386de0a532 Mon Sep 17 00:00:00 2001 From: dandds Date: Thu, 6 Sep 2018 12:08:49 -0400 Subject: [PATCH 08/11] display real request info for number, description --- atst/routes/requests/approval.py | 3 ++- templates/requests/approval.html | 12 +++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/atst/routes/requests/approval.py b/atst/routes/requests/approval.py index f120a384..20b9c890 100644 --- a/atst/routes/requests/approval.py +++ b/atst/routes/requests/approval.py @@ -27,7 +27,8 @@ def approval(request_id): return render_template( "requests/approval.html", data=data, - request_id=request.id, + request_id=request_id, + status=request.status.value, financial_review=True, pdf_available=request.task_order and request.task_order.pdf, ) diff --git a/templates/requests/approval.html b/templates/requests/approval.html index 9d219c6d..446e6dee 100644 --- a/templates/requests/approval.html +++ b/templates/requests/approval.html @@ -10,13 +10,19 @@
-

Request #1234567890

- Pending +

Request #{{ request_id }}

+ {{ status }}
-

Ongoing maintainence for Death Star (a moon-sized Imperial military battlestation armed with a planet-destroying superlaser). Its definitely hasn't been sabotaged from the start.

+

+ {% if data["details_of_use"] and data["details_of_use"]["jedi_usage"] %} + {{ data["details_of_use"]["jedi_usage"] }} + {% else %} + Missing usage information + {% endif %} +

{% with data=data, request_id=request_id %} {% include "requests/_review.html" %} From 6241ca4da7544f5b9d932b27be5e5c9545972206 Mon Sep 17 00:00:00 2001 From: dandds Date: Thu, 6 Sep 2018 12:57:03 -0400 Subject: [PATCH 09/11] do not redundantly include usage description on ccpo review page --- templates/requests/approval.html | 8 -------- 1 file changed, 8 deletions(-) diff --git a/templates/requests/approval.html b/templates/requests/approval.html index 446e6dee..e67bd935 100644 --- a/templates/requests/approval.html +++ b/templates/requests/approval.html @@ -16,14 +16,6 @@
-

- {% if data["details_of_use"] and data["details_of_use"]["jedi_usage"] %} - {{ data["details_of_use"]["jedi_usage"] }} - {% else %} - Missing usage information - {% endif %} -

- {% with data=data, request_id=request_id %} {% include "requests/_review.html" %} {% endwith %} From 4ec85a11f145907630e6cbe494c302916a18e979 Mon Sep 17 00:00:00 2001 From: dandds Date: Thu, 6 Sep 2018 13:32:26 -0400 Subject: [PATCH 10/11] because of bug LIBCLOUD-931, write file downloads to tempfile --- atst/uploader.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/atst/uploader.py b/atst/uploader.py index 5955913c..03bc2460 100644 --- a/atst/uploader.py +++ b/atst/uploader.py @@ -39,7 +39,9 @@ class Uploader: def download_stream(self, object_name): obj = self.container.get_object(object_name=object_name) - return obj.as_stream() + with NamedTemporaryFile() as tempfile: + obj.download(tempfile.name, overwrite_existing=True) + return open(tempfile.name, "rb") def _get_container(self, provider, container, key, secret): if provider == "LOCAL": From a6c069e85a37e82cc1719dedb00022618a9ce60d Mon Sep 17 00:00:00 2001 From: dandds Date: Fri, 7 Sep 2018 09:43:36 -0400 Subject: [PATCH 11/11] add task order information source to review form --- atst/forms/data.py | 2 ++ atst/models/task_order.py | 2 +- atst/routes/requests/requests_form.py | 2 ++ templates/requests/_review.html | 2 ++ 4 files changed, 7 insertions(+), 1 deletion(-) diff --git a/atst/forms/data.py b/atst/forms/data.py index 29b38b10..15161558 100644 --- a/atst/forms/data.py +++ b/atst/forms/data.py @@ -148,3 +148,5 @@ FUNDING_TYPES = [ ("PROC", "Procurement (PROC)"), ("OTHER", "Other"), ] + +TASK_ORDER_SOURCES = [("MANUAL", "Manual"), ("EDA", "EDA")] diff --git a/atst/models/task_order.py b/atst/models/task_order.py index 2a195176..344218f4 100644 --- a/atst/models/task_order.py +++ b/atst/models/task_order.py @@ -8,7 +8,7 @@ from atst.models import Base class Source(Enum): MANUAL = "Manual" - EDA = "eda" + EDA = "EDA" class FundingType(Enum): diff --git a/atst/routes/requests/requests_form.py b/atst/routes/requests/requests_form.py index ca941a8c..0aef5fec 100644 --- a/atst/routes/requests/requests_form.py +++ b/atst/routes/requests/requests_form.py @@ -10,6 +10,7 @@ from atst.forms.data import ( DATA_TRANSFER_AMOUNTS, COMPLETION_DATE_RANGES, FUNDING_TYPES, + TASK_ORDER_SOURCES, ) @@ -21,6 +22,7 @@ def option_data(): "data_transfer_amounts": DATA_TRANSFER_AMOUNTS, "completion_date_ranges": COMPLETION_DATE_RANGES, "funding_types": FUNDING_TYPES, + "task_order_sources": TASK_ORDER_SOURCES, } diff --git a/templates/requests/_review.html b/templates/requests/_review.html index 80881a71..1fd14432 100644 --- a/templates/requests/_review.html +++ b/templates/requests/_review.html @@ -140,6 +140,8 @@ {% endif %}
+ {{ DefinitionReviewField("Task Order Information Source", "task_order", "source", filter="getOptionLabel", filter_args=[task_order_sources]) }} + {{ DefinitionReviewField("Task Order Number", "task_order", "number") }} {{ DefinitionReviewField("What is the source of funding?", "task_order", "funding_type", filter="getOptionLabel", filter_args=[funding_types]) }}