Pdf uploads
This commit is contained in:
dandds 2018-08-28 13:26:01 -04:00 committed by GitHub
commit 792114b063
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 605 additions and 48 deletions

3
.gitignore vendored
View File

@ -35,3 +35,6 @@ config/dev.ini
/crl /crl
/crl-tmp /crl-tmp
*.bk *.bk
# uploads
/uploads

View File

@ -18,6 +18,8 @@ flask-session = "*"
flask-wtf = "*" flask-wtf = "*"
pyopenssl = "*" pyopenssl = "*"
requests = "*" requests = "*"
apache-libcloud = "*"
lockfile = "*"
[dev-packages] [dev-packages]
bandit = "*" bandit = "*"

73
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "41ad134816dae388385cfb15105e0eca436b25791ec4fbf67a2b36c4ae8056bd" "sha256": "552b7ac6943559a1fc3be1c4e1c91f965cbfb97c115566051950450c7cd6f78b"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@ -24,6 +24,14 @@
"index": "pypi", "index": "pypi",
"version": "==1.0.0" "version": "==1.0.0"
}, },
"apache-libcloud": {
"hashes": [
"sha256:0e2eee3802163bd0605975ed1e284cafc23203919bfa80c0cc5d3cd2543aaf97",
"sha256:48d5d64790a5112cace1a8e28d228c3f1c5bd3ddbd986a5453172d2da19f47d5"
],
"index": "pypi",
"version": "==2.3.0"
},
"asn1crypto": { "asn1crypto": {
"hashes": [ "hashes": [
"sha256:2f1adbb7546ed199e3c90ef23ec95c5cf3585bac7d11fb7eb562a3fe89c64e87", "sha256:2f1adbb7546ed199e3c90ef23ec95c5cf3585bac7d11fb7eb562a3fe89c64e87",
@ -33,10 +41,10 @@
}, },
"certifi": { "certifi": {
"hashes": [ "hashes": [
"sha256:4c1d68a1408dd090d2f3a869aa94c3947cc1d967821d1ed303208c9f41f0f2f4", "sha256:376690d6f16d32f9d1fe8932551d80b23e9d393a8578c5633a2ed39a64861638",
"sha256:b6e8b28b2b7e771a41ecdd12d4d43262ecab52adebbafa42c77d6b57fb6ad3a4" "sha256:456048c7e371c089d0a77a5212fb37a2c2dce1e24146e3b7e0261736aaeaa22a"
], ],
"version": "==2018.8.13" "version": "==2018.8.24"
}, },
"cffi": { "cffi": {
"hashes": [ "hashes": [
@ -172,6 +180,14 @@
], ],
"version": "==2.10" "version": "==2.10"
}, },
"lockfile": {
"hashes": [
"sha256:6aed02de03cba24efabcd600b30540140634fc06cfa603822d508d5361e9f799",
"sha256:6c3cb24f344923d30b2785d5ad75182c8ea7ac1b6171b08657258ec7429d50fa"
],
"index": "pypi",
"version": "==0.12.2"
},
"mako": { "mako": {
"hashes": [ "hashes": [
"sha256:4e02fde57bd4abb5ec400181e4c314f56ac3e49ba4fb8b0d50bba18cb27d25ae" "sha256:4e02fde57bd4abb5ec400181e4c314f56ac3e49ba4fb8b0d50bba18cb27d25ae"
@ -271,7 +287,6 @@
"sha256:1d936da41ee06216d89fdc7ead1ee9a5da2811a8787515a976b646e110c3f622", "sha256:1d936da41ee06216d89fdc7ead1ee9a5da2811a8787515a976b646e110c3f622",
"sha256:e4ef42e82b0b493c5849eed98b5ab49d6767caf982127e9a33167f1153b36cc5" "sha256:e4ef42e82b0b493c5849eed98b5ab49d6767caf982127e9a33167f1153b36cc5"
], ],
"markers": "python_version != '3.0.*' and python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.1.*' and python_version != '3.3.*'",
"version": "==2018.5" "version": "==2018.5"
}, },
"redis": { "redis": {
@ -317,7 +332,6 @@
"sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf", "sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf",
"sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5" "sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5"
], ],
"markers": "python_version >= '2.6' and python_version != '3.2.*' and python_version != '3.0.*' and python_version < '4' and python_version != '3.1.*' and python_version != '3.3.*'",
"version": "==1.23" "version": "==1.23"
}, },
"webassets": { "webassets": {
@ -350,6 +364,14 @@
], ],
"version": "==1.4.3" "version": "==1.4.3"
}, },
"appnope": {
"hashes": [
"sha256:5b26757dc6f79a3b7dc9fab95359328d5747fcb2409d331ea66d0272b90ab2a0",
"sha256:8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71"
],
"markers": "sys_platform == 'darwin'",
"version": "==0.1.0"
},
"argh": { "argh": {
"hashes": [ "hashes": [
"sha256:a9b3aaa1904eeb78e32394cd46c6f37ac0fb4af6dc488daa58971bdc7d7fcaf3", "sha256:a9b3aaa1904eeb78e32394cd46c6f37ac0fb4af6dc488daa58971bdc7d7fcaf3",
@ -441,7 +463,6 @@
"sha256:ea7cfd3aeb1544732d08bd9cfba40c5b78e3a91e17b1a0698ab81bfc5554c628", "sha256:ea7cfd3aeb1544732d08bd9cfba40c5b78e3a91e17b1a0698ab81bfc5554c628",
"sha256:f6d67f04abfb2b4bea7afc7fa6c18cf4c523a67956e455668be9ae42bccc21ad" "sha256:f6d67f04abfb2b4bea7afc7fa6c18cf4c523a67956e455668be9ae42bccc21ad"
], ],
"markers": "python_version != '3.2.*' and python_version != '3.0.*' and python_version != '3.1.*' and python_version >= '2.7'",
"version": "==0.9.0" "version": "==0.9.0"
}, },
"flask": { "flask": {
@ -494,7 +515,6 @@
"sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8",
"sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497"
], ],
"markers": "python_version >= '2.7' and python_version != '3.2.*' and python_version != '3.1.*' and python_version != '3.0.*' and python_version != '3.3.*'",
"version": "==4.3.4" "version": "==4.3.4"
}, },
"itsdangerous": { "itsdangerous": {
@ -612,7 +632,6 @@
"sha256:6e3836e39f4d36ae72840833db137f7b7d35105079aee6ec4a62d9f80d594dd1", "sha256:6e3836e39f4d36ae72840833db137f7b7d35105079aee6ec4a62d9f80d594dd1",
"sha256:95eb8364a4708392bae89035f45341871286a333f749c3141c20573d2b3876e1" "sha256:95eb8364a4708392bae89035f45341871286a333f749c3141c20573d2b3876e1"
], ],
"markers": "python_version >= '2.7' and python_version != '3.2.*' and python_version != '3.1.*' and python_version != '3.0.*' and python_version != '3.3.*'",
"version": "==0.7.1" "version": "==0.7.1"
}, },
"prompt-toolkit": { "prompt-toolkit": {
@ -635,7 +654,6 @@
"sha256:3fd59af7435864e1a243790d322d763925431213b6b8529c6ca71081ace3bbf7", "sha256:3fd59af7435864e1a243790d322d763925431213b6b8529c6ca71081ace3bbf7",
"sha256:e31fb2767eb657cbde86c454f02e99cb846d3cd9d61b318525140214fdc0e98e" "sha256:e31fb2767eb657cbde86c454f02e99cb846d3cd9d61b318525140214fdc0e98e"
], ],
"markers": "python_version >= '2.7' and python_version != '3.2.*' and python_version != '3.1.*' and python_version != '3.0.*' and python_version != '3.3.*'",
"version": "==1.5.4" "version": "==1.5.4"
}, },
"pygments": { "pygments": {
@ -692,11 +710,15 @@
}, },
"pyyaml": { "pyyaml": {
"hashes": [ "hashes": [
"sha256:1cbc199009e78f92d9edf554be4fe40fb7b0bef71ba688602a00e97a51909110",
"sha256:254bf6fda2b7c651837acb2c718e213df29d531eebf00edb54743d10bcb694eb", "sha256:254bf6fda2b7c651837acb2c718e213df29d531eebf00edb54743d10bcb694eb",
"sha256:3108529b78577327d15eec243f0ff348a0640b0c3478d67ad7f5648f93bac3e2", "sha256:3108529b78577327d15eec243f0ff348a0640b0c3478d67ad7f5648f93bac3e2",
"sha256:3c17fb92c8ba2f525e4b5f7941d850e7a48c3a59b32d331e2502a3cdc6648e76", "sha256:3c17fb92c8ba2f525e4b5f7941d850e7a48c3a59b32d331e2502a3cdc6648e76",
"sha256:6f89b5c95e93945b597776163403d47af72d243f366bf4622ff08bdfd1c950b7",
"sha256:8d6d96001aa7f0a6a4a95e8143225b5d06e41b1131044913fecb8f85a125714b", "sha256:8d6d96001aa7f0a6a4a95e8143225b5d06e41b1131044913fecb8f85a125714b",
"sha256:c8a88edd93ee29ede719080b2be6cb2333dfee1dccba213b422a9c8e97f2967b" "sha256:be622cc81696e24d0836ba71f6272a2b5767669b0d79fdcf0295d51ac2e156c8",
"sha256:c8a88edd93ee29ede719080b2be6cb2333dfee1dccba213b422a9c8e97f2967b",
"sha256:f39411e380e2182ad33be039e8ee5770a5d9efe01a2bfb7ae58d9ba31c4a2a9d"
], ],
"version": "==4.2b4" "version": "==4.2b4"
}, },
@ -747,6 +769,35 @@
], ],
"version": "==4.3.2" "version": "==4.3.2"
}, },
"typed-ast": {
"hashes": [
"sha256:0948004fa228ae071054f5208840a1e88747a357ec1101c17217bfe99b299d58",
"sha256:10703d3cec8dcd9eef5a630a04056bbc898abc19bac5691612acba7d1325b66d",
"sha256:1f6c4bd0bdc0f14246fd41262df7dfc018d65bb05f6e16390b7ea26ca454a291",
"sha256:25d8feefe27eb0303b73545416b13d108c6067b846b543738a25ff304824ed9a",
"sha256:29464a177d56e4e055b5f7b629935af7f49c196be47528cc94e0a7bf83fbc2b9",
"sha256:2e214b72168ea0275efd6c884b114ab42e316de3ffa125b267e732ed2abda892",
"sha256:3e0d5e48e3a23e9a4d1a9f698e32a542a4a288c871d33ed8df1b092a40f3a0f9",
"sha256:519425deca5c2b2bdac49f77b2c5625781abbaf9a809d727d3a5596b30bb4ded",
"sha256:57fe287f0cdd9ceaf69e7b71a2e94a24b5d268b35df251a88fef5cc241bf73aa",
"sha256:668d0cec391d9aed1c6a388b0d5b97cd22e6073eaa5fbaa6d2946603b4871efe",
"sha256:68ba70684990f59497680ff90d18e756a47bf4863c604098f10de9716b2c0bdd",
"sha256:6de012d2b166fe7a4cdf505eee3aaa12192f7ba365beeefaca4ec10e31241a85",
"sha256:79b91ebe5a28d349b6d0d323023350133e927b4de5b651a8aa2db69c761420c6",
"sha256:8550177fa5d4c1f09b5e5f524411c44633c80ec69b24e0e98906dd761941ca46",
"sha256:898f818399cafcdb93cbbe15fc83a33d05f18e29fb498ddc09b0214cdfc7cd51",
"sha256:94b091dc0f19291adcb279a108f5d38de2430411068b219f41b343c03b28fb1f",
"sha256:a26863198902cda15ab4503991e8cf1ca874219e0118cbf07c126bce7c4db129",
"sha256:a8034021801bc0440f2e027c354b4eafd95891b573e12ff0418dec385c76785c",
"sha256:bc978ac17468fe868ee589c795d06777f75496b1ed576d308002c8a5756fb9ea",
"sha256:c05b41bc1deade9f90ddc5d988fe506208019ebba9f2578c622516fd201f5863",
"sha256:c9b060bd1e5a26ab6e8267fd46fc9e02b54eb15fffb16d112d4c7b1c12987559",
"sha256:edb04bdd45bfd76c8292c4d9654568efaedf76fe78eb246dde69bdb13b2dad87",
"sha256:f19f2a4f547505fe9072e15f6f4ae714af51b5a681a97f187971f50c283193b6"
],
"markers": "python_version < '3.7' and implementation_name == 'cpython'",
"version": "==1.1.0"
},
"watchdog": { "watchdog": {
"hashes": [ "hashes": [
"sha256:7e65882adb7746039b6f3876ee174952f8eaaa34491ba34333ddf1fe35de4162" "sha256:7e65882adb7746039b6f3876ee174952f8eaaa34491ba34333ddf1fe35de4162"

View File

@ -0,0 +1,30 @@
"""add task order association to attachments
Revision ID: 14cd800904bc
Revises: d7db8fd35b41
Create Date: 2018-08-24 11:28:30.894412
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '14cd800904bc'
down_revision = 'd7db8fd35b41'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('task_order', sa.Column('attachment_id', sa.Integer(), nullable=True))
op.create_foreign_key(None, 'task_order', 'attachments', ['attachment_id'], ['id'])
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'task_order', type_='foreignkey')
op.drop_column('task_order', 'attachment_id')
# ### end Alembic commands ###

View File

@ -0,0 +1,36 @@
"""add attachment table
Revision ID: d7db8fd35b41
Revises: 0845b2f0f401
Create Date: 2018-08-24 11:27:15.317181
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'd7db8fd35b41'
down_revision = '0845b2f0f401'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('attachments',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('filename', sa.String(), nullable=True),
sa.Column('object_name', sa.String(), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('object_name')
)
op.create_unique_constraint(None, 'task_order', ['number'])
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'task_order', type_='unique')
op.drop_table('attachments')
# ### end Alembic commands ###

View File

@ -19,6 +19,7 @@ from atst.routes.errors import make_error_pages
from atst.domain.authnid.crl import CRLCache from atst.domain.authnid.crl import CRLCache
from atst.domain.auth import apply_authentication from atst.domain.auth import apply_authentication
from atst.eda_client import MockEDAClient from atst.eda_client import MockEDAClient
from atst.uploader import Uploader
ENV = os.getenv("FLASK_ENV", "dev") ENV = os.getenv("FLASK_ENV", "dev")
@ -43,6 +44,7 @@ def make_app(config):
make_crl_validator(app) make_crl_validator(app)
register_filters(app) register_filters(app)
make_eda_client(app) make_eda_client(app)
make_upload_storage(app)
db.init_app(app) db.init_app(app)
csrf.init_app(app) csrf.init_app(app)
@ -143,3 +145,13 @@ def make_crl_validator(app):
def make_eda_client(app): def make_eda_client(app):
app.eda_client = MockEDAClient() app.eda_client = MockEDAClient()
def make_upload_storage(app):
uploader = Uploader(
provider=app.config.get("STORAGE_PROVIDER"),
container=app.config.get("STORAGE_CONTAINER"),
key=app.config.get("STORAGE_KEY"),
secret=app.config.get("STORAGE_SECRET"),
)
app.uploader = uploader

View File

@ -3,6 +3,7 @@ from sqlalchemy import exists, and_, exc
from sqlalchemy.sql import text from sqlalchemy.sql import text
from sqlalchemy.orm.exc import NoResultFound from sqlalchemy.orm.exc import NoResultFound
from sqlalchemy.orm.attributes import flag_modified from sqlalchemy.orm.attributes import flag_modified
from werkzeug.datastructures import FileStorage
from atst.models.request import Request from atst.models.request import Request
from atst.models.request_status_event import RequestStatusEvent, RequestStatus from atst.models.request_status_event import RequestStatusEvent, RequestStatus
@ -244,11 +245,17 @@ WHERE requests_with_status.status = :status
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")
if "task_order" in request_data and isinstance(
request_data["task_order"], FileStorage
):
task_order_data["pdf"] = request_data.pop("task_order")
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
) )

View File

@ -3,6 +3,7 @@ from flask import current_app as app
from atst.database import db from atst.database import db
from atst.models.task_order import TaskOrder, Source from atst.models.task_order import TaskOrder, Source
from atst.models.attachment import Attachment
from .exceptions import NotFoundError from .exceptions import NotFoundError
@ -53,6 +54,12 @@ class TaskOrders(object):
except NotFoundError: except NotFoundError:
if task_order_data: if task_order_data:
pdf_file = task_order_data.pop("pdf")
# should catch the error here
attachment = Attachment.attach(pdf_file)
return TaskOrders.create( return TaskOrders.create(
**task_order_data, number=number, source=Source.MANUAL **task_order_data,
number=number,
source=Source.MANUAL,
pdf=attachment,
) )

View File

@ -1,4 +1,6 @@
import re import re
from flask import current_app as app
from werkzeug.datastructures import FileStorage
def iconSvg(name): def iconSvg(name):
@ -31,9 +33,24 @@ def getOptionLabel(value, options):
return next(tup[1] for tup in options if tup[0] == value) return next(tup[1] for tup in options if tup[0] == value)
def mixedContentToJson(value):
"""
This coerces the file upload in form data to its filename
so that the data can be JSON serialized.
"""
if (
isinstance(value, dict)
and "task_order" in value
and isinstance(value["task_order"], FileStorage)
):
value["task_order"] = value["task_order"].filename
return app.jinja_env.filters["tojson"](value)
def register_filters(app): def register_filters(app):
app.jinja_env.filters["iconSvg"] = iconSvg app.jinja_env.filters["iconSvg"] = iconSvg
app.jinja_env.filters["dollars"] = dollars app.jinja_env.filters["dollars"] = dollars
app.jinja_env.filters["usPhone"] = usPhone app.jinja_env.filters["usPhone"] = usPhone
app.jinja_env.filters["readableInteger"] = readableInteger app.jinja_env.filters["readableInteger"] = readableInteger
app.jinja_env.filters["getOptionLabel"] = getOptionLabel app.jinja_env.filters["getOptionLabel"] = getOptionLabel
app.jinja_env.filters["mixedContentToJson"] = mixedContentToJson

View File

@ -1,7 +1,8 @@
import re import re
from wtforms.fields.html5 import EmailField from wtforms.fields.html5 import EmailField
from wtforms.fields import StringField from wtforms.fields import StringField, FileField
from wtforms.validators import Required, Email, Regexp from wtforms.validators import Required, Email, Regexp
from flask_wtf.file import FileAllowed
from atst.domain.exceptions import NotFoundError from atst.domain.exceptions import NotFoundError
from atst.domain.pe_numbers import PENumbers from atst.domain.pe_numbers import PENumbers
@ -214,3 +215,11 @@ class ExtendedFinancialForm(BaseFinancialForm):
description="Review your task order document, the amounts for each CLIN must match exactly here", description="Review your task order document, the amounts for each CLIN must match exactly here",
filters=[number_to_int], filters=[number_to_int],
) )
task_order = FileField(
"Upload a copy of your Task Order",
validators=[
FileAllowed(["pdf"], "Only PDF documents can be uploaded."),
Required(),
],
)

