Merge pull request #566 from dod-ccpo/upload-csp-estimate
Upload proof of CSP estimate
This commit is contained in:
commit
f553c9a9ea
@ -8,7 +8,7 @@ from atst.domain.exceptions import UploadError
|
|||||||
|
|
||||||
|
|
||||||
class FileProviderInterface:
|
class FileProviderInterface:
|
||||||
_PERMITTED_MIMETYPES = ["application/pdf"]
|
_PERMITTED_MIMETYPES = ["application/pdf", "image/png"]
|
||||||
|
|
||||||
def _enforce_mimetype(self, fyle):
|
def _enforce_mimetype(self, fyle):
|
||||||
# TODO: for hardening, we should probably use a better library for
|
# TODO: for hardening, we should probably use a better library for
|
||||||
@ -57,6 +57,7 @@ class RackspaceFileProvider(FileProviderInterface):
|
|||||||
object_name = uuid4().hex
|
object_name = uuid4().hex
|
||||||
with NamedTemporaryFile() as tempfile:
|
with NamedTemporaryFile() as tempfile:
|
||||||
tempfile.write(fyle.stream.read())
|
tempfile.write(fyle.stream.read())
|
||||||
|
tempfile.seek(0)
|
||||||
self.container.upload_object(
|
self.container.upload_object(
|
||||||
file_path=tempfile.name,
|
file_path=tempfile.name,
|
||||||
object_name=object_name,
|
object_name=object_name,
|
||||||
|
@ -25,7 +25,7 @@ class TaskOrders(object):
|
|||||||
],
|
],
|
||||||
"funding": [
|
"funding": [
|
||||||
"performance_length",
|
"performance_length",
|
||||||
# "pdf",
|
"csp_estimate",
|
||||||
"clin_01",
|
"clin_01",
|
||||||
"clin_02",
|
"clin_02",
|
||||||
"clin_03",
|
"clin_03",
|
||||||
|
@ -54,12 +54,11 @@ def mixedContentToJson(value):
|
|||||||
This coerces the file upload in form data to its filename
|
This coerces the file upload in form data to its filename
|
||||||
so that the data can be JSON serialized.
|
so that the data can be JSON serialized.
|
||||||
"""
|
"""
|
||||||
if (
|
if isinstance(value, dict):
|
||||||
isinstance(value, dict)
|
for k, v in value.items():
|
||||||
and "legacy_task_order" in value
|
if hasattr(v, "filename"):
|
||||||
and hasattr(value["legacy_task_order"]["pdf"], "filename")
|
value[k] = v.filename
|
||||||
):
|
|
||||||
value["legacy_task_order"]["pdf"] = value["legacy_task_order"]["pdf"].filename
|
|
||||||
return app.jinja_env.filters["tojson"](value)
|
return app.jinja_env.filters["tojson"](value)
|
||||||
|
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ from wtforms.fields import (
|
|||||||
from wtforms.fields.html5 import DateField, TelField
|
from wtforms.fields.html5 import DateField, TelField
|
||||||
from wtforms.widgets import ListWidget, CheckboxInput
|
from wtforms.widgets import ListWidget, CheckboxInput
|
||||||
from wtforms.validators import Length
|
from wtforms.validators import Length
|
||||||
|
from flask_wtf.file import FileAllowed
|
||||||
|
|
||||||
from atst.forms.validators import IsNumber, PhoneNumber, RequiredIf
|
from atst.forms.validators import IsNumber, PhoneNumber, RequiredIf
|
||||||
|
|
||||||
@ -86,9 +87,15 @@ class FundingForm(CacheableForm):
|
|||||||
end_date = DateField(
|
end_date = DateField(
|
||||||
translate("forms.task_order.end_date_label"), format="%m/%d/%Y"
|
translate("forms.task_order.end_date_label"), format="%m/%d/%Y"
|
||||||
)
|
)
|
||||||
pdf = FileField(
|
csp_estimate = FileField(
|
||||||
translate("forms.task_order.pdf_label"),
|
translate("forms.task_order.csp_estimate_label"),
|
||||||
description=translate("forms.task_order.pdf_description"),
|
description=translate("forms.task_order.csp_estimate_description"),
|
||||||
|
validators=[
|
||||||
|
FileAllowed(
|
||||||
|
["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_01 = IntegerField(translate("forms.task_order.clin_01_label"))
|
||||||
clin_02 = IntegerField(translate("forms.task_order.clin_02_label"))
|
clin_02 = IntegerField(translate("forms.task_order.clin_02_label"))
|
||||||
|
@ -2,10 +2,12 @@ from enum import Enum
|
|||||||
|
|
||||||
import pendulum
|
import pendulum
|
||||||
from sqlalchemy import Column, Numeric, String, ForeignKey, Date, Integer
|
from sqlalchemy import Column, Numeric, String, ForeignKey, Date, Integer
|
||||||
|
from sqlalchemy.ext.hybrid import hybrid_property
|
||||||
from sqlalchemy.types import ARRAY
|
from sqlalchemy.types import ARRAY
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
|
from werkzeug.datastructures import FileStorage
|
||||||
|
|
||||||
from atst.models import Base, types, mixins
|
from atst.models import Attachment, Base, types, mixins
|
||||||
|
|
||||||
|
|
||||||
class Status(Enum):
|
class Status(Enum):
|
||||||
@ -49,7 +51,7 @@ class TaskOrder(Base, mixins.TimestampsMixin):
|
|||||||
end_date = Column(Date)
|
end_date = Column(Date)
|
||||||
performance_length = Column(Integer)
|
performance_length = Column(Integer)
|
||||||
attachment_id = Column(ForeignKey("attachments.id"))
|
attachment_id = Column(ForeignKey("attachments.id"))
|
||||||
pdf = relationship("Attachment")
|
_csp_estimate = relationship("Attachment")
|
||||||
clin_01 = Column(Numeric(scale=2))
|
clin_01 = Column(Numeric(scale=2))
|
||||||
clin_02 = Column(Numeric(scale=2))
|
clin_02 = Column(Numeric(scale=2))
|
||||||
clin_03 = Column(Numeric(scale=2))
|
clin_03 = Column(Numeric(scale=2))
|
||||||
@ -72,6 +74,23 @@ class TaskOrder(Base, mixins.TimestampsMixin):
|
|||||||
number = Column(String, unique=True) # Task Order Number
|
number = Column(String, unique=True) # Task Order Number
|
||||||
loa = Column(ARRAY(String)) # Line of Accounting (LOA)
|
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
|
||||||
|
elif isinstance(new_csp_estimate, FileStorage):
|
||||||
|
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")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_submitted(self):
|
def is_submitted(self):
|
||||||
return self.number is not None
|
return self.number is not None
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from flask import g, Response
|
from flask import g, Response, current_app as app
|
||||||
|
|
||||||
from . import task_orders_bp
|
from . import task_orders_bp
|
||||||
from atst.domain.task_orders import TaskOrders
|
from atst.domain.task_orders import TaskOrders
|
||||||
|
from atst.domain.exceptions import NotFoundError
|
||||||
from atst.utils.docx import Docx
|
from atst.utils.docx import Docx
|
||||||
|
|
||||||
|
|
||||||
@ -17,3 +18,22 @@ def download_summary(task_order_id):
|
|||||||
headers={"Content-Disposition": "attachment; filename={}".format(filename)},
|
headers={"Content-Disposition": "attachment; filename={}".format(filename)},
|
||||||
mimetype="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
mimetype="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@task_orders_bp.route("/task_orders/csp_estimate/<task_order_id>")
|
||||||
|
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")
|
||||||
|
@ -272,8 +272,9 @@ def new(screen, task_order_id=None, portfolio_id=None):
|
|||||||
"/portfolios/<portfolio_id>/task_orders/new/<int:screen>", methods=["POST"]
|
"/portfolios/<portfolio_id>/task_orders/new/<int:screen>", methods=["POST"]
|
||||||
)
|
)
|
||||||
def update(screen, task_order_id=None, portfolio_id=None):
|
def update(screen, task_order_id=None, portfolio_id=None):
|
||||||
|
form_data = {**http_request.form, **http_request.files}
|
||||||
workflow = UpdateTaskOrderWorkflow(
|
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():
|
if workflow.validate():
|
||||||
|
@ -19,6 +19,10 @@ export default {
|
|||||||
initialData: {
|
initialData: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({})
|
default: () => ({})
|
||||||
|
},
|
||||||
|
uploadErrors: {
|
||||||
|
type: Array,
|
||||||
|
default: () => ([])
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -28,6 +32,7 @@ export default {
|
|||||||
clin_02 = 0,
|
clin_02 = 0,
|
||||||
clin_03 = 0,
|
clin_03 = 0,
|
||||||
clin_04 = 0,
|
clin_04 = 0,
|
||||||
|
csp_estimate,
|
||||||
} = this.initialData
|
} = this.initialData
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -35,6 +40,7 @@ export default {
|
|||||||
clin_02,
|
clin_02,
|
||||||
clin_03,
|
clin_03,
|
||||||
clin_04,
|
clin_04,
|
||||||
|
showUpload: !csp_estimate || this.uploadErrors.length > 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -57,6 +63,9 @@ export default {
|
|||||||
const mask = createNumberMask({ prefix: '$', allowDecimal: true })
|
const mask = createNumberMask({ prefix: '$', allowDecimal: true })
|
||||||
return conformToMask(intValue.toString(), mask).conformedValue
|
return conformToMask(intValue.toString(), mask).conformedValue
|
||||||
},
|
},
|
||||||
|
showUploadInput: function() {
|
||||||
|
this.showUpload = true
|
||||||
|
},
|
||||||
updateBudget: function() {
|
updateBudget: function() {
|
||||||
document.querySelector('#to-target').innerText = this.totalBudgetStr
|
document.querySelector('#to-target').innerText = this.totalBudgetStr
|
||||||
}
|
}
|
||||||
|
@ -137,7 +137,7 @@
|
|||||||
<div class="panel__content">
|
<div class="panel__content">
|
||||||
{{ DocumentLink(
|
{{ DocumentLink(
|
||||||
title="Cloud Services Estimate",
|
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(
|
{{ DocumentLink(
|
||||||
title="Market Research",
|
title="Market Research",
|
||||||
link_url="#") }}
|
link_url="#") }}
|
||||||
|
@ -10,9 +10,9 @@
|
|||||||
|
|
||||||
{% block form_action %}
|
{% block form_action %}
|
||||||
{% if task_order_id %}
|
{% if task_order_id %}
|
||||||
<form method='POST' action="{{ url_for('task_orders.new', screen=current, task_order_id=task_order_id) }}" autocomplete="off">
|
<form method='POST' action="{{ url_for('task_orders.new', screen=current, task_order_id=task_order_id) }}" autocomplete="off" enctype="multipart/form-data">
|
||||||
{% else %}
|
{% else %}
|
||||||
<form method='POST' action="{{ url_for('task_orders.update', screen=current, portfolio_id=portfolio_id) }}" autocomplete="off">
|
<form method='POST' action="{{ url_for('task_orders.update', screen=current, portfolio_id=portfolio_id) }}" autocomplete="off" enctype="multipart/form-data">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
@ -12,7 +12,11 @@
|
|||||||
|
|
||||||
{% block form %}
|
{% block form %}
|
||||||
|
|
||||||
<funding inline-template v-bind:initial-data='{{ form.data|tojson }}'>
|
<funding
|
||||||
|
inline-template
|
||||||
|
v-bind:initial-data='{{ form.data|mixedContentToJson }}'
|
||||||
|
v-bind:upload-errors='{{ form.csp_estimate.errors | list }}'
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<!-- Get Funding Section -->
|
<!-- Get Funding Section -->
|
||||||
<h3 class="subheading">{{ "task_orders.new.funding.performance_period_title" | translate }}</h3>
|
<h3 class="subheading">{{ "task_orders.new.funding.performance_period_title" | translate }}</h3>
|
||||||
@ -28,13 +32,22 @@
|
|||||||
{{ Icon("link")}} Cloud Service Provider's estimate calculator
|
{{ Icon("link")}} Cloud Service Provider's estimate calculator
|
||||||
</a></p>
|
</a></p>
|
||||||
<p>{{ "task_orders.new.funding.estimate_usage_paragraph" | translate }}</p>
|
<p>{{ "task_orders.new.funding.estimate_usage_paragraph" | translate }}</p>
|
||||||
<div class="usa-input">
|
<template v-if="showUpload">
|
||||||
<div class="usa-input__title">
|
<div class="usa-input {% if form.csp_estimate.errors %} usa-input--error {% endif %}">
|
||||||
{{ form.pdf.label }}
|
{{ form.csp_estimate.label }}
|
||||||
|
{{ form.csp_estimate.description }}
|
||||||
|
{{ form.csp_estimate }}
|
||||||
|
{% for error in form.csp_estimate.errors %}
|
||||||
|
<span class="usa-input__message">{{error}}</span>
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{{ form.pdf.description }}
|
</template>
|
||||||
<input type="file" disabled="disabled" />
|
<template v-else>
|
||||||
</div>
|
<p>Uploaded {{ form.csp_estimate.data.filename }}</p>
|
||||||
|
<div>
|
||||||
|
<button type="button" v-on:click="showUploadInput">Change</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
|
@ -113,7 +113,14 @@
|
|||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
{% call ReviewField(("task_orders.new.review.performance_period" | translate), task_order.performance_length, filter="translateDuration") %}
|
{% call ReviewField(("task_orders.new.review.performance_period" | translate), task_order.performance_length, filter="translateDuration") %}
|
||||||
<p><a href="#" class='icon-link icon-link--left' download>{{ Icon('download') }} {{ "task_orders.new.review.usage_est_link" | translate }}</a></p>
|
{% if task_order.csp_estimate %}
|
||||||
|
<p>
|
||||||
|
<a href="{{ url_for("task_orders.download_csp_estimate", task_order_id=task_order.id) }}" class='icon-link icon-link--left' download>{{ Icon('download') }} {{ "task_orders.new.review.usage_est_link"| translate }}</a>
|
||||||
|
</p>
|
||||||
|
{% else %}
|
||||||
|
<a href="{{ url_for("task_orders.download_csp_estimate", task_order_id=task_order.id) }}" class='icon-link icon-link--left icon-link--disabled' aria-disabled="true">{{ Icon('download') }} {{ "task_orders.new.review.usage_est_link"| translate }}</a>
|
||||||
|
{{ Icon('alert', classes='icon--red') }} <span class="task-order-invite-message not-sent">{{ "task_orders.new.review.not_uploaded"| translate }}</span>
|
||||||
|
{% endif %}
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
<div class="col col--grow">
|
<div class="col col--grow">
|
||||||
|
@ -44,3 +44,14 @@ def test_download(app, uploader, pdf_upload):
|
|||||||
stream = uploader.download("abc")
|
stream = uploader.download("abc")
|
||||||
stream_content = b"".join([b for b in stream])
|
stream_content = b"".join([b for b in stream])
|
||||||
assert pdf_content == stream_content
|
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
|
||||||
|
@ -2,6 +2,7 @@ import pytest
|
|||||||
|
|
||||||
from atst.domain.task_orders import TaskOrders, TaskOrderError
|
from atst.domain.task_orders import TaskOrders, TaskOrderError
|
||||||
from atst.domain.exceptions import UnauthorizedError
|
from atst.domain.exceptions import UnauthorizedError
|
||||||
|
from atst.models.attachment import Attachment
|
||||||
|
|
||||||
from tests.factories import (
|
from tests.factories import (
|
||||||
TaskOrderFactory,
|
TaskOrderFactory,
|
||||||
@ -26,10 +27,18 @@ def test_is_section_complete():
|
|||||||
|
|
||||||
def test_all_sections_complete():
|
def test_all_sections_complete():
|
||||||
task_order = TaskOrderFactory.create()
|
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_list in TaskOrders.SECTIONS.values():
|
||||||
for attr in attr_list:
|
for attr in attr_list:
|
||||||
if not getattr(task_order, attr):
|
if not getattr(task_order, attr):
|
||||||
setattr(task_order, attr, "str12345")
|
setattr(task_order, attr, custom_attrs.get(attr, "str12345"))
|
||||||
|
|
||||||
task_order.scope = None
|
task_order.scope = None
|
||||||
assert not TaskOrders.all_sections_complete(task_order)
|
assert not TaskOrders.all_sections_complete(task_order)
|
||||||
|
@ -7,6 +7,7 @@ import datetime
|
|||||||
from faker import Faker as _Faker
|
from faker import Faker as _Faker
|
||||||
|
|
||||||
from atst.forms import data
|
from atst.forms import data
|
||||||
|
from atst.models.attachment import Attachment
|
||||||
from atst.models.environment import Environment
|
from atst.models.environment import Environment
|
||||||
from atst.models.request import Request
|
from atst.models.request import Request
|
||||||
from atst.models.request_revision import RequestRevision
|
from atst.models.request_revision import RequestRevision
|
||||||
@ -375,6 +376,14 @@ class InvitationFactory(Base):
|
|||||||
expiration_time = Invitations.current_expiration_time()
|
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 TaskOrderFactory(Base):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = TaskOrder
|
model = TaskOrder
|
||||||
@ -401,6 +410,7 @@ class TaskOrderFactory(Base):
|
|||||||
lambda *args: random_future_date(year_min=2, year_max=5)
|
lambda *args: random_future_date(year_min=2, year_max=5)
|
||||||
)
|
)
|
||||||
performance_length = random.randint(1, 24)
|
performance_length = random.randint(1, 24)
|
||||||
|
csp_estimate = factory.SubFactory(AttachmentFactory)
|
||||||
|
|
||||||
ko_first_name = factory.Faker("first_name")
|
ko_first_name = factory.Faker("first_name")
|
||||||
ko_last_name = factory.Faker("last_name")
|
ko_last_name = factory.Faker("last_name")
|
||||||
|
BIN
tests/fixtures/sample.png
vendored
Normal file
BIN
tests/fixtures/sample.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
@ -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 atst.models.task_order import TaskOrder, Status
|
||||||
|
|
||||||
from tests.factories import random_future_date, random_past_date
|
from tests.factories import random_future_date, random_past_date
|
||||||
|
from tests.mocks import PDF_FILENAME
|
||||||
|
|
||||||
|
|
||||||
class TestTaskOrderStatus:
|
class TestTaskOrderStatus:
|
||||||
@ -30,3 +35,34 @@ def test_is_submitted():
|
|||||||
|
|
||||||
to = TaskOrder(number="42")
|
to = TaskOrder(number="42")
|
||||||
assert to.is_submitted
|
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"
|
||||||
|
|
||||||
|
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
|
||||||
|
@ -28,3 +28,46 @@ def test_download_summary(client, user_session):
|
|||||||
for attr, val in task_order.to_dictionary().items():
|
for attr, val in task_order.to_dictionary().items():
|
||||||
assert attr in doc
|
assert attr in doc
|
||||||
assert xml_translated(val) 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
|
||||||
|
@ -2,6 +2,7 @@ import pytest
|
|||||||
from flask import url_for
|
from flask import url_for
|
||||||
|
|
||||||
from atst.domain.task_orders import TaskOrders
|
from atst.domain.task_orders import TaskOrders
|
||||||
|
from atst.models.attachment import Attachment
|
||||||
from atst.routes.task_orders.new import ShowTaskOrderWorkflow, UpdateTaskOrderWorkflow
|
from atst.routes.task_orders.new import ShowTaskOrderWorkflow, UpdateTaskOrderWorkflow
|
||||||
|
|
||||||
from tests.factories import UserFactory, TaskOrderFactory, PortfolioFactory
|
from tests.factories import UserFactory, TaskOrderFactory, PortfolioFactory
|
||||||
@ -42,7 +43,7 @@ def serialize_dates(data):
|
|||||||
|
|
||||||
# TODO: this test will need to be more complicated when we add validation to
|
# TODO: this test will need to be more complicated when we add validation to
|
||||||
# the forms
|
# 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()
|
creator = UserFactory.create()
|
||||||
user_session(creator)
|
user_session(creator)
|
||||||
|
|
||||||
@ -65,6 +66,7 @@ def test_create_new_task_order(client, user_session):
|
|||||||
|
|
||||||
funding_data = slice_data_for_section(task_order_data, "funding")
|
funding_data = slice_data_for_section(task_order_data, "funding")
|
||||||
funding_data = serialize_dates(funding_data)
|
funding_data = serialize_dates(funding_data)
|
||||||
|
funding_data["csp_estimate"] = pdf_upload
|
||||||
response = client.post(
|
response = client.post(
|
||||||
response.headers["Location"], data=funding_data, follow_redirects=False
|
response.headers["Location"], data=funding_data, follow_redirects=False
|
||||||
)
|
)
|
||||||
@ -124,8 +126,11 @@ def test_task_order_form_shows_errors(client, user_session):
|
|||||||
def task_order():
|
def task_order():
|
||||||
user = UserFactory.create()
|
user = UserFactory.create()
|
||||||
portfolio = PortfolioFactory.create(owner=user)
|
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):
|
def test_show_task_order(task_order):
|
||||||
|
@ -214,8 +214,9 @@ forms:
|
|||||||
label: Period of Performance length
|
label: Period of Performance length
|
||||||
start_date_label: Start Date
|
start_date_label: Start Date
|
||||||
end_date_label: End Date
|
end_date_label: End Date
|
||||||
pdf_label: Upload a copy of your CSP Cost Estimate Research
|
csp_estimate_label: Upload a copy of your CSP Cost Estimate Research
|
||||||
pdf_description: Upload a PDF or screenshot of your usage estimate from the calculator.
|
csp_estimate_description: Upload a PDF or screenshot of your usage estimate from the calculator.
|
||||||
|
file_format_not_allowed: Only PDF or PNG files can be uploaded.
|
||||||
clin_01_label: 'CLIN 01 : Unclassified'
|
clin_01_label: 'CLIN 01 : Unclassified'
|
||||||
clin_02_label: 'CLIN 02: Classified'
|
clin_02_label: 'CLIN 02: Classified'
|
||||||
clin_03_label: 'CLIN 03: Unclassified'
|
clin_03_label: 'CLIN 03: Unclassified'
|
||||||
@ -436,6 +437,7 @@ task_orders:
|
|||||||
dod_id: 'DoD ID:'
|
dod_id: 'DoD ID:'
|
||||||
invited: Invited
|
invited: Invited
|
||||||
not_invited: Not Yet Invited
|
not_invited: Not Yet Invited
|
||||||
|
not_uploaded: Not Uploaded
|
||||||
testing:
|
testing:
|
||||||
example_string: Hello World
|
example_string: Hello World
|
||||||
example_with_variables: 'Hello, {name}!'
|
example_with_variables: 'Hello, {name}!'
|
||||||
|
Loading…
x
Reference in New Issue
Block a user