Preemptively upload task order PDF

This commit is contained in:
richard-dds 2018-10-19 10:57:18 -04:00
parent 3cf0907d0d
commit 5d2b976e5f
8 changed files with 217 additions and 32 deletions

View File

@ -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 ###

View File

@ -13,6 +13,11 @@ from .query import RequestsQuery
from .authorization import RequestsAuthorization 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): def create_revision_from_request_body(body):
body = {k: v for p in body.values() for k, v in p.items()} body = {k: v for p in body.values() for k, v in p.items()}
DATES = ["start_date", "date_latest_training"] DATES = ["start_date", "date_latest_training"]
@ -156,33 +161,51 @@ class Requests(object):
return Requests.status_count(RequestStatus.APPROVED) return Requests.status_count(RequestStatus.APPROVED)
@classmethod @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 = RequestsQuery.get_with_lock(request_id)
request_data = financial_data.copy() # request_data = financial_data.copy()
task_order_data = { # task_order_data = {
k: request_data.pop(k) # k: request_data.pop(k)
for (k, v) in financial_data.items() # for (k, v) in financial_data.items()
if k in TaskOrders.TASK_ORDER_DATA # if k in TaskOrders.TASK_ORDER_DATA
} # }
if task_order_data: # if task_order_data:
task_order_number = request_data.pop("task_order_number") # task_order_number = request_data.pop("task_order_number")
else: # else:
task_order_number = request_data.get("task_order_number") # task_order_number = request_data.get("task_order_number")
task_order_file = request_data.pop("task_order", None) # task_order_file = request_data.pop("task_order", None)
if isinstance(task_order_file, FileStorage): # if isinstance(task_order_file, FileStorage):
task_order_data["pdf"] = task_order_file # task_order_data["pdf"] = task_order_file
task_order = TaskOrders.get_or_create_task_order( # task_order = TaskOrders.get_or_create_task_order(
task_order_number, task_order_data # 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: if task_order:
request.task_order = 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 return request

View File

@ -67,3 +67,13 @@ class TaskOrders(object):
source=Source.MANUAL, source=Source.MANUAL,
pdf=attachment, 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
)

View File