View File

@ -13,3 +13,4 @@ from .task_order import TaskOrder
from .workspace import Workspace from .workspace import Workspace
from .project import Project from .project import Project
from .environment import Environment from .environment import Environment
from .attachment import Attachment

32
atst/models/attachment.py Normal file
View File

@ -0,0 +1,32 @@
from sqlalchemy import Column, Integer, String
from flask import current_app as app
from atst.models import Base
from atst.database import db
from atst.uploader import UploadError
class AttachmentError(Exception):
pass
class Attachment(Base):
__tablename__ = "attachments"
id = Column(Integer, primary_key=True)
filename = Column(String)
object_name = Column(String, unique=True)
@classmethod
def attach(cls, fyle):
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)
db.session.add(attachment)
db.session.commit()
return attachment

View File

@ -1,6 +1,7 @@
from enum import Enum from enum import Enum
from sqlalchemy import Column, Integer, String, Enum as SQLAEnum from sqlalchemy import Column, Integer, String, ForeignKey, Enum as SQLAEnum
from sqlalchemy.orm import relationship
from atst.models import Base from atst.models import Base
@ -31,3 +32,6 @@ class TaskOrder(Base):
clin_1003 = Column(Integer) clin_1003 = Column(Integer)
clin_2001 = Column(Integer) clin_2001 = Column(Integer)
clin_2003 = Column(Integer) clin_2003 = Column(Integer)
attachment_id = Column(ForeignKey("attachments.id"))
pdf = relationship("Attachment")

