diff --git a/alembic/versions/1f690989e38e_add_pdf_to_task_order.py b/alembic/versions/1f690989e38e_add_pdf_to_task_order.py
new file mode 100644
index 00000000..c22926da
--- /dev/null
+++ b/alembic/versions/1f690989e38e_add_pdf_to_task_order.py
@@ -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 ###
diff --git a/atst/models/task_order.py b/atst/models/task_order.py
index 6ea523e2..170e3be4 100644
--- a/atst/models/task_order.py
+++ b/atst/models/task_order.py
@@ -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):
diff --git a/atst/utils/json.py b/atst/utils/json.py
index 4ce7bd8d..8e2a3217 100644
--- a/atst/utils/json.py
+++ b/atst/utils/json.py
@@ -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)
diff --git a/js/components/forms/funding.js b/js/components/forms/funding.js
index 20b25a2c..8e4497a6 100644
--- a/js/components/forms/funding.js
+++ b/js/components/forms/funding.js
@@ -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
},
diff --git a/js/components/upload_input.js b/js/components/upload_input.js
new file mode 100644
index 00000000..a9b31460
--- /dev/null
+++ b/js/components/upload_input.js
@@ -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
+ },
+ },
+}
diff --git a/js/index.js b/js/index.js
index f9673744..f2f82bc9 100644
--- a/js/index.js
+++ b/js/index.js
@@ -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,
},
diff --git a/templates/components/upload_input.html b/templates/components/upload_input.html
new file mode 100644
index 00000000..b6835484
--- /dev/null
+++ b/templates/components/upload_input.html
@@ -0,0 +1,24 @@
+{% macro UploadInput(field, show_label=False) -%}
+ Uploaded {{ field.data.filename }}