Merge pull request #1005 from dod-ccpo/cloud-pdf-downloads
Cloud pdf downloads
This commit is contained in:
commit
c5284fd8f9
27
README.md
27
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.
|
set to production, sent email messages are available at the `/messages` endpoint.
|
||||||
Emails are not sent in development and test modes.
|
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 | azure | mock>
|
||||||
|
|
||||||
|
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=<aws | azure | mock>
|
||||||
|
AZURE_ACCOUNT_NAME=""
|
||||||
|
AZURE_CONTAINER_NAME=""
|
||||||
|
```
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
Tests require a test database:
|
Tests require a test database:
|
||||||
|
@ -6,7 +6,10 @@ class Uploader:
|
|||||||
def generate_token(self):
|
def generate_token(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def object_name(self):
|
def generate_download_link(self, object_name, filename) -> (dict, str):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def object_name(self) -> str:
|
||||||
return str(uuid4())
|
return str(uuid4())
|
||||||
|
|
||||||
|
|
||||||
@ -17,6 +20,9 @@ class MockUploader(Uploader):
|
|||||||
def get_token(self):
|
def get_token(self):
|
||||||
return ({}, self.object_name())
|
return ({}, self.object_name())
|
||||||
|
|
||||||
|
def generate_download_link(self, object_name, filename):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
class AzureUploader(Uploader):
|
class AzureUploader(Uploader):
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
@ -53,6 +59,23 @@ class AzureUploader(Uploader):
|
|||||||
)
|
)
|
||||||
return ({"token": sas_token}, object_name)
|
return ({"token": sas_token}, object_name)
|
||||||
|
|
||||||
|
def generate_download_link(self, object_name, filename):
|
||||||
|
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=self.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):
|
class AwsUploader(Uploader):
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
@ -87,7 +110,7 @@ class AwsUploader(Uploader):
|
|||||||
presigned_post = s3_client.generate_presigned_post(
|
presigned_post = s3_client.generate_presigned_post(
|
||||||
self.bucket_name,
|
self.bucket_name,
|
||||||
object_name,
|
object_name,
|
||||||
ExpiresIn=3600,
|
ExpiresIn=self.timeout_secs,
|
||||||
Conditions=[
|
Conditions=[
|
||||||
("eq", "$Content-Type", "application/pdf"),
|
("eq", "$Content-Type", "application/pdf"),
|
||||||
("starts-with", "$x-amz-meta-filename", ""),
|
("starts-with", "$x-amz-meta-filename", ""),
|
||||||
@ -95,3 +118,22 @@ class AwsUploader(Uploader):
|
|||||||
Fields={"Content-Type": "application/pdf", "x-amz-meta-filename": ""},
|
Fields={"Content-Type": "application/pdf", "x-amz-meta-filename": ""},
|
||||||
)
|
)
|
||||||
return (presigned_post, object_name)
|
return (presigned_post, object_name)
|
||||||
|
|
||||||
|
def generate_download_link(self, object_name, filename):
|
||||||
|
s3_client = self.boto3.client(
|
||||||
|
"s3",
|
||||||
|
aws_access_key_id=self.access_key_id,
|
||||||
|
aws_secret_access_key=self.secret_key,
|
||||||
|
config=self.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,
|
||||||
|
)
|
||||||
|
@ -17,8 +17,11 @@ from atst.models.permissions import Permissions
|
|||||||
from atst.utils.flash import formatted_flash as flash
|
from atst.utils.flash import formatted_flash as flash
|
||||||
|
|
||||||
|
|
||||||
def render_task_orders_edit(template, portfolio_id=None, task_order_id=None, form=None):
|
def render_task_orders_edit(
|
||||||
render_args = {}
|
template, portfolio_id=None, task_order_id=None, form=None, extra_args=None
|
||||||
|
):
|
||||||
|
render_args = extra_args or {}
|
||||||
|
|
||||||
if task_order_id:
|
if task_order_id:
|
||||||
task_order = TaskOrders.get(task_order_id)
|
task_order = TaskOrders.get(task_order_id)
|
||||||
portfolio_id = task_order.portfolio_id
|
portfolio_id = task_order.portfolio_id
|
||||||
@ -72,7 +75,7 @@ def update_task_order(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@task_orders_bp.route("/task_orders/<portfolio_id>/upload-token")
|
@task_orders_bp.route("/task_orders/<portfolio_id>/upload_token")
|
||||||
@user_can(Permissions.CREATE_TASK_ORDER, message="edit task order form")
|
@user_can(Permissions.CREATE_TASK_ORDER, message="edit task order form")
|
||||||
def upload_token(portfolio_id):
|
def upload_token(portfolio_id):
|
||||||
(token, object_name) = app.csp.files.get_token()
|
(token, object_name) = app.csp.files.get_token()
|
||||||
@ -81,6 +84,18 @@ def upload_token(portfolio_id):
|
|||||||
return jsonify(render_args)
|
return jsonify(render_args)
|
||||||
|
|
||||||
|
|
||||||
|
@task_orders_bp.route("/task_orders/<portfolio_id>/download_link")
|
||||||
|
@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")
|
||||||
|
render_args = {
|
||||||
|
"downloadLink": app.csp.files.generate_download_link(object_name, filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonify(render_args)
|
||||||
|
|
||||||
|
|
||||||
@task_orders_bp.route("/task_orders/<task_order_id>/edit")
|
@task_orders_bp.route("/task_orders/<task_order_id>/edit")
|
||||||
@user_can(Permissions.CREATE_TASK_ORDER, message="edit task order form")
|
@user_can(Permissions.CREATE_TASK_ORDER, message="edit task order form")
|
||||||
def edit(task_order_id):
|
def edit(task_order_id):
|
||||||
@ -219,11 +234,17 @@ def submit_form_step_three_add_clins(task_order_id):
|
|||||||
def form_step_four_review(task_order_id):
|
def form_step_four_review(task_order_id):
|
||||||
task_order = TaskOrders.get(task_order_id)
|
task_order = TaskOrders.get(task_order_id)
|
||||||
|
|
||||||
|
extra_args = {
|
||||||
|
"pdf_download_url": app.csp.files.generate_download_link(
|
||||||
|
task_order.pdf.object_name, task_order.pdf.filename
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if task_order.is_completed == False:
|
if task_order.is_completed == False:
|
||||||
raise NoAccessError("task order form review")
|
raise NoAccessError("task order form review")
|
||||||
|
|
||||||
return render_task_orders_edit(
|
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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -8,7 +8,8 @@ const UploadWrapper = makeTestWrapper({
|
|||||||
components: { uploadinput },
|
components: { uploadinput },
|
||||||
templatePath: 'upload_input_template.html',
|
templatePath: 'upload_input_template.html',
|
||||||
data: function() {
|
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 },
|
components: { uploadinput },
|
||||||
templatePath: 'upload_input_error_template.html',
|
templatePath: 'upload_input_error_template.html',
|
||||||
data: function() {
|
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', () => {
|
it('should show input and button when no attachment present', () => {
|
||||||
const wrapper = mount(UploadWrapper, {
|
const wrapper = mount(UploadWrapper, {
|
||||||
propsData: {
|
propsData: {
|
||||||
initialData: { initialvalue: null, token: 'token' },
|
initialData: {},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -35,21 +36,24 @@ describe('UploadInput Test', () => {
|
|||||||
it('should show file name and hide input', () => {
|
it('should show file name and hide input', () => {
|
||||||
const wrapper = mount(UploadWrapper, {
|
const wrapper = mount(UploadWrapper, {
|
||||||
propsData: {
|
propsData: {
|
||||||
initialData: { initialvalue: 'somepdf.pdf', token: 'token' },
|
initialData: {
|
||||||
|
filename: 'somepdf.pdf',
|
||||||
|
objectName: 'abcd',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const fileInput = wrapper.find('input[type=file]').element
|
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(fileInput).toBe(undefined)
|
||||||
expect(fileNameSpan.html()).toContain('somepdf.pdf')
|
expect(fileNameLink.html()).toContain('somepdf.pdf')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should correctly display error treatment', () => {
|
it('should correctly display error treatment', () => {
|
||||||
const wrapper = mount(UploadErrorWrapper, {
|
const wrapper = mount(UploadErrorWrapper, {
|
||||||
propsData: {
|
propsData: {
|
||||||
initialData: { initialvalue: 'somepdf.pdf', token: 'token' },
|
initialData: { initialvalue: 'somepdf.pdf', objectName: 'abcd' },
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -17,7 +17,10 @@ export default {
|
|||||||
|
|
||||||
props: {
|
props: {
|
||||||
name: String,
|
name: String,
|
||||||
initialData: {
|
filename: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
objectName: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
initialErrors: {
|
initialErrors: {
|
||||||
@ -38,11 +41,12 @@ export default {
|
|||||||
|
|
||||||
data: function() {
|
data: function() {
|
||||||
return {
|
return {
|
||||||
hasInitialData: !!this.initialData,
|
hasInitialData: !!this.filename,
|
||||||
attachment: this.initialData || null,
|
attachment: this.filename || null,
|
||||||
changed: false,
|
changed: false,
|
||||||
uploadError: false,
|
uploadError: false,
|
||||||
sizeError: false,
|
sizeError: false,
|
||||||
|
downloadLink: '',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -52,6 +56,13 @@ export default {
|
|||||||
name: this.name,
|
name: this.name,
|
||||||
valid: this.hasAttachment,
|
valid: this.hasAttachment,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (this.hasInitialData) {
|
||||||
|
this.downloadLink = await this.getDownloadLink(
|
||||||
|
this.filename,
|
||||||
|
this.objectName
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
@ -70,6 +81,10 @@ export default {
|
|||||||
this.attachment = e.target.value
|
this.attachment = e.target.value
|
||||||
this.$refs.attachmentFilename.value = file.name
|
this.$refs.attachmentFilename.value = file.name
|
||||||
this.$refs.attachmentObjectName.value = response.objectName
|
this.$refs.attachmentObjectName.value = response.objectName
|
||||||
|
this.downloadLink = await this.getDownloadLink(
|
||||||
|
file.name,
|
||||||
|
response.objectName
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
this.uploadError = true
|
this.uploadError = true
|
||||||
}
|
}
|
||||||
@ -104,12 +119,21 @@ export default {
|
|||||||
this.sizeError = false
|
this.sizeError = false
|
||||||
},
|
},
|
||||||
getUploader: async function() {
|
getUploader: async function() {
|
||||||
return fetch(`/task_orders/${this.portfolioId}/upload-token`, {
|
return fetch(`/task_orders/${this.portfolioId}/upload_token`, {
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
})
|
})
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(({ token, objectName }) => buildUploader(token, objectName))
|
.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: {
|
computed: {
|
||||||
|
@ -43,6 +43,14 @@ class AzureUploader {
|
|||||||
fileReader.readAsText(file)
|
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 {
|
class AwsUploader {
|
||||||
@ -58,6 +66,7 @@ class AwsUploader {
|
|||||||
})
|
})
|
||||||
form.append('file', file)
|
form.append('file', file)
|
||||||
form.set('x-amz-meta-filename', file.name)
|
form.set('x-amz-meta-filename', file.name)
|
||||||
|
form.set('Content-Type', 'application/pdf')
|
||||||
|
|
||||||
const response = await fetch(this.presignedPost.url, {
|
const response = await fetch(this.presignedPost.url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
<span class="icon icon--check-circle-solid " aria-hidden="true"><svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="check-circle" class="svg-inline--fa fa-check-circle fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M504 256c0 136.967-111.033 248-248 248S8 392.967 8 256 119.033 8 256 8s248 111.033 248 248zM227.314 387.314l184-184c6.248-6.248 6.248-16.379 0-22.627l-22.627-22.627c-6.248-6.249-16.379-6.249-22.628 0L216 308.118l-70.059-70.059c-6.248-6.248-16.379-6.248-22.628 0l-22.627 22.627c-6.248 6.248-6.248 16.379 0 22.627l104 104c6.249 6.249 16.379 6.249 22.628.001z"></path></svg></span>
|
<span class="icon icon--check-circle-solid " aria-hidden="true"><svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="check-circle" class="svg-inline--fa fa-check-circle fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M504 256c0 136.967-111.033 248-248 248S8 392.967 8 256 119.033 8 256 8s248 111.033 248 248zM227.314 387.314l184-184c6.248-6.248 6.248-16.379 0-22.627l-22.627-22.627c-6.248-6.249-16.379-6.249-22.628 0L216 308.118l-70.059-70.059c-6.248-6.248-16.379-6.248-22.628 0l-22.627 22.627c-6.248 6.248-6.248 16.379 0 22.627l104 104c6.249 6.249 16.379 6.249 22.628.001z"></path></svg></span>
|
||||||
|
|
||||||
<span class="uploaded-file__name" v-html="baseName"></span>
|
<a class="uploaded-file__name" v-html="baseName" v-bind:href="downloadLink"></a>
|
||||||
<a href="#" class="uploaded-file__remove" v-on:click="removeAttachment">Remove</a>
|
<a href="#" class="uploaded-file__remove" v-on:click="removeAttachment">Remove</a>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="hasAttachment === false" v-bind:class='{ "usa-input": true, "usa-input--error": showErrors }'>
|
<div v-show="hasAttachment === false" v-bind:class='{ "usa-input": true, "usa-input--error": showErrors }'>
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
<uploadinput
|
<uploadinput
|
||||||
inline-template
|
inline-template
|
||||||
|
|
||||||
v-bind:initial-data='initialvalue'
|
v-bind:filename='filename'
|
||||||
|
v-bind:object-name='objectName'
|
||||||
|
|
||||||
v-bind:watch='false'
|
v-bind:watch='false'
|
||||||
v-bind:portfolio-id="''"
|
v-bind:portfolio-id="''"
|
||||||
@ -13,7 +14,7 @@
|
|||||||
|
|
||||||
<span class="icon icon--check-circle-solid " aria-hidden="true"><svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="check-circle" class="svg-inline--fa fa-check-circle fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M504 256c0 136.967-111.033 248-248 248S8 392.967 8 256 119.033 8 256 8s248 111.033 248 248zM227.314 387.314l184-184c6.248-6.248 6.248-16.379 0-22.627l-22.627-22.627c-6.248-6.249-16.379-6.249-22.628 0L216 308.118l-70.059-70.059c-6.248-6.248-16.379-6.248-22.628 0l-22.627 22.627c-6.248 6.248-6.248 16.379 0 22.627l104 104c6.249 6.249 16.379 6.249 22.628.001z"></path></svg></span>
|
<span class="icon icon--check-circle-solid " aria-hidden="true"><svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="check-circle" class="svg-inline--fa fa-check-circle fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M504 256c0 136.967-111.033 248-248 248S8 392.967 8 256 119.033 8 256 8s248 111.033 248 248zM227.314 387.314l184-184c6.248-6.248 6.248-16.379 0-22.627l-22.627-22.627c-6.248-6.249-16.379-6.249-22.628 0L216 308.118l-70.059-70.059c-6.248-6.248-16.379-6.248-22.628 0l-22.627 22.627c-6.248 6.248-6.248 16.379 0 22.627l104 104c6.249 6.249 16.379 6.249 22.628.001z"></path></svg></span>
|
||||||
|
|
||||||
<span class="uploaded-file__name" v-html="baseName"></span>
|
<a class="uploaded-file__name" v-html="baseName" v-bind:href="downloadLink"></a>
|
||||||
<a href="#" class="uploaded-file__remove" v-on:click="removeAttachment">Remove</a>
|
<a href="#" class="uploaded-file__remove" v-on:click="removeAttachment">Remove</a>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="hasAttachment === false" v-bind:class='{ "usa-input": true, "usa-input--error": showErrors }'>
|
<div v-show="hasAttachment === false" v-bind:class='{ "usa-input": true, "usa-input--error": showErrors }'>
|
||||||
|
@ -43,6 +43,11 @@
|
|||||||
margin-left: 0.5rem;
|
margin-left: 0.5rem;
|
||||||
font-weight: $font-bold;
|
font-weight: $font-bold;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
color: $color-black-light !important;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $color-black-light;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__remove {
|
&__remove {
|
||||||
|
@ -4,7 +4,8 @@
|
|||||||
<uploadinput
|
<uploadinput
|
||||||
inline-template
|
inline-template
|
||||||
{% if not field.errors %}
|
{% if not field.errors %}
|
||||||
v-bind:initial-data='{{ field.filename.data | tojson }}'
|
v-bind:filename='{{ field.filename.data | tojson }}'
|
||||||
|
v-bind:object-name='{{ field.object_name.data | tojson }}'
|
||||||
{% else %}
|
{% else %}
|
||||||
v-bind:initial-errors='true'
|
v-bind:initial-errors='true'
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -16,7 +17,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<div v-show="hasAttachment" class="uploaded-file">
|
<div v-show="hasAttachment" class="uploaded-file">
|
||||||
{{ Icon("check-circle-solid") }}
|
{{ Icon("check-circle-solid") }}
|
||||||
<span class="uploaded-file__name" v-html="baseName"></span>
|
<a class="uploaded-file__name" v-html="baseName" v-bind:href="downloadLink"></a>
|
||||||
<a href="#" class="uploaded-file__remove" v-on:click="removeAttachment">Remove</a>
|
<a href="#" class="uploaded-file__remove" v-on:click="removeAttachment">Remove</a>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="hasAttachment === false" v-bind:class='{ "usa-input": true, "usa-input--error": showErrors }'>
|
<div v-show="hasAttachment === false" v-bind:class='{ "usa-input": true, "usa-input--error": showErrors }'>
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
<div class="h3">
|
<div class="h3">
|
||||||
{{ 'task_orders.review.pdf_title' | translate }}
|
{{ 'task_orders.review.pdf_title' | translate }}
|
||||||
</div>
|
</div>
|
||||||
<a class="icon-link icon-link--download" href="{{ url_for('task_orders.download_task_order_pdf', task_order_id=task_order.id) }}">
|
<a class="icon-link icon-link--download" href="{{ pdf_download_url }}">
|
||||||
{{ Icon('check-circle-solid') }}
|
{{ Icon('check-circle-solid') }}
|
||||||
{{ task_order.pdf.filename }}
|
{{ task_order.pdf.filename }}
|
||||||
</a>
|
</a>
|
||||||
|
@ -15,8 +15,8 @@ class InitialValueForm(Form):
|
|||||||
|
|
||||||
|
|
||||||
class TaskOrderPdfForm(Form):
|
class TaskOrderPdfForm(Form):
|
||||||
filename = StringField(default="initialvalue")
|
filename = StringField(default="filename")
|
||||||
object_name = StringField()
|
object_name = StringField(default="objectName")
|
||||||
|
|
||||||
errorfield = StringField(
|
errorfield = StringField(
|
||||||
label="error", validators=[InputRequired(message="Test Error Message")]
|
label="error", validators=[InputRequired(message="Test Error Message")]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user