View File

@ -30,7 +30,6 @@ def update_financial_verification(request_id):
post_data = http_request.form post_data = http_request.form
existing_request = Requests.get(request_id) existing_request = Requests.get(request_id)
form = financial_form(post_data) form = financial_form(post_data)
rerender_args = dict( rerender_args = dict(
request_id=request_id, f=form, extended=http_request.args.get("extended") request_id=request_id, f=form, extended=http_request.args.get("extended")
) )

45
atst/uploader.py Normal file
View File

@ -0,0 +1,45 @@
from uuid import uuid4
from libcloud.storage.types import Provider
from libcloud.storage.providers import get_driver
class UploadError(Exception):
pass
class Uploader:
_PERMITTED_MIMETYPES = ["application/pdf"]
def __init__(self, provider, container=None, key=None, secret=None):
self.container = self._get_container(provider, container, key, secret)
def upload(self, fyle):
# TODO: for hardening, we should probably use a better library for
# determining mimetype and not rely on FileUpload's determination
# TODO: we should set MAX_CONTENT_LENGTH in the config to prevent large
# uploads
if not fyle.mimetype in self._PERMITTED_MIMETYPES:
raise UploadError(
"could not upload {} with mimetype {}".format(
fyle.filename, fyle.mimetype
)
)
object_name = uuid4().hex
self.container.upload_object_via_stream(
iterator=fyle.stream.__iter__(),
object_name=object_name,
extra={"acl": "private"},
)
return (fyle.filename, object_name)
def download(self, path):
pass
def _get_container(self, provider, container, key, secret):
if provider == "LOCAL":
key = container
container = ""
driver = get_driver(getattr(Provider, provider))(key=key, secret=secret)
return driver.get_container(container)

