From 4a377007f6b8166ef482ee4afa8a76074be8c379 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Mon, 21 Jan 2019 15:52:56 -0500 Subject: [PATCH 01/13] Update mixedContentToJson filter to be more general --- atst/filters.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/atst/filters.py b/atst/filters.py index 1c368aee..da7047b3 100644 --- a/atst/filters.py +++ b/atst/filters.py @@ -54,12 +54,11 @@ def mixedContentToJson(value): This coerces the file upload in form data to its filename so that the data can be JSON serialized. """ - if ( - isinstance(value, dict) - and "legacy_task_order" in value - and hasattr(value["legacy_task_order"]["pdf"], "filename") - ): - value["legacy_task_order"]["pdf"] = value["legacy_task_order"]["pdf"].filename + if isinstance(value, dict): + for k, v in value.items(): + if hasattr(v, "filename"): + value[k] = v.filename + return app.jinja_env.filters["tojson"](value) From ebf063f245488aa4b48743ac35be730af8dbf009 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Mon, 21 Jan 2019 15:53:54 -0500 Subject: [PATCH 02/13] Handle uploaded file in task order route --- atst/routes/task_orders/new.py | 3 ++- templates/task_orders/_new.html | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/atst/routes/task_orders/new.py b/atst/routes/task_orders/new.py index 496657ca..f0b90c23 100644 --- a/atst/routes/task_orders/new.py +++ b/atst/routes/task_orders/new.py @@ -272,8 +272,9 @@ def new(screen, task_order_id=None, portfolio_id=None): "/portfolios//task_orders/new/", methods=["POST"] ) def update(screen, task_order_id=None, portfolio_id=None): + form_data = {**http_request.form, **http_request.files} workflow = UpdateTaskOrderWorkflow( - g.current_user, http_request.form, screen, task_order_id, portfolio_id + g.current_user, form_data, screen, task_order_id, portfolio_id ) if workflow.validate(): diff --git a/templates/task_orders/_new.html b/templates/task_orders/_new.html index 10694ef7..b80bced9 100644 --- a/templates/task_orders/_new.html +++ b/templates/task_orders/_new.html @@ -10,9 +10,9 @@ {% block form_action %} {% if task_order_id %} -
+ {% else %} - + {% endif %} {% endblock %} From 2298c5135e3d5585b8ff09a04908bcb4132d86dc Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Mon, 21 Jan 2019 15:55:00 -0500 Subject: [PATCH 03/13] Add csp_estimate property to task order model --- atst/models/task_order.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/atst/models/task_order.py b/atst/models/task_order.py index 04260b7b..61bd4b60 100644 --- a/atst/models/task_order.py +++ b/atst/models/task_order.py @@ -2,10 +2,11 @@ from enum import Enum import pendulum from sqlalchemy import Column, Numeric, String, ForeignKey, Date, Integer +from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.types import ARRAY from sqlalchemy.orm import relationship -from atst.models import Base, types, mixins +from atst.models import Attachment, Base, types, mixins class Status(Enum): @@ -49,7 +50,7 @@ class TaskOrder(Base, mixins.TimestampsMixin): end_date = Column(Date) performance_length = Column(Integer) attachment_id = Column(ForeignKey("attachments.id")) - pdf = relationship("Attachment") + _csp_estimate = relationship("Attachment") clin_01 = Column(Numeric(scale=2)) clin_02 = Column(Numeric(scale=2)) clin_03 = Column(Numeric(scale=2)) @@ -72,6 +73,19 @@ class TaskOrder(Base, mixins.TimestampsMixin): number = Column(String, unique=True) # Task Order Number loa = Column(ARRAY(String)) # Line of Accounting (LOA) + @hybrid_property + def csp_estimate(self): + return self._csp_estimate + + @csp_estimate.setter + def csp_estimate(self, new_csp_estimate): + if isinstance(new_csp_estimate, Attachment): + self._csp_estimate = new_csp_estimate + else: + self._csp_estimate = Attachment.attach( + new_csp_estimate, "task_order", self.id + ) + @property def is_submitted(self): return self.number is not None From 6a9290619d4f03d0e212a43b11459e24db91f613 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Mon, 21 Jan 2019 15:55:37 -0500 Subject: [PATCH 04/13] Update funding form to handle uploading pdf/png --- atst/domain/csp/files.py | 2 +- atst/domain/task_orders.py | 2 +- atst/forms/task_order.py | 12 ++++++++--- js/components/forms/funding.js | 9 +++++++++ templates/task_orders/new/funding.html | 27 ++++++++++++++++++------- tests/fixtures/sample.png | Bin 0 -> 14728 bytes translations.yaml | 5 +++-- 7 files changed, 43 insertions(+), 14 deletions(-) create mode 100644 tests/fixtures/sample.png diff --git a/atst/domain/csp/files.py b/atst/domain/csp/files.py index 9fbb545e..905eee30 100644 --- a/atst/domain/csp/files.py +++ b/atst/domain/csp/files.py @@ -8,7 +8,7 @@ from atst.domain.exceptions import UploadError class FileProviderInterface: - _PERMITTED_MIMETYPES = ["application/pdf"] + _PERMITTED_MIMETYPES = ["application/pdf", "image/png"] def _enforce_mimetype(self, fyle): # TODO: for hardening, we should probably use a better library for diff --git a/atst/domain/task_orders.py b/atst/domain/task_orders.py index 95a20d62..65e9b8dd 100644 --- a/atst/domain/task_orders.py +++ b/atst/domain/task_orders.py @@ -25,7 +25,7 @@ class TaskOrders(object): ], "funding": [ "performance_length", - # "pdf", + "csp_estimate", "clin_01", "clin_02", "clin_03", diff --git a/atst/forms/task_order.py b/atst/forms/task_order.py index 5d1157d6..83cf127c 100644 --- a/atst/forms/task_order.py +++ b/atst/forms/task_order.py @@ -11,6 +11,7 @@ from wtforms.fields import ( from wtforms.fields.html5 import DateField, TelField from wtforms.widgets import ListWidget, CheckboxInput from wtforms.validators import Length +from flask_wtf.file import FileAllowed from atst.forms.validators import IsNumber, PhoneNumber, RequiredIf @@ -86,9 +87,14 @@ class FundingForm(CacheableForm): end_date = DateField( translate("forms.task_order.end_date_label"), format="%m/%d/%Y" ) - pdf = FileField( - translate("forms.task_order.pdf_label"), - description=translate("forms.task_order.pdf_description"), + csp_estimate = FileField( + translate("forms.task_order.csp_estimate_label"), + description=translate("forms.task_order.csp_estimate_description"), + validators=[ + FileAllowed( + ["pdf", "png"], translate("forms.task_order.file_format_not_allowed") + ) + ], ) clin_01 = IntegerField(translate("forms.task_order.clin_01_label")) clin_02 = IntegerField(translate("forms.task_order.clin_02_label")) diff --git a/js/components/forms/funding.js b/js/components/forms/funding.js index d75ee74b..cad96abc 100644 --- a/js/components/forms/funding.js +++ b/js/components/forms/funding.js @@ -19,6 +19,10 @@ export default { initialData: { type: Object, default: () => ({}) + }, + uploadErrors: { + type: Array, + default: () => ([]) } }, @@ -28,6 +32,7 @@ export default { clin_02 = 0, clin_03 = 0, clin_04 = 0, + csp_estimate, } = this.initialData return { @@ -35,6 +40,7 @@ export default { clin_02, clin_03, clin_04, + showUpload: !csp_estimate || this.uploadErrors.length > 0 } }, @@ -57,6 +63,9 @@ export default { const mask = createNumberMask({ prefix: '$', allowDecimal: true }) return conformToMask(intValue.toString(), mask).conformedValue }, + showUploadInput: function() { + this.showUpload = true + }, updateBudget: function() { document.querySelector('#to-target').innerText = this.totalBudgetStr } diff --git a/templates/task_orders/new/funding.html b/templates/task_orders/new/funding.html index be24a652..3f026272 100644 --- a/templates/task_orders/new/funding.html +++ b/templates/task_orders/new/funding.html @@ -12,7 +12,11 @@ {% block form %} - +

{{ "task_orders.new.funding.performance_period_title" | translate }}

@@ -28,13 +32,22 @@ {{ Icon("link")}} Cloud Service Provider's estimate calculator

{{ "task_orders.new.funding.estimate_usage_paragraph" | translate }}

-
-
- {{ form.pdf.label }} + +
diff --git a/tests/fixtures/sample.png b/tests/fixtures/sample.png new file mode 100644 index 0000000000000000000000000000000000000000..49909d239e011651c14f4f5531278dc9fc9397f0 GIT binary patch literal 14728 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 WG#I?b>e5UA0000 Date: Mon, 21 Jan 2019 16:51:40 -0500 Subject: [PATCH 05/13] Seek to beginning of file before writing Not seeking was providing files of 0 bytes. Not sure why this just recently started breaking. --- atst/domain/csp/files.py | 1 + tests/domain/csp/test_files.py | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/atst/domain/csp/files.py b/atst/domain/csp/files.py index 905eee30..a74403e5 100644 --- a/atst/domain/csp/files.py +++ b/atst/domain/csp/files.py @@ -57,6 +57,7 @@ class RackspaceFileProvider(FileProviderInterface): object_name = uuid4().hex with NamedTemporaryFile() as tempfile: tempfile.write(fyle.stream.read()) + tempfile.seek(0) self.container.upload_object( file_path=tempfile.name, object_name=object_name, diff --git a/tests/domain/csp/test_files.py b/tests/domain/csp/test_files.py index 0f50cd11..66830a63 100644 --- a/tests/domain/csp/test_files.py +++ b/tests/domain/csp/test_files.py @@ -44,3 +44,14 @@ def test_download(app, uploader, pdf_upload): stream = uploader.download("abc") stream_content = b"".join([b for b in stream]) assert pdf_content == stream_content + + +def test_downloading_uploaded_object(uploader, pdf_upload): + object_name = uploader.upload(pdf_upload) + stream = uploader.download(object_name) + stream_content = b"".join([b for b in stream]) + + pdf_upload.seek(0) + pdf_content = pdf_upload.read() + + assert stream_content == pdf_content From eb4f3f4871fa66f236a8d770c78495694fa30074 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Mon, 21 Jan 2019 16:57:30 -0500 Subject: [PATCH 06/13] Add links to download CSP estimate --- atst/routes/task_orders/index.py | 21 ++++++++++++++++++++- templates/portfolios/task_orders/show.html | 2 +- templates/task_orders/new/review.html | 2 +- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/atst/routes/task_orders/index.py b/atst/routes/task_orders/index.py index 74393e75..6a632cbb 100644 --- a/atst/routes/task_orders/index.py +++ b/atst/routes/task_orders/index.py @@ -1,5 +1,5 @@ from io import BytesIO -from flask import g, Response +from flask import g, Response, current_app as app from . import task_orders_bp from atst.domain.task_orders import TaskOrders @@ -17,3 +17,22 @@ def download_summary(task_order_id): headers={"Content-Disposition": "attachment; filename={}".format(filename)}, mimetype="application/vnd.openxmlformats-officedocument.wordprocessingml.document", ) + + +@task_orders_bp.route("/task_orders/csp_estimate/") +def download_csp_estimate(task_order_id): + task_order = TaskOrders.get(g.current_user, task_order_id) + if task_order.csp_estimate: + estimate = task_order.csp_estimate + generator = app.csp.files.download(estimate.object_name) + return Response( + generator, + headers={ + "Content-Disposition": "attachment; filename={}".format( + estimate.filename + ) + }, + ) + + else: + raise NotFoundError("task_order CSP estimate") diff --git a/templates/portfolios/task_orders/show.html b/templates/portfolios/task_orders/show.html index 38dadacf..144e6c5b 100644 --- a/templates/portfolios/task_orders/show.html +++ b/templates/portfolios/task_orders/show.html @@ -133,7 +133,7 @@
{{ DocumentLink( title="Cloud Services Estimate", - link_url="#") }} + link_url=task_order.csp_estimate and url_for("task_orders.download_csp_estimate", task_order_id=task_order.id) ) }} {{ DocumentLink( title="Market Research", link_url="#") }} diff --git a/templates/task_orders/new/review.html b/templates/task_orders/new/review.html index 440c58ad..53089982 100644 --- a/templates/task_orders/new/review.html +++ b/templates/task_orders/new/review.html @@ -113,7 +113,7 @@
{% call ReviewField(("task_orders.new.review.performance_period" | translate), task_order.performance_length, filter="translateDuration") %} -

{{ Icon('download') }} {{ "task_orders.new.review.usage_est_link" | translate }}

+

{{ Icon('download') }} {{ "task_orders.new.review.usage_est_link"| translate }}

{% endcall %}
From 30ebebb13a6d1444c052755ee5c2348f01f7ed5b Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Tue, 22 Jan 2019 11:16:03 -0500 Subject: [PATCH 07/13] Fix failing tests --- atst/models/task_order.py | 3 ++- tests/domain/test_task_orders.py | 11 ++++++++++- tests/routes/task_orders/test_new_task_order.py | 6 +++++- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/atst/models/task_order.py b/atst/models/task_order.py index 61bd4b60..f16b9389 100644 --- a/atst/models/task_order.py +++ b/atst/models/task_order.py @@ -5,6 +5,7 @@ from sqlalchemy import Column, Numeric, String, ForeignKey, Date, Integer from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.types import ARRAY from sqlalchemy.orm import relationship +from werkzeug.datastructures import FileStorage from atst.models import Attachment, Base, types, mixins @@ -81,7 +82,7 @@ class TaskOrder(Base, mixins.TimestampsMixin): def csp_estimate(self, new_csp_estimate): if isinstance(new_csp_estimate, Attachment): self._csp_estimate = new_csp_estimate - else: + elif isinstance(new_csp_estimate, FileStorage): self._csp_estimate = Attachment.attach( new_csp_estimate, "task_order", self.id ) diff --git a/tests/domain/test_task_orders.py b/tests/domain/test_task_orders.py index d992050b..ab92e024 100644 --- a/tests/domain/test_task_orders.py +++ b/tests/domain/test_task_orders.py @@ -2,6 +2,7 @@ import pytest from atst.domain.task_orders import TaskOrders, TaskOrderError from atst.domain.exceptions import UnauthorizedError +from atst.models.attachment import Attachment from tests.factories import ( TaskOrderFactory, @@ -26,10 +27,18 @@ def test_is_section_complete(): def test_all_sections_complete(): task_order = TaskOrderFactory.create() + attachment = Attachment( + filename="sample_attachment", + object_name="sample", + resource="task_order", + resource_id=task_order.id, + ) + + custom_attrs = {"csp_estimate": attachment} for attr_list in TaskOrders.SECTIONS.values(): for attr in attr_list: if not getattr(task_order, attr): - setattr(task_order, attr, "str12345") + setattr(task_order, attr, custom_attrs.get(attr, "str12345")) task_order.scope = None assert not TaskOrders.all_sections_complete(task_order) diff --git a/tests/routes/task_orders/test_new_task_order.py b/tests/routes/task_orders/test_new_task_order.py index 53374ed6..a576611e 100644 --- a/tests/routes/task_orders/test_new_task_order.py +++ b/tests/routes/task_orders/test_new_task_order.py @@ -2,6 +2,7 @@ import pytest from flask import url_for from atst.domain.task_orders import TaskOrders +from atst.models.attachment import Attachment from atst.routes.task_orders.new import ShowTaskOrderWorkflow, UpdateTaskOrderWorkflow from tests.factories import UserFactory, TaskOrderFactory, PortfolioFactory @@ -124,8 +125,11 @@ def test_task_order_form_shows_errors(client, user_session): def task_order(): user = UserFactory.create() portfolio = PortfolioFactory.create(owner=user) + attachment = Attachment(filename="sample_attachment", object_name="sample") - return TaskOrderFactory.create(creator=user, portfolio=portfolio) + return TaskOrderFactory.create( + creator=user, portfolio=portfolio, csp_estimate=attachment + ) def test_show_task_order(task_order): From f580f69d350c695da08d70930259107f77e39520 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Tue, 22 Jan 2019 11:47:54 -0500 Subject: [PATCH 08/13] Raise error if csp estimate is not of proper type --- atst/models/task_order.py | 2 ++ tests/factories.py | 10 +++++++ tests/models/test_task_order.py | 28 +++++++++++++++++++ .../routes/task_orders/test_new_task_order.py | 3 +- 4 files changed, 42 insertions(+), 1 deletion(-) diff --git a/atst/models/task_order.py b/atst/models/task_order.py index f16b9389..ac860129 100644 --- a/atst/models/task_order.py +++ b/atst/models/task_order.py @@ -86,6 +86,8 @@ class TaskOrder(Base, mixins.TimestampsMixin): self._csp_estimate = Attachment.attach( new_csp_estimate, "task_order", self.id ) + else: + raise TypeError("Could not set csp_estimate with invalid type") @property def is_submitted(self): diff --git a/tests/factories.py b/tests/factories.py index b456ee1d..f18e2332 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -7,6 +7,7 @@ import datetime from faker import Faker as _Faker from atst.forms import data +from atst.models.attachment import Attachment from atst.models.environment import Environment from atst.models.request import Request from atst.models.request_revision import RequestRevision @@ -375,6 +376,14 @@ class InvitationFactory(Base): expiration_time = Invitations.current_expiration_time() +class AttachmentFactory(Base): + class Meta: + model = Attachment + + filename = factory.Faker("domain_word") + object_name = factory.Faker("domain_word") + + class TaskOrderFactory(Base): class Meta: model = TaskOrder @@ -401,6 +410,7 @@ class TaskOrderFactory(Base): lambda *args: random_future_date(year_min=2, year_max=5) ) performance_length = random.randint(1, 24) + csp_estimate = factory.SubFactory(AttachmentFactory) ko_first_name = factory.Faker("first_name") ko_last_name = factory.Faker("last_name") diff --git a/tests/models/test_task_order.py b/tests/models/test_task_order.py index 9c0adf40..6cbfc082 100644 --- a/tests/models/test_task_order.py +++ b/tests/models/test_task_order.py @@ -1,6 +1,11 @@ +from werkzeug.datastructures import FileStorage +import pytest + +from atst.models.attachment import Attachment from atst.models.task_order import TaskOrder, Status from tests.factories import random_future_date, random_past_date +from tests.mocks import PDF_FILENAME class TestTaskOrderStatus: @@ -30,3 +35,26 @@ def test_is_submitted(): to = TaskOrder(number="42") assert to.is_submitted + + +class TestCSPEstimate: + def test_setting_estimate_with_attachment(self): + to = TaskOrder() + attachment = Attachment(filename="sample.pdf", object_name="sample") + to.csp_estimate = attachment + + assert to.attachment_id == attachment.id + + def test_setting_estimate_with_file_storage(self): + to = TaskOrder() + with open(PDF_FILENAME, "rb") as fp: + fs = FileStorage(fp, content_type="application/pdf") + to.csp_estimate = fs + + assert to.csp_estimate is not None + assert to.csp_estimate.filename == PDF_FILENAME + + def test_setting_estimate_with_invalid_object(self): + to = TaskOrder() + with pytest.raises(TypeError): + to.csp_estimate = "invalid" diff --git a/tests/routes/task_orders/test_new_task_order.py b/tests/routes/task_orders/test_new_task_order.py index a576611e..16a14c63 100644 --- a/tests/routes/task_orders/test_new_task_order.py +++ b/tests/routes/task_orders/test_new_task_order.py @@ -43,7 +43,7 @@ def serialize_dates(data): # TODO: this test will need to be more complicated when we add validation to # the forms -def test_create_new_task_order(client, user_session): +def test_create_new_task_order(client, user_session, pdf_upload): creator = UserFactory.create() user_session(creator) @@ -66,6 +66,7 @@ def test_create_new_task_order(client, user_session): funding_data = slice_data_for_section(task_order_data, "funding") funding_data = serialize_dates(funding_data) + funding_data["csp_estimate"] = pdf_upload response = client.post( response.headers["Location"], data=funding_data, follow_redirects=False ) From c155de0e843738e033ae88534d6a3044b1b1a8d0 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Tue, 22 Jan 2019 12:32:08 -0500 Subject: [PATCH 09/13] Add tests for downloading csp estimate --- atst/routes/task_orders/index.py | 1 + tests/routes/task_orders/test_index.py | 43 ++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/atst/routes/task_orders/index.py b/atst/routes/task_orders/index.py index 6a632cbb..6abf34fd 100644 --- a/atst/routes/task_orders/index.py +++ b/atst/routes/task_orders/index.py @@ -3,6 +3,7 @@ from flask import g, 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 diff --git a/tests/routes/task_orders/test_index.py b/tests/routes/task_orders/test_index.py index 9ca5ea64..45db3a82 100644 --- a/tests/routes/task_orders/test_index.py +++ b/tests/routes/task_orders/test_index.py @@ -28,3 +28,46 @@ def test_download_summary(client, user_session): for attr, val in task_order.to_dictionary().items(): assert attr in doc assert xml_translated(val) in doc + + +class TestDownloadCSPEstimate: + def setup(self): + self.user = UserFactory.create() + self.portfolio = PortfolioFactory.create(owner=self.user) + self.task_order = TaskOrderFactory.create( + creator=self.user, portfolio=self.portfolio + ) + + def test_successful_download(self, client, user_session, pdf_upload): + self.task_order.csp_estimate = pdf_upload + user_session(self.user) + response = client.get( + url_for( + "task_orders.download_csp_estimate", task_order_id=self.task_order.id + ) + ) + assert response.status_code == 200 + + pdf_upload.seek(0) + expected_contents = pdf_upload.read() + assert expected_contents == response.data + + def test_download_without_attachment(self, client, user_session): + self.task_order.attachment_id = None + user_session(self.user) + response = client.get( + url_for( + "task_orders.download_csp_estimate", task_order_id=self.task_order.id + ) + ) + assert response.status_code == 404 + + def test_download_with_wrong_user(self, client, user_session): + other_user = UserFactory.create() + user_session(other_user) + response = client.get( + url_for( + "task_orders.download_csp_estimate", task_order_id=self.task_order.id + ) + ) + assert response.status_code == 404 From 672c56232358c327304ea2070cee2078c50b0be9 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Tue, 22 Jan 2019 13:05:04 -0500 Subject: [PATCH 10/13] Allow removing a csp estimate from a task order --- atst/models/task_order.py | 2 ++ tests/models/test_task_order.py | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/atst/models/task_order.py b/atst/models/task_order.py index ac860129..d1d32206 100644 --- a/atst/models/task_order.py +++ b/atst/models/task_order.py @@ -86,6 +86,8 @@ class TaskOrder(Base, mixins.TimestampsMixin): self._csp_estimate = Attachment.attach( new_csp_estimate, "task_order", self.id ) + elif not new_csp_estimate and self._csp_estimate: + self._csp_estimate = None else: raise TypeError("Could not set csp_estimate with invalid type") diff --git a/tests/models/test_task_order.py b/tests/models/test_task_order.py index 6cbfc082..447ad258 100644 --- a/tests/models/test_task_order.py +++ b/tests/models/test_task_order.py @@ -58,3 +58,11 @@ class TestCSPEstimate: to = TaskOrder() with pytest.raises(TypeError): to.csp_estimate = "invalid" + + def test_removing_estimate(self): + attachment = Attachment(filename="sample.pdf", object_name="sample") + to = TaskOrder(csp_estimate=attachment) + assert to.csp_estimate is not None + + to.csp_estimate = "" + assert to.csp_estimate is None From d5342514a8eb5aa9dffa4f6a59d4e55751dcf3e2 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Tue, 22 Jan 2019 13:11:08 -0500 Subject: [PATCH 11/13] Don't show download link on review page if no estimate --- templates/task_orders/new/review.html | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/templates/task_orders/new/review.html b/templates/task_orders/new/review.html index 53089982..4acc09a5 100644 --- a/templates/task_orders/new/review.html +++ b/templates/task_orders/new/review.html @@ -113,7 +113,11 @@
{% call ReviewField(("task_orders.new.review.performance_period" | translate), task_order.performance_length, filter="translateDuration") %} -

{{ Icon('download') }} {{ "task_orders.new.review.usage_est_link"| translate }}

+ {% if task_order.csp_estimate %} +

+ {{ Icon('download') }} {{ "task_orders.new.review.usage_est_link"| translate }} +

+ {% endif %} {% endcall %}
From 2bea2af297f6e23aa9a5c2232def7f8a540ae6fd Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Tue, 22 Jan 2019 14:10:01 -0500 Subject: [PATCH 12/13] Show disabled button for download on task order review page --- templates/task_orders/new/review.html | 3 +++ translations.yaml | 1 + 2 files changed, 4 insertions(+) diff --git a/templates/task_orders/new/review.html b/templates/task_orders/new/review.html index 4acc09a5..840a5f10 100644 --- a/templates/task_orders/new/review.html +++ b/templates/task_orders/new/review.html @@ -117,6 +117,9 @@

{{ Icon('download') }} {{ "task_orders.new.review.usage_est_link"| translate }}

+ {% else %} + {{ Icon('download') }} {{ "task_orders.new.review.usage_est_link"| translate }} + {{ Icon('alert', classes='icon--red') }} {{ "task_orders.new.review.not_uploaded"| translate }} {% endif %} {% endcall %} diff --git a/translations.yaml b/translations.yaml index 1ab325b3..2cf3d4e7 100644 --- a/translations.yaml +++ b/translations.yaml @@ -437,6 +437,7 @@ task_orders: dod_id: 'DoD ID:' invited: Invited not_invited: Not Yet Invited + not_uploaded: Not Uploaded testing: example_string: Hello World example_with_variables: 'Hello, {name}!' From 7e4f6bf3eb5ded60e5eee889a48e6cf2f60fad80 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Tue, 22 Jan 2019 16:16:18 -0500 Subject: [PATCH 13/13] Limit types of files that can be selected when uploading proof --- atst/forms/task_order.py | 1 + 1 file changed, 1 insertion(+) diff --git a/atst/forms/task_order.py b/atst/forms/task_order.py index 83cf127c..5196e9d0 100644 --- a/atst/forms/task_order.py +++ b/atst/forms/task_order.py @@ -95,6 +95,7 @@ class FundingForm(CacheableForm): ["pdf", "png"], translate("forms.task_order.file_format_not_allowed") ) ], + render_kw={"accept": ".pdf,.png,application/pdf,image/png"}, ) clin_01 = IntegerField(translate("forms.task_order.clin_01_label")) clin_02 = IntegerField(translate("forms.task_order.clin_02_label"))