Merge pull request #606 from dod-ccpo/upload-pdf-on-ko-review
Upload PDF on KO Review Page
This commit is contained in:
commit
31f8c0f381
36
alembic/versions/1f690989e38e_add_pdf_to_task_order.py
Normal file
36
alembic/versions/1f690989e38e_add_pdf_to_task_order.py
Normal file
@ -0,0 +1,36 @@
|
||||
"""Add PDF to Task Order
|
||||
|
||||
Revision ID: 1f690989e38e
|
||||
Revises: 0ff4c31c4d28
|
||||
Create Date: 2019-02-04 15:56:57.642156
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '1f690989e38e'
|
||||
down_revision = '0ff4c31c4d28'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('task_orders', sa.Column('pdf_attachment_id', postgresql.UUID(as_uuid=True), nullable=True))
|
||||
op.drop_constraint('task_orders_attachments_attachment_id', 'task_orders', type_='foreignkey')
|
||||
op.alter_column('task_orders', 'attachment_id', new_column_name='csp_attachment_id')
|
||||
op.create_foreign_key('task_orders_attachments_pdf_attachment_id', 'task_orders', 'attachments', ['pdf_attachment_id'], ['id'])
|
||||
op.create_foreign_key('task_orders_attachments_csp_attachment_id', 'task_orders', 'attachments', ['csp_attachment_id'], ['id'])
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_constraint('task_orders_attachments_csp_attachment_id', 'task_orders', type_='foreignkey')
|
||||
op.drop_constraint('task_orders_attachments_pdf_attachment_id', 'task_orders', type_='foreignkey')
|
||||
op.alter_column('task_orders', 'csp_attachment_id', new_column_name='attachment_id')
|
||||
op.create_foreign_key('task_orders_attachments_attachment_id', 'task_orders', 'attachments', ['attachment_id'], ['id'])
|
||||
op.drop_column('task_orders', 'pdf_attachment_id')
|
||||
# ### end Alembic commands ###
|
@ -51,8 +51,8 @@ class TaskOrder(Base, mixins.TimestampsMixin):
|
||||
start_date = Column(Date) # Period of Performance
|
||||
end_date = Column(Date)
|
||||
performance_length = Column(Integer)
|
||||
attachment_id = Column(ForeignKey("attachments.id"))
|
||||
_csp_estimate = relationship("Attachment")
|
||||
csp_attachment_id = Column(ForeignKey("attachments.id"))
|
||||
_csp_estimate = relationship("Attachment", foreign_keys=[csp_attachment_id])
|
||||
clin_01 = Column(Numeric(scale=2))
|
||||
clin_02 = Column(Numeric(scale=2))
|
||||
clin_03 = Column(Numeric(scale=2))
|
||||
@ -72,6 +72,8 @@ class TaskOrder(Base, mixins.TimestampsMixin):
|
||||
so_email = Column(String) # Email
|
||||
so_phone_number = Column(String) # Phone Number
|
||||
so_dod_id = Column(String) # DOD ID
|
||||
pdf_attachment_id = Column(ForeignKey("attachments.id"))
|
||||
_pdf = relationship("Attachment", foreign_keys=[pdf_attachment_id])
|
||||
number = Column(String, unique=True) # Task Order Number
|
||||
loa = Column(String) # Line of Accounting (LOA)
|
||||
custom_clauses = Column(String) # Custom Clauses
|
||||
@ -82,16 +84,25 @@ class TaskOrder(Base, mixins.TimestampsMixin):
|
||||
|
||||
@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
|
||||
elif new_csp_estimate:
|
||||
raise TypeError("Could not set csp_estimate with invalid type")
|
||||
self._csp_estimate = self._set_attachment(new_csp_estimate, "_csp_estimate")
|
||||
|
||||
@hybrid_property
|
||||
def pdf(self):
|
||||
return self._pdf
|
||||
|
||||
@pdf.setter
|
||||
def pdf(self, new_pdf):
|
||||
self._pdf = self._set_attachment(new_pdf, "_pdf")
|
||||
|
||||
def _set_attachment(self, new_attachment, attribute):
|
||||
if isinstance(new_attachment, Attachment):
|
||||
return new_attachment
|
||||
elif isinstance(new_attachment, FileStorage):
|
||||
return Attachment.attach(new_attachment, "task_order", self.id)
|
||||
elif not new_attachment and hasattr(self, attribute):
|
||||
return None
|
||||
else:
|
||||
raise TypeError("Could not set attachment with invalid type")
|
||||
|
||||
@property
|
||||
def is_submitted(self):
|
||||
|
@ -1,4 +1,5 @@
|
||||
from flask.json import JSONEncoder
|
||||
from werkzeug.datastructures import FileStorage
|
||||
from datetime import date
|
||||
from atst.models.attachment import Attachment
|
||||
|
||||
@ -7,6 +8,8 @@ class CustomJSONEncoder(JSONEncoder):
|
||||
def default(self, obj):
|
||||
if isinstance(obj, Attachment):
|
||||
return obj.filename
|
||||
if isinstance(obj, date):
|
||||
elif isinstance(obj, date):
|
||||
return obj.strftime("%Y-%m-%d")
|
||||
elif isinstance(obj, FileStorage):
|
||||
return obj.filename
|
||||
return JSONEncoder.default(self, obj)
|
||||
|
@ -4,6 +4,7 @@ import { conformToMask } from 'vue-text-mask'
|
||||
import FormMixin from '../../mixins/form'
|
||||
import textinput from '../text_input'
|
||||
import optionsinput from '../options_input'
|
||||
import uploadinput from '../upload_input'
|
||||
|
||||
export default {
|
||||
name: 'funding',
|
||||
@ -13,6 +14,7 @@ export default {
|
||||
components: {
|
||||
textinput,
|
||||
optionsinput,
|
||||
uploadinput,
|
||||
},
|
||||
|
||||
props: {
|
||||
@ -32,7 +34,6 @@ export default {
|
||||
clin_02 = 0,
|
||||
clin_03 = 0,
|
||||
clin_04 = 0,
|
||||
csp_estimate,
|
||||
} = this.initialData
|
||||
|
||||
return {
|
||||
@ -40,7 +41,6 @@ export default {
|
||||
clin_02,
|
||||
clin_03,
|
||||
clin_04,
|
||||
showUpload: !csp_estimate || this.uploadErrors.length > 0,
|
||||
}
|
||||
},
|
||||
|
||||
@ -63,9 +63,6 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
showUploadInput: function() {
|
||||
this.showUpload = true
|
||||
},
|
||||
updateBudget: function() {
|
||||
document.querySelector('#to-target').innerText = this.totalBudgetStr
|
||||
},
|
||||
|
41
js/components/upload_input.js
Normal file
41
js/components/upload_input.js
Normal file
@ -0,0 +1,41 @@
|
||||
import createNumberMask from 'text-mask-addons/dist/createNumberMask'
|
||||
import { conformToMask } from 'vue-text-mask'
|
||||
|
||||
import FormMixin from '../mixins/form'
|
||||
import textinput from './text_input'
|
||||
import optionsinput from './options_input'
|
||||
|
||||
export default {
|
||||
name: 'uploadinput',
|
||||
|
||||
mixins: [FormMixin],
|
||||
|
||||
components: {
|
||||
textinput,
|
||||
optionsinput,
|
||||
},
|
||||
|
||||
props: {
|
||||
initialData: {
|
||||
type: String,
|
||||
},
|
||||
uploadErrors: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
|
||||
data: function() {
|
||||
const pdf = this.initialData
|
||||
|
||||
return {
|
||||
showUpload: !pdf || this.uploadErrors.length > 0,
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
showUploadInput: function() {
|
||||
this.showUpload = true
|
||||
},
|
||||
},
|
||||
}
|
@ -20,6 +20,7 @@ import NewApplication from './components/forms/new_application'
|
||||
import EditEnvironmentRole from './components/forms/edit_environment_role'
|
||||
import EditApplicationRoles from './components/forms/edit_application_roles'
|
||||
import funding from './components/forms/funding'
|
||||
import uploadinput from './components/upload_input'
|
||||
import Modal from './mixins/modal'
|
||||
import selector from './components/selector'
|
||||
import BudgetChart from './components/charts/budget_chart'
|
||||
@ -64,6 +65,7 @@ const app = new Vue({
|
||||
RequestsList,
|
||||
ConfirmationPopover,
|
||||
funding,
|
||||
uploadinput,
|
||||
DateSelector,
|
||||
EditOfficerForm,
|
||||
},
|
||||
|
24
templates/components/upload_input.html
Normal file
24
templates/components/upload_input.html
Normal file
@ -0,0 +1,24 @@
|
||||
{% macro UploadInput(field, show_label=False) -%}
|
||||
<uploadinput inline-template v-bind:initial-data='{{ field.data | tojson }}' v-bind:upload-errors='{{ field.errors | list }}'>
|
||||
<div>
|
||||
<template v-if="showUpload">
|
||||
<div class="usa-input {% if field.errors %} usa-input--error {% endif %}">
|
||||
{% if show_label %}
|
||||
{{ field.label }}
|
||||
{% endif %}
|
||||
{{ field.description }}
|
||||
{{ field }}
|
||||
{% for error in field.errors %}
|
||||
<span class="usa-input__message">{{error}}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p>Uploaded {{ field.data.filename }}</p>
|
||||
<div>
|
||||
<button type="button" v-on:click="showUploadInput">Change</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</uploadinput>
|
||||
{%- endmacro %}
|
@ -9,6 +9,7 @@
|
||||
{% from "components/text_input.html" import TextInput %}
|
||||
{% from "components/alert.html" import Alert %}
|
||||
{% from "components/review_field.html" import ReviewField %}
|
||||
{% from "components/upload_input.html" import UploadInput %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
@ -16,7 +17,10 @@
|
||||
|
||||
{% include "fragments/flash.html" %}
|
||||
|
||||
{% block form_action %}
|
||||
<form method='POST' action="{{ url_for('portfolios.submit_ko_review', portfolio_id=portfolio.id, task_order_id=task_order.id, form=form) }}" autocomplete="off" enctype="multipart/form-data">
|
||||
{% endblock %}
|
||||
|
||||
{{ form.csrf_token }}
|
||||
|
||||
{% block form %}
|
||||
@ -60,11 +64,7 @@
|
||||
<div class="h2">{{ "task_orders.ko_review.task_order_information"| translate }}</div>
|
||||
|
||||
<div class="form__sub-fields">
|
||||
<div class="usa-input">
|
||||
<div class="usa-input__title">{{ form.pdf.label }}</div>
|
||||
{{ form.pdf.description }}
|
||||
{{ form.pdf }}
|
||||
</div>
|
||||
{{ UploadInput(form.pdf) }}
|
||||
{{ TextInput(form.number) }}
|
||||
{{ TextInput(form.loa) }}
|
||||
{{ TextInput(form.custom_clauses, paragraph=True) }}
|
||||
|
@ -11,6 +11,7 @@
|
||||
|
||||
{% block form %}
|
||||
|
||||
|
||||
<!-- App Info Section -->
|
||||
<h3 class="task-order-form__heading subheading">{{ "task_orders.new.app_info.basic_info_title"| translate }}</h3>
|
||||
{{ TextInput(form.portfolio_name, placeholder="The name of your office or organization", validation="portfolioName") }}
|
||||
|
@ -3,6 +3,7 @@
|
||||
{% from "components/text_input.html" import TextInput %}
|
||||
{% from "components/options_input.html" import OptionsInput %}
|
||||
{% from "components/date_input.html" import DateInput %}
|
||||
{% from "components/upload_input.html" import UploadInput %}
|
||||
|
||||
{% from "components/icon.html" import Icon %}
|
||||
|
||||
@ -32,22 +33,7 @@
|
||||
{{ Icon("link")}} Go to Cloud Service Provider’s estimate calculator
|
||||
</a></p>
|
||||
<p>{{ "task_orders.new.funding.estimate_usage_paragraph" | translate }}</p>
|
||||
<template v-if="showUpload">
|
||||
<div class="usa-input {% if form.csp_estimate.errors %} usa-input--error {% endif %}">
|
||||
{{ 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>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p>Uploaded {{ form.csp_estimate.data.filename }}</p>
|
||||
<div>
|
||||
<button type="button" v-on:click="showUploadInput">Change</button>
|
||||
</div>
|
||||
</template>
|
||||
{{ UploadInput(form.csp_estimate, show_label=True) }}
|
||||
|
||||
<hr>
|
||||
|
||||
|
@ -47,7 +47,7 @@ class TestCSPEstimate:
|
||||
attachment = Attachment(filename="sample.pdf", object_name="sample")
|
||||
to.csp_estimate = attachment
|
||||
|
||||
assert to.attachment_id == attachment.id
|
||||
assert to.csp_attachment_id == attachment.id
|
||||
|
||||
def test_setting_estimate_with_file_storage(self):
|
||||
to = TaskOrder()
|
||||
@ -77,3 +77,41 @@ class TestCSPEstimate:
|
||||
|
||||
to.csp_estimate = ""
|
||||
assert to.csp_estimate is None
|
||||
|
||||
|
||||
class TestPDF:
|
||||
def test_setting_pdf_with_attachment(self):
|
||||
to = TaskOrder()
|
||||
attachment = Attachment(filename="sample.pdf", object_name="sample")
|
||||
to.pdf = attachment
|
||||
|
||||
assert to.pdf_attachment_id == attachment.id
|
||||
|
||||
def test_setting_pdf_with_file_storage(self):
|
||||
to = TaskOrder()
|
||||
with open(PDF_FILENAME, "rb") as fp:
|
||||
fs = FileStorage(fp, content_type="application/pdf")
|
||||
to.pdf = fs
|
||||
|
||||
assert to.pdf is not None
|
||||
assert to.pdf.filename == PDF_FILENAME
|
||||
|
||||
def test_setting_pdf_with_invalid_object(self):
|
||||
to = TaskOrder()
|
||||
with pytest.raises(TypeError):
|
||||
to.pdf = "invalid"
|
||||
|
||||
def test_setting_pdf_with_empty_value(self):
|
||||
to = TaskOrder()
|
||||
assert to.pdf is None
|
||||
|
||||
to.pdf = ""
|
||||
assert to.pdf is None
|
||||
|
||||
def test_removing_pdf(self):
|
||||
attachment = Attachment(filename="sample.pdf", object_name="sample")
|
||||
to = TaskOrder(pdf=attachment)
|
||||
assert to.pdf is not None
|
||||
|
||||
to.pdf = ""
|
||||
assert to.pdf is None
|
||||
|
@ -251,3 +251,41 @@ def test_cor_redirected_to_build_page(client, user_session):
|
||||
url_for("task_orders.new", screen=1, task_order_id=task_order.id)
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_submit_completed_ko_review_page(client, user_session, pdf_upload):
|
||||
portfolio = PortfolioFactory.create()
|
||||
ko = UserFactory.create()
|
||||
PortfolioRoleFactory.create(
|
||||
role=Roles.get("officer"),
|
||||
portfolio=portfolio,
|
||||
user=ko,
|
||||
status=PortfolioStatus.ACTIVE,
|
||||
)
|
||||
task_order = TaskOrderFactory.create(portfolio=portfolio, contracting_officer=ko)
|
||||
user_session(ko)
|
||||
form_data = {
|
||||
"start_date": "02/10/2019",
|
||||
"end_date": "03/10/2019",
|
||||
"number": "1938745981",
|
||||
"loa": "0813458013405",
|
||||
"custom_clauses": "hi im a custom clause",
|
||||
"pdf": pdf_upload,
|
||||
}
|
||||
|
||||
response = client.post(
|
||||
url_for(
|
||||
"portfolios.ko_review",
|
||||
portfolio_id=portfolio.id,
|
||||
task_order_id=task_order.id,
|
||||
),
|
||||
data=form_data,
|
||||
)
|
||||
|
||||
assert task_order.pdf
|
||||
assert response.headers["Location"] == url_for(
|
||||
"portfolios.view_task_order",
|
||||
portfolio_id=portfolio.id,
|
||||
task_order_id=task_order.id,
|
||||
_external=True,
|
||||
)
|
||||
|
@ -53,7 +53,7 @@ class TestDownloadCSPEstimate:
|
||||
assert expected_contents == response.data
|
||||
|
||||
def test_download_without_attachment(self, client, user_session):
|
||||
self.task_order.attachment_id = None
|
||||
self.task_order.csp_attachment_id = None
|
||||
user_session(self.user)
|
||||
response = client.get(
|
||||
url_for(
|
||||
|
Loading…
x
Reference in New Issue
Block a user