View File

@ -20,4 +20,6 @@ SECRET_KEY = change_me_into_something_secret
SESSION_COOKIE_NAME=atat SESSION_COOKIE_NAME=atat
SESSION_TYPE = redis SESSION_TYPE = redis
SESSION_USE_SIGNER = True SESSION_USE_SIGNER = True
STORAGE_CONTAINER=uploads
STORAGE_PROVIDER=LOCAL
WTF_CSRF_ENABLED = true WTF_CSRF_ENABLED = true

View File

@ -5,6 +5,9 @@
source "$(dirname "${0}")"/../script/include/global_header.inc.sh source "$(dirname "${0}")"/../script/include/global_header.inc.sh
# create upload directory for app
mkdir uploads | true
# Enable database resetting # Enable database resetting
RESET_DB="true" RESET_DB="true"

View File

@ -6,6 +6,9 @@ source "$(dirname "${0}")"/../script/include/global_header.inc.sh
export FLASK_ENV=test export FLASK_ENV=test
# create upload directory for app
mkdir uploads | true
# Enable database resetting # Enable database resetting
RESET_DB="true" RESET_DB="true"

View File

@ -6,7 +6,7 @@
{% block content %} {% block content %}
<financial inline-template v-bind:initial-data='{{ f.data|tojson }}'> <financial inline-template v-bind:initial-data='{{ f.data|mixedContentToJson }}'>
<div class="col"> <div class="col">
{% if extended %} {% if extended %}
@ -38,7 +38,7 @@
{% block form_action %} {% block form_action %}
{% if extended %} {% if extended %}
<form method='POST' action="{{ url_for('requests.financial_verification', request_id=request_id, extended=True) }}" autocomplete="off"> <form method='POST' action="{{ url_for('requests.financial_verification', request_id=request_id, extended=True) }}" autocomplete="off" enctype="multipart/form-data">
{% else %} {% else %}
<form method='POST' action="{{ url_for('requests.financial_verification', request_id=request_id) }}" autocomplete="off"> <form method='POST' action="{{ url_for('requests.financial_verification', request_id=request_id) }}" autocomplete="off">
{% endif %} {% endif %}
@ -94,6 +94,14 @@
f.clin_2003,placeholder="7,000", f.clin_2003,placeholder="7,000",
validation='integer' validation='integer'
) }} ) }}
<div class="usa-input usa-input--validation--anything {% if f.task_order.errors %} usa-input--error {% endif %}">
{{ f.task_order.label }}
{{ f.task_order }}
{% for error in f.task_order.errors %}
<span class="usa-input__message">{{error}}</span>
{% endfor %}
</div>
</fieldset> </fieldset>
{% endif %} {% endif %}

