Added ability to upload images to s3 and store them as part of ES object
This commit is contained in:
parent
9c74599950
commit
d14518ac91
|
@ -1,10 +1,12 @@
|
||||||
from flask import Blueprint, render_template, abort, jsonify, request
|
from flask import Blueprint, render_template, abort, jsonify, request
|
||||||
from elasticsearch import Elasticsearch, RequestsHttpConnection
|
from elasticsearch import Elasticsearch, RequestsHttpConnection
|
||||||
from requests_aws4auth import AWS4Auth
|
from requests_aws4auth import AWS4Auth
|
||||||
|
import boto3
|
||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
import config
|
import config
|
||||||
import setup
|
import setup
|
||||||
|
import uuid
|
||||||
|
|
||||||
sys.path.insert(0,'..')
|
sys.path.insert(0,'..')
|
||||||
|
|
||||||
|
@ -13,6 +15,18 @@ sys.path.insert(0,'..')
|
||||||
reload(sys)
|
reload(sys)
|
||||||
sys.setdefaultencoding("utf-8")
|
sys.setdefaultencoding("utf-8")
|
||||||
|
|
||||||
|
s3 = boto3.client('s3')
|
||||||
|
|
||||||
|
def create_presigned_post():
|
||||||
|
bucket_name = config.JOYCE_S3_BUCKET
|
||||||
|
key_name = str(uuid.uuid4())
|
||||||
|
response = s3.generate_presigned_post(
|
||||||
|
bucket_name,
|
||||||
|
key_name,
|
||||||
|
ExpiresIn = 3600
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
|
||||||
if config.ENVIRONMENT == 'local':
|
if config.ENVIRONMENT == 'local':
|
||||||
es = Elasticsearch(config.ELASTICSEARCH_LOCAL_HOST)
|
es = Elasticsearch(config.ELASTICSEARCH_LOCAL_HOST)
|
||||||
|
|
||||||
|
@ -62,7 +76,6 @@ def es_get_document(index, id):
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def es_index_document(index, id, body):
|
def es_index_document(index, id, body):
|
||||||
print(index)
|
|
||||||
res = es.index(
|
res = es.index(
|
||||||
index=index,
|
index=index,
|
||||||
doc_type='doc',
|
doc_type='doc',
|
||||||
|
@ -325,7 +338,6 @@ def search_text():
|
||||||
data = json.loads(request.data)
|
data = json.loads(request.data)
|
||||||
results = es_search_text(data.get('data'))
|
results = es_search_text(data.get('data'))
|
||||||
return jsonify(results)
|
return jsonify(results)
|
||||||
|
|
||||||
#
|
#
|
||||||
# Refresh ES
|
# Refresh ES
|
||||||
# TODO: Restrict to dev only
|
# TODO: Restrict to dev only
|
||||||
|
@ -333,3 +345,13 @@ def search_text():
|
||||||
def refresh_es():
|
def refresh_es():
|
||||||
setup.es_setup()
|
setup.es_setup()
|
||||||
return 'Success!'
|
return 'Success!'
|
||||||
|
|
||||||
|
#
|
||||||
|
# Get Signed URL for Upload
|
||||||
|
#
|
||||||
|
@api.route('/signed_post/')
|
||||||
|
def media_post_url():
|
||||||
|
data = jsonify(create_presigned_post())
|
||||||
|
print 'hey'
|
||||||
|
# + '?signature=' + url.fields.signature + '&AWSAccessKeyId=' + url.fields.AWSAccessKeyId
|
||||||
|
return data
|
|
@ -25,7 +25,8 @@
|
||||||
"react-router": "^4.2.0",
|
"react-router": "^4.2.0",
|
||||||
"react-router-dom": "^4.2.2",
|
"react-router-dom": "^4.2.2",
|
||||||
"react-router-redux": "^5.0.0-alpha.9",
|
"react-router-redux": "^5.0.0-alpha.9",
|
||||||
"redux": "^3.7.2"
|
"redux": "^3.7.2",
|
||||||
|
"uuid": "^3.3.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@fortawesome/fontawesome-free": "^5.5.0",
|
"@fortawesome/fontawesome-free": "^5.5.0",
|
||||||
|
|
|
@ -47,7 +47,19 @@ const apiActions = {
|
||||||
type: 'GET_SEARCH_RESULTS',
|
type: 'GET_SEARCH_RESULTS',
|
||||||
data: response.data,
|
data: response.data,
|
||||||
status: response.status ? response.status : 'request'
|
status: response.status ? response.status : 'request'
|
||||||
})
|
}),
|
||||||
|
uploadMediaToS3Request: (response, data) =>
|
||||||
|
({
|
||||||
|
type: 'UPLOAD_TO_S3_REQUEST',
|
||||||
|
file: data,
|
||||||
|
signed_post: response.data
|
||||||
|
}),
|
||||||
|
uploadMediaToS3Response: (response) =>
|
||||||
|
({
|
||||||
|
type: 'UPLOAD_TO_S3_RESPONSE',
|
||||||
|
status: response.status,
|
||||||
|
url: response.url ? response.url : undefined
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
export default apiActions
|
export default apiActions
|
|
@ -122,6 +122,12 @@ const userActions = {
|
||||||
hideAdmin: () =>
|
hideAdmin: () =>
|
||||||
({
|
({
|
||||||
type: 'HIDE_ADMIN_HEADER'
|
type: 'HIDE_ADMIN_HEADER'
|
||||||
|
}),
|
||||||
|
// Media
|
||||||
|
uploadMediaInput: input =>
|
||||||
|
({
|
||||||
|
type: 'UPLOAD_MEDIA_SUBMIT',
|
||||||
|
data: input
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
const MediaUploadInput = ({input, onChange}) =>
|
const MediaUploadInput = ({input, s3Path, onChange, onUpload}) =>
|
||||||
<div className="input-group mb-3">
|
<div className="input-group mb-3">
|
||||||
<div className="custom-file">
|
<div className="custom-file">
|
||||||
<input type="file" className="custom-file-input" id="media_input" onChange={onChange}/>
|
<input type="file" className="custom-file-input" id="media_input" onChange={onChange}/>
|
||||||
<label className="custom-file-label" htmlFor="media_input">{input ? input[0].name : 'Choose file'}</label>
|
<label className="custom-file-label" htmlFor="media_input">{input ? input[0].name : 'Choose file'}</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="input-group-append">
|
||||||
|
<button className="btn btn-outline-warning" type="button" disabled={input ? false : true} onClick={()=>onUpload(input)}>Upload</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
export default MediaUploadInput
|
export default MediaUploadInput
|
|
@ -23,6 +23,7 @@ const EditorEditMode = ({
|
||||||
onColorPickerInputChange,
|
onColorPickerInputChange,
|
||||||
onColorSwatchClick,
|
onColorSwatchClick,
|
||||||
onMediaInputChange,
|
onMediaInputChange,
|
||||||
|
onMediaUpload,
|
||||||
cancelEdit,
|
cancelEdit,
|
||||||
onSubmitClick,
|
onSubmitClick,
|
||||||
onToolButtonClick,
|
onToolButtonClick,
|
||||||
|
@ -56,9 +57,12 @@ const EditorEditMode = ({
|
||||||
onColorSwatchClick={onColorSwatchClick}
|
onColorSwatchClick={onColorSwatchClick}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
{docType === 'media' &&
|
{docType === 'media' && inputs.s3Path &&
|
||||||
<MediaUploadInput input={inputs.fileUpload} onChange={onMediaInputChange}/>
|
<p>File uploaded!</p>
|
||||||
}
|
}
|
||||||
|
{docType === 'media' && !inputs.s3Path &&
|
||||||
|
<MediaUploadInput input={inputs.uploadFile} onChange={onMediaInputChange} onUpload={onMediaUpload}/>
|
||||||
|
}
|
||||||
</EditorAttributeContentBlock>
|
</EditorAttributeContentBlock>
|
||||||
<EditorBottomBarContentBlock>
|
<EditorBottomBarContentBlock>
|
||||||
<EditorSubmitOptions
|
<EditorSubmitOptions
|
||||||
|
@ -100,6 +104,9 @@ const mapDispatchToProps = dispatch => {
|
||||||
onMediaInputChange: input => {
|
onMediaInputChange: input => {
|
||||||
dispatch(actions.updateMediaInput(input))
|
dispatch(actions.updateMediaInput(input))
|
||||||
},
|
},
|
||||||
|
onMediaUpload: input => {
|
||||||
|
dispatch(actions.uploadMediaInput(input))
|
||||||
|
},
|
||||||
cancelEdit: () => {
|
cancelEdit: () => {
|
||||||
dispatch(actions.cancelEdit())
|
dispatch(actions.cancelEdit())
|
||||||
},
|
},
|
||||||
|
|
|
@ -24,8 +24,7 @@ const joyceAPI = store => next => action => {
|
||||||
if (action.status === 'request') {
|
if (action.status === 'request') {
|
||||||
if (action.id) {
|
if (action.id) {
|
||||||
api.HTTPPostWriteDocument(action.id, action.docType, action.data).then(response =>
|
api.HTTPPostWriteDocument(action.id, action.docType, action.data).then(response =>
|
||||||
store.dispatch(actions.saveDocument(response)
|
store.dispatch(actions.saveDocument(response))
|
||||||
)
|
|
||||||
)} else {
|
)} else {
|
||||||
api.HTTPPutCreateDocument(action.docType, action.data).then(response =>
|
api.HTTPPutCreateDocument(action.docType, action.data).then(response =>
|
||||||
store.dispatch(actions.saveDocument(response))
|
store.dispatch(actions.saveDocument(response))
|
||||||
|
@ -46,7 +45,23 @@ const joyceAPI = store => next => action => {
|
||||||
store.dispatch(actions.getSearchResults(response))
|
store.dispatch(actions.getSearchResults(response))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
case 'UPLOAD_MEDIA_SUBMIT':
|
||||||
|
api.HTTPGetSignedPost().then(response =>
|
||||||
|
store.dispatch(actions.uploadMediaToS3Request(response, action.data[0])
|
||||||
|
))
|
||||||
|
break
|
||||||
|
case 'UPLOAD_TO_S3_REQUEST':
|
||||||
|
const formData = new FormData()
|
||||||
|
const url = action.signed_post.url
|
||||||
|
formData.append('AWSAccessKeyId', action.signed_post.fields.AWSAccessKeyId)
|
||||||
|
formData.append('key', action.signed_post.fields.key)
|
||||||
|
formData.append('policy', action.signed_post.fields.policy)
|
||||||
|
formData.append('signature', action.signed_post.fields.signature)
|
||||||
|
formData.append('file', action.file)
|
||||||
|
api.HTTPPostMedia(url, formData).then(response =>
|
||||||
|
store.dispatch(actions.uploadMediaToS3Response(response))
|
||||||
|
)
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { EditorState, Modifier } from 'draft-js'
|
||||||
import { stateToHTML } from 'draft-js-export-html'
|
import { stateToHTML } from 'draft-js-export-html'
|
||||||
|
|
||||||
import actions from '../actions'
|
import actions from '../actions'
|
||||||
|
import api from '../modules/api'
|
||||||
import helpers from '../modules/helpers'
|
import helpers from '../modules/helpers'
|
||||||
import { validateSubmittedDocument, validateSubmittedAnnotation } from '../modules/validation'
|
import { validateSubmittedDocument, validateSubmittedAnnotation } from '../modules/validation'
|
||||||
import { html_export_options, convertToSearchText, linkDecorator } from '../modules/editorSettings.js'
|
import { html_export_options, convertToSearchText, linkDecorator } from '../modules/editorSettings.js'
|
||||||
|
@ -25,11 +26,14 @@ const joyceInterface = store => next => action => {
|
||||||
const data = {
|
const data = {
|
||||||
title: action.inputs.documentTitle,
|
title: action.inputs.documentTitle,
|
||||||
html_source: stateToHTML(textContent, html_export_options),
|
html_source: stateToHTML(textContent, html_export_options),
|
||||||
search_text: convertToSearchText(textContent)
|
search_text: convertToSearchText(textContent),
|
||||||
}
|
}
|
||||||
if (action.docType === 'tags') {
|
if (action.docType === 'tags') {
|
||||||
data.color = action.inputs.colorPicker
|
data.color = action.inputs.colorPicker
|
||||||
}
|
}
|
||||||
|
if (action.docType === 'media') {
|
||||||
|
data.s3Path = action.inputs.s3Path
|
||||||
|
}
|
||||||
if (action.currentDocument.id) {
|
if (action.currentDocument.id) {
|
||||||
data.id = action.currentDocument.id
|
data.id = action.currentDocument.id
|
||||||
}
|
}
|
||||||
|
@ -90,7 +94,7 @@ const joyceInterface = store => next => action => {
|
||||||
// Search Action Middleware
|
// Search Action Middleware
|
||||||
case 'CLICK_SEARCH':
|
case 'CLICK_SEARCH':
|
||||||
store.dispatch(actions.getSearchResults({data: action.data}))
|
store.dispatch(actions.getSearchResults({data: action.data}))
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,9 +43,20 @@ const api = {
|
||||||
axios.get(apiRoute + 'refresh/').then(res => {
|
axios.get(apiRoute + 'refresh/').then(res => {
|
||||||
return {status: 'success', data: res.data}
|
return {status: 'success', data: res.data}
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
console.log(error)
|
|
||||||
return {status: 'error', data: error}
|
return {status: 'error', data: error}
|
||||||
}),
|
}),
|
||||||
|
HTTPGetSignedPost: () =>
|
||||||
|
axios.get(apiRoute + 'signed_post/').then(res=> {
|
||||||
|
return {status: 'success', data: res.data}
|
||||||
|
}).catch(error => {
|
||||||
|
return {status: 'error', data: error}
|
||||||
|
}),
|
||||||
|
HTTPPostMedia: (url, formData) =>
|
||||||
|
axios.post(url, formData, {headers: {'Content-Type': 'image/*', 'ACL': 'public-read'}}).then(res=> {
|
||||||
|
return {status: 'success', url: url + formData.get('key')}
|
||||||
|
}).catch(error => {
|
||||||
|
return {status: 'error', data: error}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export default api
|
export default api
|
|
@ -2,18 +2,34 @@ const initialState = {
|
||||||
documentTitle: '',
|
documentTitle: '',
|
||||||
search: '',
|
search: '',
|
||||||
colorPicker: '',
|
colorPicker: '',
|
||||||
fileUpload: undefined
|
uploadFile: undefined,
|
||||||
|
s3Path: undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputs = (state=initialState, action) => {
|
const inputs = (state=initialState, action) => {
|
||||||
switch(action.type) {
|
switch(action.type) {
|
||||||
// Document Title
|
// Document Title
|
||||||
case 'GET_DOCUMENT_TEXT':
|
case 'GET_DOCUMENT_TEXT':
|
||||||
if (action.status === 'success' && action.state === 'currentDocument') {
|
console.log('state', action.state)
|
||||||
|
console.log('status', action.status)
|
||||||
|
console.log('docType', action.docType)
|
||||||
|
if (action.status === 'success' && action.state === 'currentDocument' && ['tags', 'media'].indexOf(action.docType) <= 0 ) {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
documentTitle: action.data.title
|
documentTitle: action.data.title
|
||||||
}
|
}
|
||||||
|
} else if (action.status === 'success' && action.docType === 'tags') {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
documentTitle: action.data.title,
|
||||||
|
colorPicker: action.data.color
|
||||||
|
}
|
||||||
|
} else if (action.status === 'success' && action.state === 'currentDocument' && action.docType === 'media') {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
documentTitle: action.data.title,
|
||||||
|
s3Path: action.data.s3Path
|
||||||
|
}
|
||||||
} else { return state }
|
} else { return state }
|
||||||
case 'CREATE_DOCUMENT':
|
case 'CREATE_DOCUMENT':
|
||||||
return {
|
return {
|
||||||
|
@ -32,13 +48,6 @@ const inputs = (state=initialState, action) => {
|
||||||
search: action.data
|
search: action.data
|
||||||
}
|
}
|
||||||
// Color Picker
|
// Color Picker
|
||||||
case 'GET_DOCUMENT_TEXT':
|
|
||||||
if (action.status === 'success' && action.docType === 'tags') {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
colorPicker: action.data.color
|
|
||||||
}
|
|
||||||
} else { return state }
|
|
||||||
case 'SAVE_DOCUMENT':
|
case 'SAVE_DOCUMENT':
|
||||||
if (action.status === 'success' && action.docType === 'tags') {
|
if (action.status === 'success' && action.docType === 'tags') {
|
||||||
return {
|
return {
|
||||||
|
@ -65,10 +74,18 @@ const inputs = (state=initialState, action) => {
|
||||||
case 'UPDATE_MEDIA_INPUT':
|
case 'UPDATE_MEDIA_INPUT':
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
fileUpload: action.data
|
uploadFile: action.data
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return state
|
return state
|
||||||
|
// S3 File
|
||||||
|
case 'UPLOAD_TO_S3_RESPONSE':
|
||||||
|
if (action.status === 'success') {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
s3Path: action.url
|
||||||
|
}
|
||||||
|
} else { return state }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue