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 }}