View File

@ -3,17 +3,23 @@ import pytest
import alembic.config import alembic.config
import alembic.command import alembic.command
from logging.config import dictConfig from logging.config import dictConfig
from werkzeug.datastructures import FileStorage
from tempfile import TemporaryDirectory
from atst.app import make_app, make_config from atst.app import make_app, make_config
from atst.database import db as _db from atst.database import db as _db
import tests.factories as factories import tests.factories as factories
from tests.mocks import PDF_FILENAME
dictConfig({"version": 1, "handlers": {"wsgi": {"class": "logging.NullHandler"}}}) dictConfig({"version": 1, "handlers": {"wsgi": {"class": "logging.NullHandler"}}})
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def app(request): def app(request):
upload_dir = TemporaryDirectory()
config = make_config() config = make_config()
config.update({"STORAGE_CONTAINER": upload_dir.name})
_app = make_app(config) _app = make_app(config)
@ -22,6 +28,8 @@ def app(request):
yield _app yield _app
upload_dir.cleanup()
ctx.pop() ctx.pop()
@ -103,3 +111,24 @@ def user_session(monkeypatch, session):
) )
return set_user_session return set_user_session
@pytest.fixture
def pdf_upload():
with open(PDF_FILENAME, "rb") as fp:
yield FileStorage(fp, content_type="application/pdf")
@pytest.fixture
def extended_financial_verification_data(pdf_upload):
return {
"funding_type": "RDTE",
"funding_type_other": "other",
"clin_0001": "50000",
"clin_0003": "13000",
"clin_1001": "30000",
"clin_1003": "7000",
"clin_2001": "30000",
"clin_2003": "7000",
"task_order": pdf_upload,
}

