From 5d2b976e5f9da3e9f0fd2ffa257bb76db27449d3 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Fri, 19 Oct 2018 10:57:18 -0400 Subject: [PATCH] Preemptively upload task order PDF --- .../9c24c609878a_resource_attachments.py | 32 +++++++++++ atst/domain/requests/requests.py | 57 +++++++++++++------ atst/domain/task_orders.py | 10 ++++ atst/models/attachment.py | 43 +++++++++++++- .../routes/requests/financial_verification.py | 55 ++++++++++++++++-- js/components/forms/financial.js | 17 +++++- .../requests/financial_verification.html | 22 ++++--- tests/routes/test_financial_verification.py | 13 +++++ 8 files changed, 217 insertions(+), 32 deletions(-) create mode 100644 alembic/versions/9c24c609878a_resource_attachments.py diff --git a/alembic/versions/9c24c609878a_resource_attachments.py b/alembic/versions/9c24c609878a_resource_attachments.py new file mode 100644 index 00000000..ecb19a91 --- /dev/null +++ b/alembic/versions/9c24c609878a_resource_attachments.py @@ -0,0 +1,32 @@ +"""resource attachments + +Revision ID: 9c24c609878a +Revises: 903d7c66ff1d +Create Date: 2018-10-18 17:14:25.566215 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = '9c24c609878a' +down_revision = '903d7c66ff1d' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('attachments', sa.Column('resource', sa.String(), nullable=True)) + op.add_column('attachments', sa.Column('resource_id', postgresql.UUID(as_uuid=True), nullable=True)) + op.create_index(op.f('ix_attachments_resource_id'), 'attachments', ['resource_id'], unique=False) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_attachments_resource_id'), table_name='attachments') + op.drop_column('attachments', 'resource_id') + op.drop_column('attachments', 'resource') + # ### end Alembic commands ### diff --git a/atst/domain/requests/requests.py b/atst/domain/requests/requests.py index db2c44c9..be38880e 100644 --- a/atst/domain/requests/requests.py +++ b/atst/domain/requests/requests.py @@ -13,6 +13,11 @@ from .query import RequestsQuery from .authorization import RequestsAuthorization +def pick(keys, d): + _keys = set(keys) + return {k: v for (k, v) in d.items() if k in _keys} + + def create_revision_from_request_body(body): body = {k: v for p in body.values() for k, v in p.items()} DATES = ["start_date", "date_latest_training"] @@ -156,33 +161,51 @@ class Requests(object): return Requests.status_count(RequestStatus.APPROVED) @classmethod - def update_financial_verification(cls, request_id, financial_data): + def update_financial_verification(cls, request_id, financial_data, task_order=None): request = RequestsQuery.get_with_lock(request_id) - request_data = financial_data.copy() - task_order_data = { - k: request_data.pop(k) - for (k, v) in financial_data.items() - if k in TaskOrders.TASK_ORDER_DATA - } + # request_data = financial_data.copy() + # task_order_data = { + # k: request_data.pop(k) + # for (k, v) in financial_data.items() + # if k in TaskOrders.TASK_ORDER_DATA + # } - if task_order_data: - task_order_number = request_data.pop("task_order_number") - else: - task_order_number = request_data.get("task_order_number") + # if task_order_data: + # task_order_number = request_data.pop("task_order_number") + # else: + # task_order_number = request_data.get("task_order_number") - task_order_file = request_data.pop("task_order", None) - if isinstance(task_order_file, FileStorage): - task_order_data["pdf"] = task_order_file + # task_order_file = request_data.pop("task_order", None) + # if isinstance(task_order_file, FileStorage): + # task_order_data["pdf"] = task_order_file - task_order = TaskOrders.get_or_create_task_order( - task_order_number, task_order_data + # task_order = TaskOrders.get_or_create_task_order( + # task_order_number, task_order_data + # ) + + delta = pick( + [ + "uii_ids", + "pe_id", + "treasury_code", + "ba_code", + "fname_co", + "lname_co", + "email_co", + "office_co", + "fname_cor", + "lname_cor", + "email_cor", + "office_cor", + ], + financial_data, ) if task_order: request.task_order = task_order - request = Requests.update(request.id, {"financial_verification": request_data}) + request = Requests.update(request.id, {"financial_verification": delta}) return request diff --git a/atst/domain/task_orders.py b/atst/domain/task_orders.py index 943447c2..afe4b6a0 100644 --- a/atst/domain/task_orders.py +++ b/atst/domain/task_orders.py @@ -67,3 +67,13 @@ class TaskOrders(object): source=Source.MANUAL, pdf=attachment, ) + + @classmethod + def get_or_create(cls, number, attachment=None, data=None): + try: + return TaskOrders.get(number) + except NotFoundError: + data = data or {} + return TaskOrders.create( + **data, number=number, pdf=attachment, source=Source.MANUAL + ) diff --git a/atst/models/attachment.py b/atst/models/attachment.py index 9838ea05..9a8dfcbf 100644 --- a/atst/models/attachment.py +++ b/atst/models/attachment.py @@ -1,9 +1,12 @@ from sqlalchemy import Column, String +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm.exc import NoResultFound from flask import current_app as app from atst.models import Base, types, mixins from atst.database import db from atst.uploader import UploadError +from atst.domain.exceptions import NotFoundError class AttachmentError(Exception): @@ -16,20 +19,56 @@ class Attachment(Base, mixins.TimestampsMixin): id = types.Id() filename = Column(String, nullable=False) object_name = Column(String, unique=True, nullable=False) + resource = Column(String) + resource_id = Column(UUID(as_uuid=True), index=True) @classmethod - def attach(cls, fyle): + def attach(cls, fyle, resource=None, resource_id=None): try: filename, object_name = app.uploader.upload(fyle) except UploadError as e: raise AttachmentError("Could not add attachment. " + str(e)) - attachment = Attachment(filename=filename, object_name=object_name) + attachment = Attachment( + filename=filename, + object_name=object_name, + resource=resource, + resource_id=resource_id, + ) db.session.add(attachment) db.session.commit() return attachment + @classmethod + def get(cls, id_): + try: + return db.session.query(Attachment).filter_by(id=id_).one() + except NoResultFound: + raise NotFoundError("attachment") + + @classmethod + def get_for_resource(cls, resource, resource_id): + try: + return ( + db.session.query(Attachment) + .filter_by(resource=resource, resource_id=resource_id) + .one() + ) + except NoResultFound: + raise NotFoundError("attachment") + + @classmethod + def delete_for_resource(cls, resource, resource_id): + try: + return ( + db.session.query(Attachment) + .filter_by(resource=resource, resource_id=resource_id) + .delete() + ) + except NoResultFound: + raise NotFoundError("attachment") + def __repr__(self): return "".format(self.filename, self.id) diff --git a/atst/routes/requests/financial_verification.py b/atst/routes/requests/financial_verification.py index 0c887393..dbafce03 100644 --- a/atst/routes/requests/financial_verification.py +++ b/atst/routes/requests/financial_verification.py @@ -1,15 +1,18 @@ from flask import g, render_template, redirect, url_for from flask import request as http_request -from werkzeug.datastructures import ImmutableMultiDict +from werkzeug.datastructures import ImmutableMultiDict, FileStorage from . import requests_bp from atst.domain.requests import Requests from atst.forms.financial import FinancialForm, ExtendedFinancialForm from atst.forms.exceptions import FormValidationError +from atst.domain.exceptions import NotFoundError from atst.domain.requests.financial_verification import ( PENumberValidator, TaskOrderNumberValidator, ) +from atst.models.attachment import Attachment +from atst.domain.task_orders import TaskOrders class FinancialVerificationBase(object): @@ -28,10 +31,46 @@ class FinancialVerificationBase(object): mdict = ImmutableMultiDict(formdata) if formdata is not None else None if is_extended: + try: + attachment = Attachment.get_for_resource("task_order", self.request.id) + existing_fv_data["task_order"] = attachment.filename + except NotFoundError: + pass + return ExtendedFinancialForm(formdata=mdict, data=existing_fv_data) else: return FinancialForm(formdata=mdict, data=existing_fv_data) + def _process_attachment(self, is_extended, form): + attachment = None + if self.is_extended: + attachment = None + if isinstance(form.task_order.data, FileStorage): + Attachment.delete_for_resource("task_order", self.request.id) + attachment = Attachment.attach( + form.task_order.data, "task_order", self.request.id + ) + elif isinstance(form.task_order.data, str): + attachment = Attachment.get_for_resource("task_order", self.request.id) + + if attachment: + form.task_order.data = attachment.id + + return attachment + + def _try_create_task_order(self, form, attachment): + form_data = form.data + + task_order_number = form_data.get("task_order_number") + if task_order_number: + task_order_data = { + k: v for (k, v) in form_data.items() if k in TaskOrders.TASK_ORDER_DATA + } + return TaskOrders.get_or_create( + task_order_number, attachment=attachment, data=task_order_data + ) + return None + def _apply_pe_number_error(self, field): suggestion = self.pe_validator.suggest_pe_id(field.data) error_str = ( @@ -56,7 +95,8 @@ class GetFinancialVerificationForm(FinancialVerificationBase): self.is_extended = is_extended def execute(self): - return self._get_form(self.request, self.is_extended) + form = self._get_form(self.request, self.is_extended) + return form class UpdateFinancialVerification(FinancialVerificationBase): @@ -94,9 +134,12 @@ class UpdateFinancialVerification(FinancialVerificationBase): self._apply_task_order_number_error(form.task_order_number) should_submit = False + attachment = self._process_attachment(self.is_extended, form) + if should_update: + task_order = self._try_create_task_order(form, attachment) updated_request = Requests.update_financial_verification( - self.request.id, form.data + self.request.id, form.data, task_order=task_order ) if should_submit: return Requests.submit_financial_verification(updated_request) @@ -140,8 +183,10 @@ class SaveFinancialVerificationDraft(FinancialVerificationBase): valid = False self._apply_task_order_number_error(form.task_order_number) + attachment = self._process_attachment(self.is_extended, form) + task_order = self._try_create_task_order(form, attachment) updated_request = Requests.update_financial_verification( - self.request.id, form.data + self.request.id, form.data, task_order=task_order ) if valid: @@ -205,7 +250,7 @@ def update_financial_verification(request_id): @requests_bp.route("/requests/verify//draft", methods=["POST"]) def save_financial_verification_draft(request_id): request = Requests.get(g.current_user, request_id) - fv_data = http_request.form + fv_data = {**http_request.form, **http_request.files} is_extended = http_request.args.get("extended") try: diff --git a/js/components/forms/financial.js b/js/components/forms/financial.js index c4c3b02d..febe8d47 100644 --- a/js/components/forms/financial.js +++ b/js/components/forms/financial.js @@ -25,7 +25,22 @@ export default { } = this.initialData return { - funding_type + funding_type, + shouldForceShowTaskOrder: false + } + }, + + computed: { + showTaskOrder: function() { + return !this.initialData.task_order || this.shouldForceShowTaskOrder + } + }, + + methods: { + forceShowTaskOrder: function(e) { + console.log("forceShowTaskOrder", e) + e.preventDefault() + this.shouldForceShowTaskOrder = true } } } diff --git a/templates/requests/financial_verification.html b/templates/requests/financial_verification.html index 5e8cd666..59a05c5d 100644 --- a/templates/requests/financial_verification.html +++ b/templates/requests/financial_verification.html @@ -106,13 +106,21 @@ validation='dollars' ) }} -
- {{ f.task_order.label }} - {{ f.task_order }} - {% for error in f.task_order.errors %} - {{error}} - {% endfor %} -
+ + {% endif %} diff --git a/tests/routes/test_financial_verification.py b/tests/routes/test_financial_verification.py index 554beec3..59f8f95c 100644 --- a/tests/routes/test_financial_verification.py +++ b/tests/routes/test_financial_verification.py @@ -4,6 +4,7 @@ from flask import url_for from atst.eda_client import MockEDAClient from atst.routes.requests.financial_verification import ( + GetFinancialVerificationForm, UpdateFinancialVerification, SaveFinancialVerificationDraft, ) @@ -213,6 +214,18 @@ def test_updated_request_has_pdf(fv_data, extended_financial_verification_data): assert updated_request.task_order.pdf +def test_can_save_draft_with_just_pdf(extended_financial_verification_data): + request = RequestFactory.create() + user = UserFactory.create() + data = {"task_order": extended_financial_verification_data["task_order"]} + SaveFinancialVerificationDraft( + TrueValidator, TrueValidator, user, request, data, is_extended=True + ).execute() + + form = GetFinancialVerificationForm(user, request, is_extended=True).execute() + assert form.task_order + + def test_update_fv_route(client, user_session, fv_data): user = UserFactory.create() request = RequestFactory.create(creator=user)