@ -1,9 +1,12 @@
from sqlalchemy import Column, String 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 flask import current_app as app
from atst.models import Base, types, mixins from atst.models import Base, types, mixins
from atst.database import db from atst.database import db
from atst.uploader import UploadError from atst.uploader import UploadError
from atst.domain.exceptions import NotFoundError
class AttachmentError(Exception): class AttachmentError(Exception):
@ -16,20 +19,56 @@ class Attachment(Base, mixins.TimestampsMixin):
id = types.Id() id = types.Id()
filename = Column(String, nullable=False) filename = Column(String, nullable=False)
object_name = Column(String, unique=True, nullable=False) object_name = Column(String, unique=True, nullable=False)
resource = Column(String)
resource_id = Column(UUID(as_uuid=True), index=True)
@classmethod @classmethod
def attach(cls, fyle): def attach(cls, fyle, resource=None, resource_id=None):
try: try:
filename, object_name = app.uploader.upload(fyle) filename, object_name = app.uploader.upload(fyle)
except UploadError as e: except UploadError as e:
raise AttachmentError("Could not add attachment. " + str(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.add(attachment)
db.session.commit() db.session.commit()
return attachment 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): def __repr__(self):
return "<Attachment(name='{}', id='{}')>".format(self.filename, self.id) return "<Attachment(name='{}', id='{}')>".format(self.filename, self.id)

View File

@ -1,15 +1,18 @@
from flask import g, render_template, redirect, url_for from flask import g, render_template, redirect, url_for
from flask import request as http_request from flask import request as http_request
from werkzeug.datastructures import ImmutableMultiDict from werkzeug.datastructures import ImmutableMultiDict, FileStorage
from . import requests_bp from . import requests_bp
from atst.domain.requests import Requests from atst.domain.requests import Requests
from atst.forms.financial import FinancialForm, ExtendedFinancialForm from atst.forms.financial import FinancialForm, ExtendedFinancialForm
from atst.forms.exceptions import FormValidationError from atst.forms.exceptions import FormValidationError
from atst.domain.exceptions import NotFoundError
from atst.domain.requests.financial_verification import ( from atst.domain.requests.financial_verification import (
PENumberValidator, PENumberValidator,
TaskOrderNumberValidator, TaskOrderNumberValidator,
) )
from atst.models.attachment import Attachment
from atst.domain.task_orders import TaskOrders
class FinancialVerificationBase(object): class FinancialVerificationBase(object):
@ -28,10 +31,46 @@ class FinancialVerificationBase(object):
mdict = ImmutableMultiDict(formdata) if formdata is not None else None mdict = ImmutableMultiDict(formdata) if formdata is not None else None
if is_extended: 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) return ExtendedFinancialForm(formdata=mdict, data=existing_fv_data)
else: else:
return FinancialForm(formdata=mdict, data=existing_fv_data) 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): def _apply_pe_number_error(self, field):
suggestion = self.pe_validator.suggest_pe_id(field.data) suggestion = self.pe_validator.suggest_pe_id(field.data)
error_str = ( error_str = (
@ -56,7 +95,8 @@ class GetFinancialVerificationForm(FinancialVerificationBase):
self.is_extended = is_extended self.is_extended = is_extended
def execute(self): 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): class UpdateFinancialVerification(FinancialVerificationBase):
@ -94,9 +134,12 @@ class UpdateFinancialVerification(FinancialVerificationBase):
self._apply_task_order_number_error(form.task_order_number) self._apply_task_order_number_error(form.task_order_number)
should_submit = False should_submit = False
attachment = self._process_attachment(self.is_extended, form)
if should_update: if should_update:
task_order = self._try_create_task_order(form, attachment)
updated_request = Requests.update_financial_verification( updated_request = Requests.update_financial_verification(
self.request.id, form.data self.request.id, form.data, task_order=task_order
) )
if should_submit: if should_submit:
return Requests.submit_financial_verification(updated_request) return Requests.submit_financial_verification(updated_request)
@ -140,8 +183,10 @@ class SaveFinancialVerificationDraft(FinancialVerificationBase):
valid = False valid = False
self._apply_task_order_number_error(form.task_order_number) 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( updated_request = Requests.update_financial_verification(
self.request.id, form.data self.request.id, form.data, task_order=task_order
) )
if valid: if valid:
@ -205,7 +250,7 @@ def update_financial_verification(request_id):
@requests_bp.route("/requests/verify/<string:request_id>/draft", methods=["POST"]) @requests_bp.route("/requests/verify/<string:request_id>/draft", methods=["POST"])
def save_financial_verification_draft(request_id): def save_financial_verification_draft(request_id):
request = Requests.get(g.current_user, 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") is_extended = http_request.args.get("extended")
try: try:

View File

@ -25,7 +25,22 @@ export default {
} = this.initialData } = this.initialData
return { 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
} }
} }
} }

View File

@ -106,13 +106,21 @@
validation='dollars' validation='dollars'
) }} ) }}
<div class="usa-input usa-input--validation--anything {% if f.task_order.errors %} usa-input--error {% endif %}"> <template v-if="showTaskOrder">
<div class="usa-input {% if f.task_order.errors %} usa-input--error {% endif %}">
{{ f.task_order.label }} {{ f.task_order.label }}
{{ f.task_order }} {{ f.task_order }}
{% for error in f.task_order.errors %} {% for error in f.task_order.errors %}
<span class="usa-input__message">{{error}}</span> <span class="usa-input__message">{{error}}</span>
{% endfor %} {% endfor %}
</div> </div>
</template>
<template v-else>
<p>Uploaded {{ f.task_order.data }}.</p>
<div>
<button v-on:click="forceShowTaskOrder($event)">Change</button>
</div>
</template>
</fieldset> </fieldset>
{% endif %} {% endif %}

View File

@ -4,6 +4,7 @@ from flask import url_for
from atst.eda_client import MockEDAClient from atst.eda_client import MockEDAClient
from atst.routes.requests.financial_verification import ( from atst.routes.requests.financial_verification import (
GetFinancialVerificationForm,
UpdateFinancialVerification, UpdateFinancialVerification,
SaveFinancialVerificationDraft, SaveFinancialVerificationDraft,
) )
@ -213,6 +214,18 @@ def test_updated_request_has_pdf(fv_data, extended_financial_verification_data):
assert updated_request.task_order.pdf 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): def test_update_fv_route(client, user_session, fv_data):
user = UserFactory.create() user = UserFactory.create()
request = RequestFactory.create(creator=user) request = RequestFactory.create(creator=user)