View File

@ -121,25 +121,20 @@ request_financial_data = {
"treasury_code": "00123456", "treasury_code": "00123456",
"ba_code": "024A", "ba_code": "024A",
} }
task_order_financial_data = {
"funding_type": "RDTE",
"funding_type_other": "other",
"clin_0001": 50000,
"clin_0003": 13000,
"clin_1001": 30000,
"clin_1003": 7000,
"clin_2001": 30000,
"clin_2003": 7000,
}
def test_update_financial_verification_without_task_order(): def test_update_financial_verification_without_task_order(
extended_financial_verification_data
):
request = RequestFactory.create() request = RequestFactory.create()
financial_data = {**request_financial_data, **task_order_financial_data} financial_data = {**request_financial_data, **extended_financial_verification_data}
Requests.update_financial_verification(request.id, financial_data) Requests.update_financial_verification(request.id, financial_data)
assert request.task_order assert request.task_order
assert request.task_order.clin_0001 == task_order_financial_data["clin_0001"] assert request.task_order.clin_0001 == int(
extended_financial_verification_data["clin_0001"]
)
assert request.task_order.source == TaskOrderSource.MANUAL assert request.task_order.source == TaskOrderSource.MANUAL
assert request.task_order.pdf
def test_update_financial_verification_with_task_order(): def test_update_financial_verification_with_task_order():

View File

@ -36,3 +36,10 @@ def test_nonexistent_task_order_raises_with_client(monkeypatch):
) )
with pytest.raises(NotFoundError): with pytest.raises(NotFoundError):
TaskOrders.get("some other fake numer") TaskOrders.get("some other fake numer")
def test_create_attachment(extended_financial_verification_data):
task_order_data = extended_financial_verification_data.copy()
task_order_data["pdf"] = task_order_data.pop("task_order")
task_order = TaskOrders.get_or_create_task_order("abc123", task_order_data)
assert task_order.pdf

