From d4fd3fb262d60c1787ef84246345cb6ed91ad0c4 Mon Sep 17 00:00:00 2001 From: Montana Date: Thu, 24 Jan 2019 16:37:46 -0500 Subject: [PATCH 1/9] Fix merge conflicts --- atst/domain/authz.py | 9 ++++ atst/routes/task_orders/new.py | 2 + templates/components/datepicker.html | 50 ++++++++++++++++++++ templates/portfolios/task_orders/review.html | 7 ++- templates/task_orders/new/app_info.html | 12 +++++ 5 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 templates/components/datepicker.html diff --git a/atst/domain/authz.py b/atst/domain/authz.py index de3c2156..f0c44afe 100644 --- a/atst/domain/authz.py +++ b/atst/domain/authz.py @@ -36,6 +36,15 @@ class Authorization(object): def is_ccpo(cls, user): return user.atat_role.name == "ccpo" + @classmethod + def check_is_mo_or_cor(cls, user, task_order): + if ( + task_order.contracting_officer_representative != user + and task_order.creator != user + ): + message = "build Task Order {}".format(task_order.id) + raise UnauthorizedError(user, message) + @classmethod def check_is_ko(cls, user, task_order): if task_order.contracting_officer != user: diff --git a/atst/routes/task_orders/new.py b/atst/routes/task_orders/new.py index 8892ba05..62647ce0 100644 --- a/atst/routes/task_orders/new.py +++ b/atst/routes/task_orders/new.py @@ -261,6 +261,7 @@ def get_started(): @task_orders_bp.route("/portfolios//task_orders/new/") def new(screen, task_order_id=None, portfolio_id=None): workflow = ShowTaskOrderWorkflow(g.current_user, screen, task_order_id) + Authorization.check_is_mo_or_cor(g.current_user, task_order) return render_template( workflow.template, current=screen, @@ -283,6 +284,7 @@ def update(screen, task_order_id=None, portfolio_id=None): workflow = UpdateTaskOrderWorkflow( g.current_user, form_data, screen, task_order_id, portfolio_id ) + Authorization.check_is_mo_or_cor(g.current_user, task_order) if workflow.validate(): workflow.update() return redirect( diff --git a/templates/components/datepicker.html b/templates/components/datepicker.html new file mode 100644 index 00000000..c597aeb5 --- /dev/null +++ b/templates/components/datepicker.html @@ -0,0 +1,50 @@ +{% from "components/icon.html" import Icon %} + +{% macro DatePicker(field) -%} + + +
+ + + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ {{ Icon("ok", classes="icon--green") }} +
+ +
+
+ +{%- endmacro %} diff --git a/templates/portfolios/task_orders/review.html b/templates/portfolios/task_orders/review.html index 61b50e8c..73e43186 100644 --- a/templates/portfolios/task_orders/review.html +++ b/templates/portfolios/task_orders/review.html @@ -16,8 +16,11 @@ {% include "fragments/flash.html" %} -
- {{ form.csrf_token }} + {% block form_action %} + {% if task_order_id %} + + {% endif %} + {% endblock %} {% block form %} diff --git a/templates/task_orders/new/app_info.html b/templates/task_orders/new/app_info.html index a5cd23d7..fc819d93 100644 --- a/templates/task_orders/new/app_info.html +++ b/templates/task_orders/new/app_info.html @@ -4,6 +4,7 @@ {% from "components/options_input.html" import OptionsInput %} {% from "components/date_input.html" import DateInput %} {% from "components/multi_checkbox_input.html" import MultiCheckboxInput %} +{% from "components/datepicker.html" import DatePicker %} {% block heading %} {{ "task_orders.new.app_info.section_title"| translate }} @@ -11,6 +12,17 @@ {% block form %} +
+ + + For example: 04 28 1986 + + {{ DatePicker() }} +
+ +

{{ "task_orders.new.app_info.basic_info_title"| translate }}

{{ TextInput(form.portfolio_name, placeholder="The name of your office or organization", validation="portfolioName") }} From ce2b4b6ea12e0ea65479ee3da7afdbc987af009a Mon Sep 17 00:00:00 2001 From: Montana Date: Tue, 5 Feb 2019 09:06:23 -0500 Subject: [PATCH 2/9] Add pdf column and uploader --- .../1f690989e38e_add_pdf_to_task_order.py | 36 +++++++++++++ atst/domain/authz.py | 9 ---- atst/models/task_order.py | 21 +++++++- atst/routes/task_orders/new.py | 2 - js/components/upload.js | 42 ++++++++++++++++ js/index.js | 2 + templates/components/datepicker.html | 50 ------------------- templates/portfolios/task_orders/review.html | 33 +++++++++--- templates/task_orders/new/app_info.html | 11 ---- 9 files changed, 124 insertions(+), 82 deletions(-) create mode 100644 alembic/versions/1f690989e38e_add_pdf_to_task_order.py create mode 100644 js/components/upload.js delete mode 100644 templates/components/datepicker.html 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..a7d30c9a --- /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(None, 'task_orders', 'attachments', ['pdf_attachment_id'], ['id']) + op.create_foreign_key(None, 'task_orders', 'attachments', ['csp_attachment_id'], ['id']) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'task_orders', type_='foreignkey') + op.drop_constraint(None, '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/domain/authz.py b/atst/domain/authz.py index f0c44afe..de3c2156 100644 --- a/atst/domain/authz.py +++ b/atst/domain/authz.py @@ -36,15 +36,6 @@ class Authorization(object): def is_ccpo(cls, user): return user.atat_role.name == "ccpo" - @classmethod - def check_is_mo_or_cor(cls, user, task_order): - if ( - task_order.contracting_officer_representative != user - and task_order.creator != user - ): - message = "build Task Order {}".format(task_order.id) - raise UnauthorizedError(user, message) - @classmethod def check_is_ko(cls, user, task_order): if task_order.contracting_officer != user: diff --git a/atst/models/task_order.py b/atst/models/task_order.py index 6ea523e2..60cb2ac9 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 @@ -93,6 +95,21 @@ class TaskOrder(Base, mixins.TimestampsMixin): elif new_csp_estimate: raise TypeError("Could not set csp_estimate with invalid type") + @hybrid_property + def pdf(self): + return self._pdf + + @pdf.setter + def pdf(self, new_pdf): + if isinstance(new_pdf, Attachment): + self._pdf = new_pdf + elif isinstance(new_pdf, FileStorage): + self._pdf = Attachment.attach(new_pdf, "task_order", self.id) + elif not new_pdf and self._pdf: + self._pdf = None + elif new_pdf: + raise TypeError("Could not set pdf with invalid type") + @property def is_submitted(self): diff --git a/atst/routes/task_orders/new.py b/atst/routes/task_orders/new.py index 62647ce0..8892ba05 100644 --- a/atst/routes/task_orders/new.py +++ b/atst/routes/task_orders/new.py @@ -261,7 +261,6 @@ def get_started(): @task_orders_bp.route("/portfolios//task_orders/new/") def new(screen, task_order_id=None, portfolio_id=None): workflow = ShowTaskOrderWorkflow(g.current_user, screen, task_order_id) - Authorization.check_is_mo_or_cor(g.current_user, task_order) return render_template( workflow.template, current=screen, @@ -284,7 +283,6 @@ def update(screen, task_order_id=None, portfolio_id=None): workflow = UpdateTaskOrderWorkflow( g.current_user, form_data, screen, task_order_id, portfolio_id ) - Authorization.check_is_mo_or_cor(g.current_user, task_order) if workflow.validate(): workflow.update() return redirect( diff --git a/js/components/upload.js b/js/components/upload.js new file mode 100644 index 00000000..a53c1bb1 --- /dev/null +++ b/js/components/upload.js @@ -0,0 +1,42 @@ +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: 'upload', + + mixins: [FormMixin], + + components: { + textinput, + optionsinput, + }, + + props: { + initialData: { + type: Object, + default: () => ({}), + }, + 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..8fb30039 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 upload from './components/upload' 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, + upload, DateSelector, EditOfficerForm, }, diff --git a/templates/components/datepicker.html b/templates/components/datepicker.html deleted file mode 100644 index c597aeb5..00000000 --- a/templates/components/datepicker.html +++ /dev/null @@ -1,50 +0,0 @@ -{% from "components/icon.html" import Icon %} - -{% macro DatePicker(field) -%} - - -
- - - -
- - -
- -
- - -
- -
- - -
- -
- {{ Icon("ok", classes="icon--green") }} -
- -
-
- -{%- endmacro %} diff --git a/templates/portfolios/task_orders/review.html b/templates/portfolios/task_orders/review.html index 73e43186..2caa0167 100644 --- a/templates/portfolios/task_orders/review.html +++ b/templates/portfolios/task_orders/review.html @@ -17,11 +17,11 @@ {% include "fragments/flash.html" %} {% block form_action %} - {% if task_order_id %} - - {% endif %} + {% endblock %} + {{ form.csrf_token }} + {% block form %} {% set message = "task_orders.ko_review.submitted_by" | translate({"name": task_order.creator.full_name}) %} @@ -63,11 +63,28 @@
{{ "task_orders.ko_review.task_order_information"| translate }}
-
-
{{ form.pdf.label }}
- {{ form.pdf.description }} - {{ form.pdf }} -
+ + +
+ + +
+
+ {{ TextInput(form.number) }} {{ TextInput(form.loa) }} {{ TextInput(form.custom_clauses, paragraph=True) }} diff --git a/templates/task_orders/new/app_info.html b/templates/task_orders/new/app_info.html index fc819d93..ee8fe008 100644 --- a/templates/task_orders/new/app_info.html +++ b/templates/task_orders/new/app_info.html @@ -4,7 +4,6 @@ {% from "components/options_input.html" import OptionsInput %} {% from "components/date_input.html" import DateInput %} {% from "components/multi_checkbox_input.html" import MultiCheckboxInput %} -{% from "components/datepicker.html" import DatePicker %} {% block heading %} {{ "task_orders.new.app_info.section_title"| translate }} @@ -12,16 +11,6 @@ {% block form %} -
- - - For example: 04 28 1986 - - {{ DatePicker() }} -
-

{{ "task_orders.new.app_info.basic_info_title"| translate }}

From 9182b1078c2d37691d1dc5a5cebea8a0160091cd Mon Sep 17 00:00:00 2001 From: Montana Date: Wed, 6 Feb 2019 08:55:46 -0500 Subject: [PATCH 3/9] Refactor attachment setters and add tests --- atst/models/task_order.py | 33 ++++++++++----------- tests/models/test_task_order.py | 40 +++++++++++++++++++++++++- tests/routes/task_orders/test_index.py | 2 +- 3 files changed, 57 insertions(+), 18 deletions(-) diff --git a/atst/models/task_order.py b/atst/models/task_order.py index 60cb2ac9..119cf217 100644 --- a/atst/models/task_order.py +++ b/atst/models/task_order.py @@ -84,15 +84,7 @@ 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: + if not self._set_attachment_type(new_csp_estimate, "_csp_estimate"): raise TypeError("Could not set csp_estimate with invalid type") @hybrid_property @@ -101,15 +93,24 @@ class TaskOrder(Base, mixins.TimestampsMixin): @pdf.setter def pdf(self, new_pdf): - if isinstance(new_pdf, Attachment): - self._pdf = new_pdf - elif isinstance(new_pdf, FileStorage): - self._pdf = Attachment.attach(new_pdf, "task_order", self.id) - elif not new_pdf and self._pdf: - self._pdf = None - elif new_pdf: + if not self._set_attachment_type(new_pdf, "_pdf"): raise TypeError("Could not set pdf with invalid type") + def _set_attachment_type(self, new_attachment, property): + if isinstance(new_attachment, Attachment): + setattr(self, property, new_attachment) + return True + elif isinstance(new_attachment, FileStorage): + setattr( + self, property, Attachment.attach(new_attachment, "task_order", self.id) + ) + return True + elif not new_attachment and hasattr(self, property): + setattr(self, property, None) + return True + else: + return False + @property def is_submitted(self): diff --git a/tests/models/test_task_order.py b/tests/models/test_task_order.py index 560c6989..908daec2 100644 --- a/tests/models/test_task_order.py +++ b/tests/models/test_task_order.py @@ -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 diff --git a/tests/routes/task_orders/test_index.py b/tests/routes/task_orders/test_index.py index 45db3a82..784e4488 100644 --- a/tests/routes/task_orders/test_index.py +++ b/tests/routes/task_orders/test_index.py @@ -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( From fb95033dbe91218af7ebfd5a72945cf9e26f9bd1 Mon Sep 17 00:00:00 2001 From: Montana Date: Wed, 6 Feb 2019 11:07:43 -0500 Subject: [PATCH 4/9] Only set attrs in the setter --- atst/models/task_order.py | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/atst/models/task_order.py b/atst/models/task_order.py index 119cf217..e03cbdd9 100644 --- a/atst/models/task_order.py +++ b/atst/models/task_order.py @@ -84,8 +84,7 @@ class TaskOrder(Base, mixins.TimestampsMixin): @csp_estimate.setter def csp_estimate(self, new_csp_estimate): - if not self._set_attachment_type(new_csp_estimate, "_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): @@ -93,23 +92,17 @@ class TaskOrder(Base, mixins.TimestampsMixin): @pdf.setter def pdf(self, new_pdf): - if not self._set_attachment_type(new_pdf, "_pdf"): - raise TypeError("Could not set pdf with invalid type") + self._pdf = self._set_attachment(new_pdf, "_pdf") - def _set_attachment_type(self, new_attachment, property): + def _set_attachment(self, new_attachment, property): if isinstance(new_attachment, Attachment): - setattr(self, property, new_attachment) - return True + return new_attachment elif isinstance(new_attachment, FileStorage): - setattr( - self, property, Attachment.attach(new_attachment, "task_order", self.id) - ) - return True + return Attachment.attach(new_attachment, "task_order", self.id) elif not new_attachment and hasattr(self, property): - setattr(self, property, None) - return True + return None else: - return False + raise TypeError("Could not set attachment with invalid type") @property def is_submitted(self): From a6eab76ab79004d453c36f14eb1444c75afba376 Mon Sep 17 00:00:00 2001 From: Montana Date: Wed, 6 Feb 2019 13:38:27 -0500 Subject: [PATCH 5/9] Rename --- atst/models/task_order.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/atst/models/task_order.py b/atst/models/task_order.py index e03cbdd9..170e3be4 100644 --- a/atst/models/task_order.py +++ b/atst/models/task_order.py @@ -94,12 +94,12 @@ class TaskOrder(Base, mixins.TimestampsMixin): def pdf(self, new_pdf): self._pdf = self._set_attachment(new_pdf, "_pdf") - def _set_attachment(self, new_attachment, property): + 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, property): + elif not new_attachment and hasattr(self, attribute): return None else: raise TypeError("Could not set attachment with invalid type") From 193d128d7f004cfc8e44e040571ed48b56242a88 Mon Sep 17 00:00:00 2001 From: Montana Date: Thu, 7 Feb 2019 11:31:52 -0500 Subject: [PATCH 6/9] Update JSONEncoder to accept FileStorage objects --- atst/utils/json.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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) From eaa5c939220669a15aadee7bb68233058913b036 Mon Sep 17 00:00:00 2001 From: Montana Date: Thu, 7 Feb 2019 13:12:30 -0500 Subject: [PATCH 7/9] Create upload macro --- js/components/forms/funding.js | 7 ++---- js/components/{upload.js => upload_input.js} | 7 +++--- js/index.js | 4 ++-- templates/components/upload_input.html | 24 ++++++++++++++++++++ templates/portfolios/task_orders/review.html | 24 ++------------------ templates/task_orders/new/funding.html | 18 ++------------- 6 files changed, 35 insertions(+), 49 deletions(-) rename js/components/{upload.js => upload_input.js} (86%) create mode 100644 templates/components/upload_input.html 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.js b/js/components/upload_input.js similarity index 86% rename from js/components/upload.js rename to js/components/upload_input.js index a53c1bb1..a9b31460 100644 --- a/js/components/upload.js +++ b/js/components/upload_input.js @@ -6,7 +6,7 @@ import textinput from './text_input' import optionsinput from './options_input' export default { - name: 'upload', + name: 'uploadinput', mixins: [FormMixin], @@ -17,8 +17,7 @@ export default { props: { initialData: { - type: Object, - default: () => ({}), + type: String, }, uploadErrors: { type: Array, @@ -27,7 +26,7 @@ export default { }, data: function() { - const { pdf } = this.initialData + const pdf = this.initialData return { showUpload: !pdf || this.uploadErrors.length > 0, diff --git a/js/index.js b/js/index.js index 8fb30039..f2f82bc9 100644 --- a/js/index.js +++ b/js/index.js @@ -20,7 +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 upload from './components/upload' +import uploadinput from './components/upload_input' import Modal from './mixins/modal' import selector from './components/selector' import BudgetChart from './components/charts/budget_chart' @@ -65,7 +65,7 @@ const app = new Vue({ RequestsList, ConfirmationPopover, funding, - upload, + 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) -%} + +
+ + +
+
+{%- endmacro %} diff --git a/templates/portfolios/task_orders/review.html b/templates/portfolios/task_orders/review.html index 2caa0167..e19054d4 100644 --- a/templates/portfolios/task_orders/review.html +++ b/templates/portfolios/task_orders/review.html @@ -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 %} @@ -63,28 +64,7 @@
{{ "task_orders.ko_review.task_order_information"| translate }}
- - -
- - -
-
- + {{ UploadInput(form.pdf) }} {{ TextInput(form.number) }} {{ TextInput(form.loa) }} {{ TextInput(form.custom_clauses, paragraph=True) }} diff --git a/templates/task_orders/new/funding.html b/templates/task_orders/new/funding.html index cc012474..cced51f7 100644 --- a/templates/task_orders/new/funding.html +++ b/templates/task_orders/new/funding.html @@ -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

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

- - + {{ UploadInput(form.csp_estimate, show_label=True) }}
From b5e6e67212d1d4496892b36d2d976dea2a47df9c Mon Sep 17 00:00:00 2001 From: Montana Date: Thu, 7 Feb 2019 13:24:02 -0500 Subject: [PATCH 8/9] Add names to foreign keys on latest migration --- alembic/versions/1f690989e38e_add_pdf_to_task_order.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/alembic/versions/1f690989e38e_add_pdf_to_task_order.py b/alembic/versions/1f690989e38e_add_pdf_to_task_order.py index a7d30c9a..c22926da 100644 --- a/alembic/versions/1f690989e38e_add_pdf_to_task_order.py +++ b/alembic/versions/1f690989e38e_add_pdf_to_task_order.py @@ -21,15 +21,15 @@ def upgrade(): 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(None, 'task_orders', 'attachments', ['pdf_attachment_id'], ['id']) - op.create_foreign_key(None, 'task_orders', 'attachments', ['csp_attachment_id'], ['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(None, 'task_orders', type_='foreignkey') - op.drop_constraint(None, 'task_orders', type_='foreignkey') + 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') From d45f8e871b1166db02cd5452ffdf682b34700368 Mon Sep 17 00:00:00 2001 From: Montana Date: Thu, 7 Feb 2019 13:58:20 -0500 Subject: [PATCH 9/9] Test for KO review submission --- tests/routes/portfolios/test_task_orders.py | 38 +++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/routes/portfolios/test_task_orders.py b/tests/routes/portfolios/test_task_orders.py index 6bb44b5e..5a66abd9 100644 --- a/tests/routes/portfolios/test_task_orders.py +++ b/tests/routes/portfolios/test_task_orders.py @@ -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, + )