Preemptively upload task order PDF
This commit is contained in:
parent
3cf0907d0d
commit
5d2b976e5f
32
alembic/versions/9c24c609878a_resource_attachments.py
Normal file
32
alembic/versions/9c24c609878a_resource_attachments.py
Normal 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 ###
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
)
|
||||||
|
@ -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)
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 %}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user