198
tests/fixtures/sample.pdf vendored Normal file
View File

@ -0,0 +1,198 @@
%PDF-1.3
%âãÏÓ
1 0 obj
<<
/Type /Catalog
/Outlines 2 0 R
/Pages 3 0 R
>>
endobj
2 0 obj
<<
/Type /Outlines
/Count 0
>>
endobj
3 0 obj
<<
/Type /Pages
/Count 2
/Kids [ 4 0 R 6 0 R ]
>>
endobj
4 0 obj
<<
/Type /Page
/Parent 3 0 R
/Resources <<
/Font <<
/F1 9 0 R
>>
/ProcSet 8 0 R
>>
/MediaBox [0 0 612.0000 792.0000]
/Contents 5 0 R
>>
endobj
5 0 obj
<< /Length 1074 >>
stream
2 J
BT
0 0 0 rg
/F1 0027 Tf
57.3750 722.2800 Td
( A Simple PDF File ) Tj
ET
BT
/F1 0010 Tf
69.2500 688.6080 Td
( This is a small demonstration .pdf file - ) Tj
ET
BT
/F1 0010 Tf
69.2500 664.7040 Td
( just for use in the Virtual Mechanics tutorials. More text. And more ) Tj
ET
BT
/F1 0010 Tf
69.2500 652.7520 Td
( text. And more text. And more text. And more text. ) Tj
ET
BT
/F1 0010 Tf
69.2500 628.8480 Td
( And more text. And more text. And more text. And more text. And more ) Tj
ET
BT
/F1 0010 Tf
69.2500 616.8960 Td
( text. And more text. Boring, zzzzz. And more text. And more text. And ) Tj
ET
BT
/F1 0010 Tf
69.2500 604.9440 Td
( more text. And more text. And more text. And more text. And more text. ) Tj
ET
BT
/F1 0010 Tf
69.2500 592.9920 Td
( And more text. And more text. ) Tj
ET
BT
/F1 0010 Tf
69.2500 569.0880 Td
( And more text. And more text. And more text. And more text. And more ) Tj
ET
BT
/F1 0010 Tf
69.2500 557.1360 Td
( text. And more text. And more text. Even more. Continued on page 2 ...) Tj
ET
endstream
endobj
6 0 obj
<<
/Type /Page
/Parent 3 0 R
/Resources <<
/Font <<
/F1 9 0 R
>>
/ProcSet 8 0 R
>>
/MediaBox [0 0 612.0000 792.0000]
/Contents 7 0 R
>>
endobj
7 0 obj
<< /Length 676 >>
stream
2 J
BT
0 0 0 rg
/F1 0027 Tf
57.3750 722.2800 Td
( Simple PDF File 2 ) Tj
ET
BT
/F1 0010 Tf
69.2500 688.6080 Td
( ...continued from page 1. Yet more text. And more text. And more text. ) Tj
ET
BT
/F1 0010 Tf
69.2500 676.6560 Td
( And more text. And more text. And more text. And more text. And more ) Tj
ET
BT
/F1 0010 Tf
69.2500 664.7040 Td
( text. Oh, how boring typing this stuff. But not as boring as watching ) Tj
ET
BT
/F1 0010 Tf
69.2500 652.7520 Td
( paint dry. And more text. And more text. And more text. And more text. ) Tj
ET
BT
/F1 0010 Tf
69.2500 640.8000 Td
( Boring. More, a little more text. The end, and just as well. ) Tj
ET
endstream
endobj
8 0 obj
[/PDF /Text]
endobj
9 0 obj
<<
/Type /Font
/Subtype /Type1
/Name /F1
/BaseFont /Helvetica
/Encoding /WinAnsiEncoding
>>
endobj
10 0 obj
<<
/Creator (Rave \(http://www.nevrona.com/rave\))
/Producer (Nevrona Designs)
/CreationDate (D:20060301072826)
>>
endobj
xref
0 11
0000000000 65535 f
0000000019 00000 n
0000000093 00000 n
0000000147 00000 n
0000000222 00000 n
0000000390 00000 n
0000001522 00000 n
0000001690 00000 n
0000002423 00000 n
0000002456 00000 n
0000002574 00000 n
trailer
<<
/Size 11
/Root 1 0 R
/Info 10 0 R
>>
startxref
2714
%%EOF

View File

@ -11,3 +11,5 @@ DOD_SDN = f"CN={DOD_SDN_INFO['last_name']}.{DOD_SDN_INFO['first_name']}.G.{DOD_S
MOCK_VALID_PE_ID = "8675309U" MOCK_VALID_PE_ID = "8675309U"
FIXTURE_EMAIL_ADDRESS = "artgarfunkel@uso.mil" FIXTURE_EMAIL_ADDRESS = "artgarfunkel@uso.mil"
PDF_FILENAME = "tests/fixtures/sample.pdf"

View File

@ -0,0 +1,18 @@
import pytest
from werkzeug.datastructures import FileStorage
from atst.models.attachment import Attachment, AttachmentError
from tests.mocks import PDF_FILENAME
def test_attach(pdf_upload):
attachment = Attachment.attach(pdf_upload)
assert attachment.filename == PDF_FILENAME
def test_attach_raises():
with open(PDF_FILENAME, "rb") as fp:
fs = FileStorage(fp, content_type="something/else")
with pytest.raises(AttachmentError):
Attachment.attach(fs)

View File

@ -1,4 +1,5 @@
import urllib import urllib
import pytest
from flask import url_for from flask import url_for
from atst.eda_client import MockEDAClient from atst.eda_client import MockEDAClient
@ -24,16 +25,6 @@ class TestPENumberInForm:
"treasury_code": "00123456", "treasury_code": "00123456",
"ba_code": "02A", "ba_code": "02A",
} }
extended_data = {
"funding_type": "RDTE",
"funding_type_other": "other",
"clin_0001": "50000",
"clin_0003": "13000",
"clin_1001": "30000",
"clin_1003": "7000",
"clin_2001": "30000",
"clin_2003": "7000",
}
def _set_monkeypatches(self, monkeypatch): def _set_monkeypatches(self, monkeypatch):
monkeypatch.setattr( monkeypatch.setattr(
@ -50,8 +41,7 @@ class TestPENumberInForm:
url_kwargs["extended"] = True url_kwargs["extended"] = True
response = client.post( response = client.post(
url_for("requests.financial_verification", **url_kwargs), url_for("requests.financial_verification", **url_kwargs),
headers={"Content-Type": "application/x-www-form-urlencoded"}, data=data,
data=urllib.parse.urlencode(data),
follow_redirects=False, follow_redirects=False,
) )
return response return response
@ -101,7 +91,6 @@ class TestPENumberInForm:
def test_submit_financial_form_with_invalid_task_order( def test_submit_financial_form_with_invalid_task_order(
self, monkeypatch, user_session, client self, monkeypatch, user_session, client
): ):
monkeypatch.setattr("atst.domain.requests.Requests.get", lambda i: MOCK_REQUEST)
user_session() user_session()
data = dict(self.required_data) data = dict(self.required_data)
@ -126,15 +115,30 @@ class TestPENumberInForm:
assert "enter TO information manually" not in response.data.decode() assert "enter TO information manually" not in response.data.decode()
def test_submit_extended_financial_form(self, monkeypatch, user_session, client): def test_submit_extended_financial_form(
monkeypatch.setattr("atst.domain.requests.Requests.get", lambda i: MOCK_REQUEST) self, monkeypatch, user_session, client, extended_financial_verification_data
):
request = RequestFactory.create()
monkeypatch.setattr("atst.domain.requests.Requests.get", lambda i: request)
monkeypatch.setattr("atst.forms.financial.validate_pe_id", lambda *args: True)
user_session() user_session()
data = {**self.required_data, **extended_financial_verification_data}
data = {**self.required_data, **self.extended_data}
data["pe_id"] = MOCK_REQUEST.body["financial_verification"]["pe_id"]
data["task_order_number"] = "1234567" data["task_order_number"] = "1234567"
response = self.submit_data(client, data, extended=True) response = self.submit_data(client, data, extended=True)
assert response.status_code == 302 assert response.status_code == 302
assert "/projects/new" in response.headers.get("Location") assert "/projects/new" in response.headers.get("Location")
def test_submit_invalid_extended_financial_form(
self, monkeypatch, user_session, client, extended_financial_verification_data
):
monkeypatch.setattr("atst.forms.financial.validate_pe_id", lambda *args: True)
user_session()
data = {**self.required_data, **extended_financial_verification_data}
data["task_order_number"] = "1234567"
del (data["clin_0001"])
response = self.submit_data(client, data, extended=True)
assert response.status_code == 200

33
tests/test_uploader.py Normal file
View File

@ -0,0 +1,33 @@
import os
import pytest
from werkzeug.datastructures import FileStorage
from atst.uploader import Uploader, UploadError
from tests.mocks import PDF_FILENAME
@pytest.fixture(scope="function")
def upload_dir(tmpdir):
return tmpdir.mkdir("uploads")
@pytest.fixture
def uploader(upload_dir):
return Uploader("LOCAL", container=upload_dir)
NONPDF_FILENAME = "tests/fixtures/disa-pki.html"
def test_upload(uploader, upload_dir, pdf_upload):
filename, object_name = uploader.upload(pdf_upload)
assert filename == PDF_FILENAME
assert os.path.isfile(os.path.join(upload_dir, object_name))
def test_upload_fails_for_non_pdfs(uploader):
with open(NONPDF_FILENAME, "rb") as fp:
fs = FileStorage(fp, content_type="text/plain")
with pytest.raises(UploadError):
uploader.upload(fs)