From b1823071740b786d805043d99cad49f7a277c26c Mon Sep 17 00:00:00 2001 From: richard-dds Date: Tue, 6 Aug 2019 11:54:43 -0400 Subject: [PATCH 01/12] Azure file downloads --- atst/domain/csp/file_uploads.py | 21 +++++++++++++++++++++ atst/routes/task_orders/new.py | 19 ++++++++++++++++--- js/lib/upload.js | 9 +++++++++ templates/fragments/task_order_review.html | 2 +- 4 files changed, 47 insertions(+), 4 deletions(-) diff --git a/atst/domain/csp/file_uploads.py b/atst/domain/csp/file_uploads.py index be6f2cae..bfc47689 100644 --- a/atst/domain/csp/file_uploads.py +++ b/atst/domain/csp/file_uploads.py @@ -6,6 +6,9 @@ class Uploader: def generate_token(self): pass + def generate_download_link(self, object_name, filename): + pass + def object_name(self): return str(uuid4()) @@ -17,6 +20,9 @@ class MockUploader(Uploader): def get_token(self): return ({}, self.object_name()) + def generate_download_link(self, object_name, filename): + return "" + class AzureUploader(Uploader): def __init__(self, config): @@ -53,6 +59,21 @@ class AzureUploader(Uploader): ) return ({"token": sas_token}, object_name) + def generate_download_link(self, object_name, filename): + account = CloudStorageAccount( + account_name=self.account_name, account_key=self.storage_key + ) + bbs = account.create_block_blob_service() + sas_token = bbs.generate_blob_shared_access_signature( + self.container_name, + object_name, + permission=BlobPermissions.READ, + expiry=datetime.utcnow() + self.timeout, + content_disposition=f"attachment; filename={filename}", + protocol="https" + ) + return bbs.make_blob_url(self.container_name, object_name, protocol="https", sas_token=sas_token) + class AwsUploader(Uploader): def __init__(self, config): diff --git a/atst/routes/task_orders/new.py b/atst/routes/task_orders/new.py index 201a6923..a953f619 100644 --- a/atst/routes/task_orders/new.py +++ b/atst/routes/task_orders/new.py @@ -17,8 +17,9 @@ from atst.models.permissions import Permissions from atst.utils.flash import formatted_flash as flash -def render_task_orders_edit(template, portfolio_id=None, task_order_id=None, form=None): - render_args = {} +def render_task_orders_edit(template, portfolio_id=None, task_order_id=None, form=None, extra_args=None): + render_args = extra_args or {} + if task_order_id: task_order = TaskOrders.get(task_order_id) portfolio_id = task_order.portfolio_id @@ -114,10 +115,14 @@ def edit(task_order_id): @task_orders_bp.route("/task_orders//form/step_1") @user_can(Permissions.CREATE_TASK_ORDER, message="view task order form") def form_step_one_add_pdf(portfolio_id=None, task_order_id=None): + (token, object_name) = current_app.uploader.get_token() + extra_args = {"token": token, "object_name": object_name} + return render_task_orders_edit( "task_orders/step_1.html", portfolio_id=portfolio_id, task_order_id=task_order_id, + extra_args=extra_args, ) @@ -219,11 +224,19 @@ def submit_form_step_three_add_clins(task_order_id): def form_step_four_review(task_order_id): task_order = TaskOrders.get(task_order_id) + (token, object_name) = current_app.uploader.get_token() + extra_args = { + "token": token, + "object_name": object_name, + "pdf_download_url": current_app.uploader.generate_download_link(task_order.pdf.object_name, task_order.pdf.filename) + } + if task_order.is_completed == False: raise NoAccessError("task order form review") return render_task_orders_edit( - "task_orders/step_4.html", task_order_id=task_order_id + "task_orders/step_4.html", task_order_id=task_order_id, + extra_args=extra_args, ) diff --git a/js/lib/upload.js b/js/lib/upload.js index f2c5440b..50fe3edd 100644 --- a/js/lib/upload.js +++ b/js/lib/upload.js @@ -43,6 +43,14 @@ class AzureUploader { fileReader.readAsText(file) }) } + + downloadUrl(objectName) { + const blobService = Azure.createBlobServiceWithSas( + `https://${this.accountName}.blob.core.windows.net`, + this.sasToken + ) + return blobService.getUrl(this.containerName, objectName, this.sasToken) + } } class AwsUploader { @@ -58,6 +66,7 @@ class AwsUploader { }) form.append('file', file) form.set('x-amz-meta-filename', file.name) + form.set('Content-Type', 'application/pdf') const response = await fetch(this.presignedPost.url, { method: 'POST', diff --git a/templates/fragments/task_order_review.html b/templates/fragments/task_order_review.html index 563f9169..d9d69f04 100644 --- a/templates/fragments/task_order_review.html +++ b/templates/fragments/task_order_review.html @@ -13,7 +13,7 @@
{{ 'task_orders.review.pdf_title' | translate }}
- + {{ Icon('check-circle-solid') }} {{ task_order.pdf.filename }} From 96f1c0d29515daba43b69b75092fb74a5c343fba Mon Sep 17 00:00:00 2001 From: richard-dds Date: Tue, 6 Aug 2019 14:27:07 -0400 Subject: [PATCH 02/12] AWS downloads --- atst/domain/csp/file_uploads.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/atst/domain/csp/file_uploads.py b/atst/domain/csp/file_uploads.py index bfc47689..a8947e20 100644 --- a/atst/domain/csp/file_uploads.py +++ b/atst/domain/csp/file_uploads.py @@ -108,7 +108,7 @@ class AwsUploader(Uploader): presigned_post = s3_client.generate_presigned_post( self.bucket_name, object_name, - ExpiresIn=3600, + ExpiresIn=self.timeout_secs, Conditions=[ ("eq", "$Content-Type", "application/pdf"), ("starts-with", "$x-amz-meta-filename", ""), @@ -116,3 +116,14 @@ class AwsUploader(Uploader): Fields={"Content-Type": "application/pdf", "x-amz-meta-filename": ""}, ) return (presigned_post, object_name) + + def generate_download_link(self, object_name, filename): + s3_client = boto3.client( + "s3", + aws_access_key_id=self.access_key_id, + aws_secret_access_key=self.secret_key, + config=boto3.session.Config( + signature_version="s3v4", region_name=self.region_name + ), + ) + return s3_client.generate_presigned_url("get_object", Params={"Bucket": self.bucket_name, "Key": object_name, "ResponseContentDisposition": f"attachment; filename={filename}"}, ExpiresIn=self.timeout_secs) From 04b77c113c38e53198de44df81fbd43f039fe4e9 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Tue, 6 Aug 2019 14:27:38 -0400 Subject: [PATCH 03/12] Formatting --- atst/domain/csp/file_uploads.py | 16 +++++++++++++--- atst/routes/task_orders/new.py | 11 +++++++---- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/atst/domain/csp/file_uploads.py b/atst/domain/csp/file_uploads.py index a8947e20..46341994 100644 --- a/atst/domain/csp/file_uploads.py +++ b/atst/domain/csp/file_uploads.py @@ -70,9 +70,11 @@ class AzureUploader(Uploader): permission=BlobPermissions.READ, expiry=datetime.utcnow() + self.timeout, content_disposition=f"attachment; filename={filename}", - protocol="https" + protocol="https", + ) + return bbs.make_blob_url( + self.container_name, object_name, protocol="https", sas_token=sas_token ) - return bbs.make_blob_url(self.container_name, object_name, protocol="https", sas_token=sas_token) class AwsUploader(Uploader): @@ -126,4 +128,12 @@ class AwsUploader(Uploader): signature_version="s3v4", region_name=self.region_name ), ) - return s3_client.generate_presigned_url("get_object", Params={"Bucket": self.bucket_name, "Key": object_name, "ResponseContentDisposition": f"attachment; filename={filename}"}, ExpiresIn=self.timeout_secs) + return s3_client.generate_presigned_url( + "get_object", + Params={ + "Bucket": self.bucket_name, + "Key": object_name, + "ResponseContentDisposition": f"attachment; filename={filename}", + }, + ExpiresIn=self.timeout_secs, + ) diff --git a/atst/routes/task_orders/new.py b/atst/routes/task_orders/new.py index a953f619..1177d73c 100644 --- a/atst/routes/task_orders/new.py +++ b/atst/routes/task_orders/new.py @@ -17,7 +17,9 @@ from atst.models.permissions import Permissions from atst.utils.flash import formatted_flash as flash -def render_task_orders_edit(template, portfolio_id=None, task_order_id=None, form=None, extra_args=None): +def render_task_orders_edit( + template, portfolio_id=None, task_order_id=None, form=None, extra_args=None +): render_args = extra_args or {} if task_order_id: @@ -228,15 +230,16 @@ def form_step_four_review(task_order_id): extra_args = { "token": token, "object_name": object_name, - "pdf_download_url": current_app.uploader.generate_download_link(task_order.pdf.object_name, task_order.pdf.filename) + "pdf_download_url": current_app.uploader.generate_download_link( + task_order.pdf.object_name, task_order.pdf.filename + ), } if task_order.is_completed == False: raise NoAccessError("task order form review") return render_task_orders_edit( - "task_orders/step_4.html", task_order_id=task_order_id, - extra_args=extra_args, + "task_orders/step_4.html", task_order_id=task_order_id, extra_args=extra_args ) From b7fbe9d2ac568914a43d0f1a085f2a5126b355fe Mon Sep 17 00:00:00 2001 From: richard-dds Date: Thu, 29 Aug 2019 11:10:05 -0400 Subject: [PATCH 04/12] Use underscores in urls --- atst/routes/task_orders/new.py | 2 +- js/components/upload_input.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/atst/routes/task_orders/new.py b/atst/routes/task_orders/new.py index 1177d73c..7e890255 100644 --- a/atst/routes/task_orders/new.py +++ b/atst/routes/task_orders/new.py @@ -75,7 +75,7 @@ def update_task_order( ) -@task_orders_bp.route("/task_orders//upload-token") +@task_orders_bp.route("/task_orders//upload_token") @user_can(Permissions.CREATE_TASK_ORDER, message="edit task order form") def upload_token(portfolio_id): (token, object_name) = app.csp.files.get_token() diff --git a/js/components/upload_input.js b/js/components/upload_input.js index 428cb367..df0cab99 100644 --- a/js/components/upload_input.js +++ b/js/components/upload_input.js @@ -104,7 +104,7 @@ export default { this.sizeError = false }, getUploader: async function() { - return fetch(`/task_orders/${this.portfolioId}/upload-token`, { + return fetch(`/task_orders/${this.portfolioId}/upload_token`, { credentials: 'include', }) .then(response => response.json()) From acc821a47578a95438ecf1803adbe13fcba7800b Mon Sep 17 00:00:00 2001 From: richard-dds Date: Thu, 29 Aug 2019 11:10:25 -0400 Subject: [PATCH 05/12] Fix generate_download_link implementations --- atst/domain/csp/file_uploads.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/atst/domain/csp/file_uploads.py b/atst/domain/csp/file_uploads.py index 46341994..a437cdbe 100644 --- a/atst/domain/csp/file_uploads.py +++ b/atst/domain/csp/file_uploads.py @@ -6,10 +6,10 @@ class Uploader: def generate_token(self): pass - def generate_download_link(self, object_name, filename): + def generate_download_link(self, object_name, filename) -> (dict, str): pass - def object_name(self): + def object_name(self) -> str: return str(uuid4()) @@ -60,14 +60,14 @@ class AzureUploader(Uploader): return ({"token": sas_token}, object_name) def generate_download_link(self, object_name, filename): - account = CloudStorageAccount( + account = self.CloudStorageAccount( account_name=self.account_name, account_key=self.storage_key ) bbs = account.create_block_blob_service() sas_token = bbs.generate_blob_shared_access_signature( self.container_name, object_name, - permission=BlobPermissions.READ, + permission=self.BlobPermissions.READ, expiry=datetime.utcnow() + self.timeout, content_disposition=f"attachment; filename={filename}", protocol="https", @@ -120,11 +120,11 @@ class AwsUploader(Uploader): return (presigned_post, object_name) def generate_download_link(self, object_name, filename): - s3_client = boto3.client( + s3_client = self.boto3.client( "s3", aws_access_key_id=self.access_key_id, aws_secret_access_key=self.secret_key, - config=boto3.session.Config( + config=self.boto3.session.Config( signature_version="s3v4", region_name=self.region_name ), ) From e8234dcf347b2529caef9ce56614434620f09d06 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Thu, 29 Aug 2019 11:11:25 -0400 Subject: [PATCH 06/12] Allow user to download files on TO upload page --- atst/routes/task_orders/new.py | 20 +++++++++++++------- js/components/upload_input.js | 23 +++++++++++++++++++++-- templates/components/upload_input.html | 4 ++-- 3 files changed, 36 insertions(+), 11 deletions(-) diff --git a/atst/routes/task_orders/new.py b/atst/routes/task_orders/new.py index 7e890255..57133b47 100644 --- a/atst/routes/task_orders/new.py +++ b/atst/routes/task_orders/new.py @@ -84,6 +84,18 @@ def upload_token(portfolio_id): return jsonify(render_args) +@task_orders_bp.route("/task_orders//download_link") +@user_can(Permissions.CREATE_TASK_ORDER, message="edit task order form") +def download_link(portfolio_id): + filename = http_request.args.get("filename") + object_name = http_request.args.get("objectName") + render_args = { + "downloadLink": app.csp.files.generate_download_link(object_name, filename) + } + + return jsonify(render_args) + + @task_orders_bp.route("/task_orders//edit") @user_can(Permissions.CREATE_TASK_ORDER, message="edit task order form") def edit(task_order_id): @@ -117,14 +129,10 @@ def edit(task_order_id): @task_orders_bp.route("/task_orders//form/step_1") @user_can(Permissions.CREATE_TASK_ORDER, message="view task order form") def form_step_one_add_pdf(portfolio_id=None, task_order_id=None): - (token, object_name) = current_app.uploader.get_token() - extra_args = {"token": token, "object_name": object_name} - return render_task_orders_edit( "task_orders/step_1.html", portfolio_id=portfolio_id, task_order_id=task_order_id, - extra_args=extra_args, ) @@ -228,11 +236,9 @@ def form_step_four_review(task_order_id): (token, object_name) = current_app.uploader.get_token() extra_args = { - "token": token, - "object_name": object_name, "pdf_download_url": current_app.uploader.generate_download_link( task_order.pdf.object_name, task_order.pdf.filename - ), + ) } if task_order.is_completed == False: diff --git a/js/components/upload_input.js b/js/components/upload_input.js index df0cab99..394c0057 100644 --- a/js/components/upload_input.js +++ b/js/components/upload_input.js @@ -18,7 +18,7 @@ export default { props: { name: String, initialData: { - type: String, + type: Object, }, initialErrors: { type: Boolean, @@ -39,10 +39,11 @@ export default { data: function() { return { hasInitialData: !!this.initialData, - attachment: this.initialData || null, + attachment: this.initialData.filename || null, changed: false, uploadError: false, sizeError: false, + downloadLink: '', } }, @@ -52,6 +53,11 @@ export default { name: this.name, valid: this.hasAttachment, }) + + if (this.hasInitialData) { + const { filename, objectName } = this.initialData + this.downloadLink = await this.getDownloadLink(filename, objectName) + } }, methods: { @@ -70,6 +76,10 @@ export default { this.attachment = e.target.value this.$refs.attachmentFilename.value = file.name this.$refs.attachmentObjectName.value = response.objectName + this.downloadLink = await this.getDownloadLink( + file.name, + response.objectName + ) } else { this.uploadError = true } @@ -110,6 +120,15 @@ export default { .then(response => response.json()) .then(({ token, objectName }) => buildUploader(token, objectName)) }, + getDownloadLink: async function(filename, objectName) { + const { downloadLink } = await fetch( + `/task_orders/${ + this.portfolioId + }/download_link?filename=${filename}&objectName=${objectName}`, + { credentials: 'include' } + ).then(r => r.json()) + return downloadLink + }, }, computed: { diff --git a/templates/components/upload_input.html b/templates/components/upload_input.html index c2cc176d..10177664 100644 --- a/templates/components/upload_input.html +++ b/templates/components/upload_input.html @@ -4,7 +4,7 @@
{{ Icon("check-circle-solid") }} - + Remove
From e1c15f4b3aa1c916f7ee15a61d3474bb72001687 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Thu, 29 Aug 2019 11:22:43 -0400 Subject: [PATCH 07/12] Fix TO link styling --- styles/elements/_uploader.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/styles/elements/_uploader.scss b/styles/elements/_uploader.scss index d7f6bd9b..ec384f91 100644 --- a/styles/elements/_uploader.scss +++ b/styles/elements/_uploader.scss @@ -43,6 +43,11 @@ margin-left: 0.5rem; font-weight: $font-bold; text-decoration: underline; + color: $color-black-light !important; + + &:hover { + color: $color-black-light; + } } &__remove { From 922a48f76ada540fd245e0ef4046f86818f9a115 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Thu, 29 Aug 2019 13:24:48 -0400 Subject: [PATCH 08/12] Fix download link generation --- atst/routes/task_orders/new.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/atst/routes/task_orders/new.py b/atst/routes/task_orders/new.py index 57133b47..c94b0213 100644 --- a/atst/routes/task_orders/new.py +++ b/atst/routes/task_orders/new.py @@ -234,9 +234,8 @@ def submit_form_step_three_add_clins(task_order_id): def form_step_four_review(task_order_id): task_order = TaskOrders.get(task_order_id) - (token, object_name) = current_app.uploader.get_token() extra_args = { - "pdf_download_url": current_app.uploader.generate_download_link( + "pdf_download_url": app.csp.files.generate_download_link( task_order.pdf.object_name, task_order.pdf.filename ) } From b26cd3ffae4bd70d0384e2b3d87aa48d5525943b Mon Sep 17 00:00:00 2001 From: richard-dds Date: Thu, 29 Aug 2019 15:46:30 -0400 Subject: [PATCH 09/12] Allow user to download a previously uploaded pdf --- js/components/__tests__/upload_input.test.js | 18 +++++++++++------- js/components/upload_input.js | 18 ++++++++++++------ .../upload_input_error_template.html | 2 +- js/test_templates/upload_input_template.html | 5 +++-- templates/components/upload_input.html | 5 +++-- tests/render_vue_component.py | 4 ++-- 6 files changed, 32 insertions(+), 20 deletions(-) diff --git a/js/components/__tests__/upload_input.test.js b/js/components/__tests__/upload_input.test.js index c867eea3..c2853cc7 100644 --- a/js/components/__tests__/upload_input.test.js +++ b/js/components/__tests__/upload_input.test.js @@ -8,7 +8,8 @@ const UploadWrapper = makeTestWrapper({ components: { uploadinput }, templatePath: 'upload_input_template.html', data: function() { - return { initialvalue: this.initialData.initialvalue, token: this.token } + const { filename, objectName } = this.initialData + return { filename, objectName } }, }) @@ -16,7 +17,7 @@ const UploadErrorWrapper = makeTestWrapper({ components: { uploadinput }, templatePath: 'upload_input_error_template.html', data: function() { - return { initialvalue: null, token: null } + return { filename: null, objectName: null } }, }) @@ -24,7 +25,7 @@ describe('UploadInput Test', () => { it('should show input and button when no attachment present', () => { const wrapper = mount(UploadWrapper, { propsData: { - initialData: { initialvalue: null, token: 'token' }, + initialData: {}, }, }) @@ -35,21 +36,24 @@ describe('UploadInput Test', () => { it('should show file name and hide input', () => { const wrapper = mount(UploadWrapper, { propsData: { - initialData: { initialvalue: 'somepdf.pdf', token: 'token' }, + initialData: { + filename: 'somepdf.pdf', + objectName: 'abcd', + }, }, }) const fileInput = wrapper.find('input[type=file]').element - const fileNameSpan = wrapper.find('.uploaded-file__name') + const fileNameLink = wrapper.find('.uploaded-file__name') expect(fileInput).toBe(undefined) - expect(fileNameSpan.html()).toContain('somepdf.pdf') + expect(fileNameLink.html()).toContain('somepdf.pdf') }) it('should correctly display error treatment', () => { const wrapper = mount(UploadErrorWrapper, { propsData: { - initialData: { initialvalue: 'somepdf.pdf', token: 'token' }, + initialData: { initialvalue: 'somepdf.pdf', objectName: 'abcd' }, }, }) diff --git a/js/components/upload_input.js b/js/components/upload_input.js index 394c0057..b4927d1b 100644 --- a/js/components/upload_input.js +++ b/js/components/upload_input.js @@ -17,8 +17,11 @@ export default { props: { name: String, - initialData: { - type: Object, + filename: { + type: String, + }, + objectName: { + type: String, }, initialErrors: { type: Boolean, @@ -38,8 +41,8 @@ export default { data: function() { return { - hasInitialData: !!this.initialData, - attachment: this.initialData.filename || null, + hasInitialData: false, + attachment: this.filename || null, changed: false, uploadError: false, sizeError: false, @@ -54,9 +57,12 @@ export default { valid: this.hasAttachment, }) + this.hasInitialData = !!this.filename if (this.hasInitialData) { - const { filename, objectName } = this.initialData - this.downloadLink = await this.getDownloadLink(filename, objectName) + this.downloadLink = await this.getDownloadLink( + this.filename, + this.objectName + ) } }, diff --git a/js/test_templates/upload_input_error_template.html b/js/test_templates/upload_input_error_template.html index 50c1c3ab..ff3676ab 100644 --- a/js/test_templates/upload_input_error_template.html +++ b/js/test_templates/upload_input_error_template.html @@ -13,7 +13,7 @@ - + Remove
diff --git a/js/test_templates/upload_input_template.html b/js/test_templates/upload_input_template.html index d572cfe1..07fbb194 100644 --- a/js/test_templates/upload_input_template.html +++ b/js/test_templates/upload_input_template.html @@ -1,7 +1,8 @@
diff --git a/templates/components/upload_input.html b/templates/components/upload_input.html index 10177664..e6066f65 100644 --- a/templates/components/upload_input.html +++ b/templates/components/upload_input.html @@ -4,7 +4,8 @@
{{ Icon("check-circle-solid") }} - + Remove
diff --git a/tests/render_vue_component.py b/tests/render_vue_component.py index ab33303c..6c23ab9d 100644 --- a/tests/render_vue_component.py +++ b/tests/render_vue_component.py @@ -15,8 +15,8 @@ class InitialValueForm(Form): class TaskOrderPdfForm(Form): - filename = StringField(default="initialvalue") - object_name = StringField() + filename = StringField(default="filename") + object_name = StringField(default="objectName") errorfield = StringField( label="error", validators=[InputRequired(message="Test Error Message")] From 518376a1d62c0c62f6b572003d2408b1f470f905 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Thu, 29 Aug 2019 16:08:55 -0400 Subject: [PATCH 10/12] Simplify hasInitialData --- js/components/upload_input.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/js/components/upload_input.js b/js/components/upload_input.js index b4927d1b..a0ad494c 100644 --- a/js/components/upload_input.js +++ b/js/components/upload_input.js @@ -41,7 +41,7 @@ export default { data: function() { return { - hasInitialData: false, + hasInitialData: !!this.filename, attachment: this.filename || null, changed: false, uploadError: false, @@ -57,7 +57,6 @@ export default { valid: this.hasAttachment, }) - this.hasInitialData = !!this.filename if (this.hasInitialData) { this.downloadLink = await this.getDownloadLink( this.filename, From e3f1d8b51cf5964382007a8c693b762ef2f6ea10 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Fri, 30 Aug 2019 15:44:36 -0400 Subject: [PATCH 11/12] Add README section for testing uploads / downloads in dev --- README.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/README.md b/README.md index ffe17fab..eac40a5c 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,33 @@ When the `DEBUG` environment variable is enabled and the app environment is not set to production, sent email messages are available at the `/messages` endpoint. Emails are not sent in development and test modes. +### File Uploads and Downloads + +Testing file uploads and downloads locally requires a few configuration options. + +In the flask config (`config/base.ini`, perhaps): + +``` +CSP= + +AWS_REGION_NAME="" +AWS_ACCESS_KEY="" +AWS_SECRET_KEY="" +AWS_BUCKET_NAME="" + +AZURE_STORAGE_KEY="" +AZURE_ACCOUNT_NAME="" +AZURE_TO_BUCKET_NAME="" +``` + +There are also some build-time configuration that are used by parcel. Add these to `.env.local`, and run `rm -r .cache/` before running `yarn build`: + +``` +CLOUD_PROVIDER= +AZURE_ACCOUNT_NAME="" +AZURE_CONTAINER_NAME="" +``` + ## Testing Tests require a test database: From 7d3bda949693926c2f5437e283078b9e061f9356 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Fri, 30 Aug 2019 15:53:26 -0400 Subject: [PATCH 12/12] Update permission for download_link endpoint --- atst/routes/task_orders/new.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/atst/routes/task_orders/new.py b/atst/routes/task_orders/new.py index c94b0213..c284d3f1 100644 --- a/atst/routes/task_orders/new.py +++ b/atst/routes/task_orders/new.py @@ -85,7 +85,7 @@ def upload_token(portfolio_id): @task_orders_bp.route("/task_orders//download_link") -@user_can(Permissions.CREATE_TASK_ORDER, message="edit task order form") +@user_can(Permissions.VIEW_TASK_ORDER_DETAILS, message="view task order download link") def download_link(portfolio_id): filename = http_request.args.get("filename") object_name = http_request.args.get("objectName")