1
0
mirror of https://github.com/ascribe/onion.git synced 2025-02-14 21:10:27 +01:00

Merge remote-tracking branch 'origin/AD-435-hash-locally-for-when-a-artist-do' into AD-435-hash-locally-for-when-a-artist-do

This commit is contained in:
diminator 2015-07-27 14:39:42 +02:00
commit b105e77009
6 changed files with 256 additions and 127 deletions

View File

@ -34,7 +34,12 @@ let SlidesContainer = React.createClass({
// check if slide_num was defined, and if not then default to 0 // check if slide_num was defined, and if not then default to 0
let queryParams = this.getQuery(); let queryParams = this.getQuery();
if(!('slide_num' in queryParams)) { if(!('slide_num' in queryParams)) {
this.replaceWith(this.getPathname(), null, {slide_num: 0});
// we're first requiring all the other possible queryParams and then set
// the specific one we need instead of overwriting them
queryParams.slide_num = 0;
this.replaceWith(this.getPathname(), null, queryParams);
} }
// init container width // init container width
@ -60,7 +65,12 @@ let SlidesContainer = React.createClass({
setSlideNum(slideNum) { setSlideNum(slideNum) {
if(slideNum < 0 || slideNum < React.Children.count(this.props.children)) { if(slideNum < 0 || slideNum < React.Children.count(this.props.children)) {
this.replaceWith(this.getPathname(), null, {slide_num: slideNum}); // we're first requiring all the other possible queryParams in order to not
// delete some of them that might be needed by other components
let queryParams = this.getQuery();
queryParams.slide_num = slideNum;
this.replaceWith(this.getPathname(), null, queryParams);
this.setState({ this.setState({
slideNum: slideNum slideNum: slideNum
}); });

View File

@ -1,12 +1,11 @@
'use strict'; 'use strict';
import React from 'react'; import React from 'react';
import ProgressBar from 'react-progressbar';
import FileDragAndDropDialog from './file_drag_and_drop_dialog'; import FileDragAndDropDialog from './file_drag_and_drop_dialog';
import FileDragAndDropPreviewIterator from './file_drag_and_drop_preview_iterator'; import FileDragAndDropPreviewIterator from './file_drag_and_drop_preview_iterator';
import AppConstants from '../../constants/application_constants';
import { getLangText } from '../../utils/lang_utils'; import { getLangText } from '../../utils/lang_utils';
// Taken from: https://github.com/fedosejev/react-file-drag-and-drop // Taken from: https://github.com/fedosejev/react-file-drag-and-drop
@ -31,8 +30,13 @@ let FileDragAndDrop = React.createClass({
areAssetsDownloadable: React.PropTypes.bool, areAssetsDownloadable: React.PropTypes.bool,
areAssetsEditable: React.PropTypes.bool, areAssetsEditable: React.PropTypes.bool,
localHashing: React.PropTypes.bool,
// triggers a FileDragAndDrop-global spinner // triggers a FileDragAndDrop-global spinner
hashingProgress: React.PropTypes.number hashingProgress: React.PropTypes.number,
// sets the value of this.state.hashingProgress in reactfineuploader
// to -1 which is code for: aborted
handleCancelHashing: React.PropTypes.func
}, },
handleDragStart(event) { handleDragStart(event) {
@ -146,16 +150,16 @@ let FileDragAndDrop = React.createClass({
className += this.props.dropzoneInactive ? 'inactive-dropzone' : 'active-dropzone'; className += this.props.dropzoneInactive ? 'inactive-dropzone' : 'active-dropzone';
className += this.props.className ? ' ' + this.props.className : ''; className += this.props.className ? ' ' + this.props.className : '';
// if !== -1: triggers a FileDragAndDrop-global spinner // if !== -2: triggers a FileDragAndDrop-global spinner
if(this.props.hashingProgress !== -1) { if(this.props.hashingProgress !== -2) {
return ( return (
<div className={className}> <div className={className}>
<p>{getLangText('Computing hashes... This may take a few minutes.')}</p> <p>{getLangText('Computing hash(es)... This may take a few minutes.')}</p>
<p>{this.props.hashingProgress}</p> <p>
<img <span>{Math.ceil(this.props.hashingProgress)}%</span>
height={35} <span onClick={this.props.handleCancelHashing}> {getLangText('Cancel hashing')}</span>
className="action-file" </p>
src={AppConstants.baseUrl + 'static/img/ascribe_animated_medium.gif'} /> <ProgressBar completed={this.props.hashingProgress} color="#48DACB"/>
</div> </div>
); );
} else { } else {
@ -172,7 +176,8 @@ let FileDragAndDrop = React.createClass({
<FileDragAndDropDialog <FileDragAndDropDialog
multipleFiles={this.props.multiple} multipleFiles={this.props.multiple}
hasFiles={hasFiles} hasFiles={hasFiles}
onClick={this.handleOnClick}/> onClick={this.handleOnClick}
localHashing={this.props.localHashing}/>
<FileDragAndDropPreviewIterator <FileDragAndDropPreviewIterator
files={this.props.filesToUpload} files={this.props.filesToUpload}
handleDeleteFile={this.handleDeleteFile} handleDeleteFile={this.handleDeleteFile}

View File

@ -1,38 +1,82 @@
'use strict'; 'use strict';
import React from 'react'; import React from 'react';
import Router from 'react-router';
import ButtonLink from 'react-router-bootstrap/lib/ButtonLink';
import { getLangText } from '../../utils/lang_utils'; import { getLangText } from '../../utils/lang_utils';
let Link = Router.Link;
let FileDragAndDropDialog = React.createClass({ let FileDragAndDropDialog = React.createClass({
propTypes: { propTypes: {
hasFiles: React.PropTypes.bool, hasFiles: React.PropTypes.bool,
multipleFiles: React.PropTypes.bool, multipleFiles: React.PropTypes.bool,
onClick: React.PropTypes.func onClick: React.PropTypes.func,
localHashing: React.PropTypes.bool
}, },
mixins: [Router.State],
render() { render() {
const queryParams = this.getQuery();
if(this.props.hasFiles) { if(this.props.hasFiles) {
return null; return null;
} else { } else {
if(this.props.multipleFiles) { if(!queryParams.method) {
let queryParamsHash = Object.assign({}, queryParams);
queryParamsHash.method = 'hash';
let queryParamsUpload = Object.assign({}, queryParams);
queryParamsUpload.method = 'upload';
return ( return (
<span className="file-drag-and-drop-dialog"> <span className="file-drag-and-drop-dialog present-options">
{getLangText('Click or drag to add files')} <p>{getLangText('Would you rather')}</p>
<Link
to={this.getPath()}
query={queryParamsHash}>
<span className="btn btn-default btn-sm">
{getLangText('Hash your work')}
</span>
</Link>
<span> or </span>
<Link
to={this.getPath()}
query={queryParamsUpload}>
<span className="btn btn-default btn-sm">
{getLangText('Upload and hash your work')}
</span>
</Link>
</span> </span>
); );
} else { } else {
return ( if(this.props.multipleFiles) {
<span className="file-drag-and-drop-dialog"> return (
<p>{getLangText('Drag a file here')}</p> <span className="file-drag-and-drop-dialog">
<p>{getLangText('or')}</p> {getLangText('Click or drag to add files')}
<span
className="btn btn-default"
onClick={this.props.onClick}>
{getLangText('choose a file to upload')}
</span> </span>
</span> );
); } else {
let dialog = queryParams.method === 'hash' ? getLangText('choose a file to hash') : getLangText('choose a file to upload');
return (
<span className="file-drag-and-drop-dialog">
<p>{getLangText('Drag a file here')}</p>
<p>{getLangText('or')}</p>
<span
className="btn btn-default"
onClick={this.props.onClick}>
{dialog}
</span>
</span>
);
}
} }
} }
} }

View File

@ -151,7 +151,7 @@ var ReactS3FineUploader = React.createClass({
} }
return name; return name;
}, },
multiple: true, multiple: false,
defaultErrorMessage: getLangText('Unexpected error. Please contact us if this happens repeatedly.') defaultErrorMessage: getLangText('Unexpected error. Please contact us if this happens repeatedly.')
}; };
}, },
@ -161,7 +161,9 @@ var ReactS3FineUploader = React.createClass({
filesToUpload: [], filesToUpload: [],
uploader: new fineUploader.s3.FineUploaderBasic(this.propsToConfig()), uploader: new fineUploader.s3.FineUploaderBasic(this.propsToConfig()),
csrfToken: getCookie(AppConstants.csrftoken), csrfToken: getCookie(AppConstants.csrftoken),
hashingProgress: -1 // for hashing feedback hashingProgress: -2
// -1: aborted
// -2: uninitialized
}; };
}, },
@ -219,111 +221,123 @@ var ReactS3FineUploader = React.createClass({
}, },
requestKey(fileId) { requestKey(fileId) {
let defer = new fineUploader.Promise();
let filename = this.state.uploader.getName(fileId); let filename = this.state.uploader.getName(fileId);
let uuid = this.state.uploader.getUuid(fileId); let uuid = this.state.uploader.getUuid(fileId);
window.fetch(this.props.keyRoutine.url, { return Q.Promise((resolve, reject) => {
method: 'post', window.fetch(this.props.keyRoutine.url, {
headers: { method: 'post',
'Accept': 'application/json', headers: {
'Content-Type': 'application/json', 'Accept': 'application/json',
'X-CSRFToken': getCookie(AppConstants.csrftoken) 'Content-Type': 'application/json',
}, 'X-CSRFToken': getCookie(AppConstants.csrftoken)
credentials: 'include', },
body: JSON.stringify({ credentials: 'include',
'filename': filename, body: JSON.stringify({
'category': this.props.keyRoutine.fileClass, 'filename': filename,
'uuid': uuid, 'category': this.props.keyRoutine.fileClass,
'piece_id': this.props.keyRoutine.pieceId 'uuid': uuid,
'piece_id': this.props.keyRoutine.pieceId
})
}) })
}) .then((res) => {
.then((res) => { return res.json();
return res.json(); })
}) .then((res) =>{
.then((res) =>{ resolve(res.key);
defer.success(res.key); })
}) .catch((err) => {
.catch((err) => { reject(err);
defer.failure(err); });
}); });
return defer;
}, },
createBlob(file) { createBlob(file) {
let defer = new fineUploader.Promise(); return Q.Promise((resolve, reject) => {
window.fetch(this.props.createBlobRoutine.url, {
window.fetch(this.props.createBlobRoutine.url, { method: 'post',
method: 'post', headers: {
headers: { 'Accept': 'application/json',
'Accept': 'application/json', 'Content-Type': 'application/json',
'Content-Type': 'application/json', 'X-CSRFToken': getCookie(AppConstants.csrftoken)
'X-CSRFToken': getCookie(AppConstants.csrftoken) },
}, credentials: 'include',
credentials: 'include', body: JSON.stringify({
body: JSON.stringify({ 'filename': file.name,
'filename': file.name, 'key': file.key,
'key': file.key, 'piece_id': this.props.createBlobRoutine.pieceId
'piece_id': this.props.createBlobRoutine.pieceId })
}) })
}) .then((res) => {
.then((res) => { return res.json();
return res.json(); })
}) .then((res) =>{
.then((res) =>{ if(res.otherdata) {
if(res.otherdata) { file.s3Url = res.otherdata.url_safe;
file.s3Url = res.otherdata.url_safe; file.s3UrlSafe = res.otherdata.url_safe;
file.s3UrlSafe = res.otherdata.url_safe; } else if(res.digitalwork) {
} else if(res.digitalwork) { file.s3Url = res.digitalwork.url_safe;
file.s3Url = res.digitalwork.url_safe; file.s3UrlSafe = res.digitalwork.url_safe;
file.s3UrlSafe = res.digitalwork.url_safe; } else {
} else { throw new Error(getLangText('Could not find a url to download.'));
throw new Error(getLangText('Could not find a url to download.')); }
} resolve(res);
defer.success(res.key); })
}) .catch((err) => {
.catch((err) => { reject(err);
defer.failure(err); console.logGlobal(err);
console.logGlobal(err); });
}); });
return defer;
}, },
/* FineUploader specific callback function handlers */ /* FineUploader specific callback function handlers */
onComplete(id) { onComplete(id) {
let files = this.state.filesToUpload; let files = this.state.filesToUpload;
// Set the state of the completed file to 'upload successful' in order to
// remove it from the GUI
files[id].status = 'upload successful'; files[id].status = 'upload successful';
files[id].key = this.state.uploader.getKey(id); files[id].key = this.state.uploader.getKey(id);
let newState = React.addons.update(this.state, { let newState = React.addons.update(this.state, {
filesToUpload: { $set: files } filesToUpload: { $set: files }
}); });
this.setState(newState);
this.createBlob(files[id]);
// since the form validation props isReadyForFormSubmission, setIsUploadReady and submitKey this.setState(newState);
// are optional, we'll only trigger them when they're actually defined
if(this.props.submitKey) { // Only after the blob has been created server-side, we can make the form submittable.
this.props.submitKey(files[id].key); this.createBlob(files[id])
} else { .then(() => {
console.warn('You didn\'t define submitKey in as a prop in react-s3-fine-uploader'); // since the form validation props isReadyForFormSubmission, setIsUploadReady and submitKey
} // are optional, we'll only trigger them when they're actually defined
if(this.props.submitKey) {
this.props.submitKey(files[id].key);
} else {
console.warn('You didn\'t define submitKey in as a prop in react-s3-fine-uploader');
}
// for explanation, check comment of if statement above
if(this.props.isReadyForFormSubmission && this.props.setIsUploadReady) {
// also, lets check if after the completion of this upload,
// the form is ready for submission or not
if(this.props.isReadyForFormSubmission(this.state.filesToUpload)) {
// if so, set uploadstatus to true
this.props.setIsUploadReady(true);
} else {
this.props.setIsUploadReady(false);
}
} else {
console.warn('You didn\'t define the functions isReadyForFormSubmission and/or setIsUploadReady in as a prop in react-s3-fine-uploader');
}
})
.catch((err) => {
console.logGlobal(err);
let notification = new GlobalNotificationModel(err.message, 'danger', 5000);
GlobalNotificationActions.appendGlobalNotification(notification);
});
// for explanation, check comment of if statement above
if(this.props.isReadyForFormSubmission && this.props.setIsUploadReady) {
// also, lets check if after the completion of this upload,
// the form is ready for submission or not
if(this.props.isReadyForFormSubmission(this.state.filesToUpload)) {
// if so, set uploadstatus to true
this.props.setIsUploadReady(true);
} else {
this.props.setIsUploadReady(false);
}
} else {
console.warn('You didn\'t define the functions isReadyForFormSubmission and/or setIsUploadReady in as a prop in react-s3-fine-uploader');
}
}, },
onError() { onError() {
@ -524,23 +538,42 @@ var ReactS3FineUploader = React.createClass({
// with the all function for iterables and essentially replace all original files // with the all function for iterables and essentially replace all original files
// with their txt representative // with their txt representative
Q.all(convertedFilePromises) Q.all(convertedFilePromises)
.progress(({index, value}) => { .progress(({index, value: {progress, reject}}) => {
// update file's progress
files[index].progress = value;
// calculate overall progress // hashing progress has been aborted from outside
let overallHashingProgress = 0; // To get out of the executing, we need to call reject from the
for(let i = 0; i < files.length; i++) { // inside of the promise's execution.
let filesSliceOfOverall = files[i].size / overallFileSize; // This is why we're passing (along with value) a function that essentially
// just does that (calling reject(err))
overallHashingProgress += filesSliceOfOverall * files[i].progress; //
// In the promises catch method, we're then checking if the interruption
// was due to that error or another generic one.
if(this.state.hashingProgress === -1) {
reject(new Error(getLangText('Hashing canceled')));
} }
this.setState({ hashingProgress: overallHashingProgress }); // update file's progress
files[index].progress = progress;
// calculate weighted average for overall progress of all
// currently hashing files
let overallHashingProgress = 0;
for(let i = 0; i < files.length; i++) {
let filesSliceOfOverall = files[i].size / overallFileSize;
overallHashingProgress += filesSliceOfOverall * files[i].progress;
}
// Multiply by 100, since react-progressbar expects decimal numbers
this.setState({ hashingProgress: overallHashingProgress * 100});
}) })
.then((convertedFiles) => { .then((convertedFiles) => {
// clear hashing progress, since its done
this.setState({ hashingProgress: -2});
// actually replacing all files with their txt-hash representative // actually replacing all files with their txt-hash representative
files = convertedFiles; files = convertedFiles;
@ -551,9 +584,19 @@ var ReactS3FineUploader = React.createClass({
}) })
.catch((err) => { .catch((err) => {
// if we're running into an error during the hash creation, we'll tell the user // If the error is that hashing has been canceled, we want to display a success
console.logGlobal(err); // message instead of a danger message
let notification = new GlobalNotificationModel(err.message, 'danger', 5000); let typeOfMessage = 'danger';
if(err.message === getLangText('Hashing canceled')) {
typeOfMessage = 'success';
this.setState({ hashingProgress: -2 });
} else {
// if there was a more generic error, we also log it
console.logGlobal(err);
}
let notification = new GlobalNotificationModel(err.message, typeOfMessage, 5000);
GlobalNotificationActions.appendGlobalNotification(notification); GlobalNotificationActions.appendGlobalNotification(notification);
}); });
@ -565,6 +608,13 @@ var ReactS3FineUploader = React.createClass({
} }
}, },
handleCancelHashing() {
// Every progress tick of the hashing function in handleUploadFile there is a
// check if this.state.hashingProgress is -1. If so, there is an error thrown that cancels
// the hashing of all files immediately.
this.setState({ hashingProgress: -1 });
},
// ReactFineUploader is essentially just a react layer around s3 fineuploader. // ReactFineUploader is essentially just a react layer around s3 fineuploader.
// However, since we need to display the status of a file (progress, uploading) as well as // However, since we need to display the status of a file (progress, uploading) as well as
// be able to execute actions on a currently uploading file we need to exactly sync the file list // be able to execute actions on a currently uploading file we need to exactly sync the file list
@ -648,11 +698,13 @@ var ReactS3FineUploader = React.createClass({
handleCancelFile={this.handleCancelFile} handleCancelFile={this.handleCancelFile}
handlePauseFile={this.handlePauseFile} handlePauseFile={this.handlePauseFile}
handleResumeFile={this.handleResumeFile} handleResumeFile={this.handleResumeFile}
handleCancelHashing={this.handleCancelHashing}
multiple={this.props.multiple} multiple={this.props.multiple}
areAssetsDownloadable={this.props.areAssetsDownloadable} areAssetsDownloadable={this.props.areAssetsDownloadable}
areAssetsEditable={this.props.areAssetsEditable} areAssetsEditable={this.props.areAssetsEditable}
dropzoneInactive={!this.props.areAssetsEditable || !this.props.multiple && this.state.filesToUpload.filter((file) => file.status !== 'deleted' && file.status !== 'canceled' && file.size !== -1).length > 0} dropzoneInactive={!this.props.areAssetsEditable || !this.props.multiple && this.state.filesToUpload.filter((file) => file.status !== 'deleted' && file.status !== 'canceled' && file.size !== -1).length > 0}
hashingProgress={this.state.hashingProgress} /> hashingProgress={this.state.hashingProgress}
localHashing={this.props.localHashing} />
</div> </div>
); );
} }

View File

@ -69,7 +69,15 @@ export function computeHashOfFile(file) {
end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize; end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;
// send progress // send progress
notify(start / file.size); // Due to the fact that progressHandler and notify are going to be removed in v2
// of Q, the functionality of throwing errors in the progressHandler will not be implemented
// anymore. To still be able to throw an error however, we can just expose the promise's reject
// method to the .progress function to stop the execution immediately.
notify({
progress: start / file.size,
reject
});
fileReader.readAsArrayBuffer(blobSlice.call(file, start, end)); fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
} }

View File

@ -10,7 +10,7 @@
cursor: default !important; cursor: default !important;
padding: 1.5em 1.5em 1.5em 0; padding: 1.5em 0 1.5em 0;
} }
.inactive-dropzone { .inactive-dropzone {
@ -19,6 +19,16 @@
outline: 0; outline: 0;
} }
.present-options {
> p {
margin-bottom: .75em !important;
}
.btn {
margin: 0 1em 0 1em;
}
}
.file-drag-and-drop .file-drag-and-drop-dialog > p:first-child { .file-drag-and-drop .file-drag-and-drop-dialog > p:first-child {
font-size: 1.5em !important; font-size: 1.5em !important;