1
0
mirror of https://github.com/ascribe/onion.git synced 2024-06-30 13:41:57 +02:00

Merge remote-tracking branch 'remotes/origin/master' into AD-1362-implement-portfolioreview-judging-flow

This commit is contained in:
diminator 2015-12-09 16:48:33 +01:00
commit 3461a28f36
12 changed files with 223 additions and 221 deletions

View File

@ -1,51 +0,0 @@
'use strict';
import { alt } from '../alt';
import CoaFetcher from '../fetchers/coa_fetcher';
import Q from 'q';
class CoaActions {
constructor() {
this.generateActions(
'updateCoa',
'flushCoa'
);
}
fetchOrCreate(id, bitcoinId) {
return Q.Promise((resolve, reject) => {
CoaFetcher.fetchOne(id)
.then((res) => {
if (res.coa) {
this.actions.updateCoa(res.coa);
resolve(res.coa);
}
else {
this.actions.create(bitcoinId);
}
})
.catch((err) => {
console.logGlobal(err);
this.actions.updateCoa(null);
reject(err);
});
});
}
create(bitcoinId) {
return Q.Promise((resolve, reject) => {
CoaFetcher.create(bitcoinId)
.then((res) => {
this.actions.updateCoa(res.coa);
})
.catch((err) => {
console.logGlobal(err);
this.actions.updateCoa(null);
reject(err);
});
});
}
}
export default alt.createActions(CoaActions);

View File

@ -1,27 +1,19 @@
'use strict'; 'use strict';
import { alt } from '../alt'; import { alt } from '../alt';
import EditionFetcher from '../fetchers/edition_fetcher';
class EditionActions { class EditionActions {
constructor() { constructor() {
this.generateActions( this.generateActions(
'updateEdition', 'fetchEdition',
'editionFailed' 'successFetchEdition',
'successFetchCoa',
'flushEdition',
'errorCoa',
'errorEdition'
); );
} }
fetchOne(editionId) {
EditionFetcher.fetchOne(editionId)
.then((res) => {
this.actions.updateEdition(res.edition);
})
.catch((err) => {
console.logGlobal(err);
this.actions.editionFailed(err.json);
});
}
} }
export default alt.createActions(EditionActions); export default alt.createActions(EditionActions);

View File

@ -8,11 +8,6 @@ import Row from 'react-bootstrap/lib/Row';
import Col from 'react-bootstrap/lib/Col'; import Col from 'react-bootstrap/lib/Col';
import Glyphicon from 'react-bootstrap/lib/Glyphicon'; import Glyphicon from 'react-bootstrap/lib/Glyphicon';
import UserActions from '../../actions/user_actions';
import UserStore from '../../stores/user_store';
import CoaActions from '../../actions/coa_actions';
import CoaStore from '../../stores/coa_store';
import HistoryIterator from './history_iterator'; import HistoryIterator from './history_iterator';
import MediaContainer from './media_container'; import MediaContainer from './media_container';
@ -44,6 +39,8 @@ let Edition = React.createClass({
actionPanelButtonListType: React.PropTypes.func, actionPanelButtonListType: React.PropTypes.func,
furtherDetailsType: React.PropTypes.func, furtherDetailsType: React.PropTypes.func,
edition: React.PropTypes.object, edition: React.PropTypes.object,
coaError: React.PropTypes.object,
currentUser: React.PropTypes.object,
loadEdition: React.PropTypes.func loadEdition: React.PropTypes.func
}, },
@ -55,32 +52,6 @@ let Edition = React.createClass({
}; };
}, },
getInitialState() {
return UserStore.getState();
},
componentDidMount() {
UserStore.listen(this.onChange);
UserActions.fetchCurrentUser();
},
componentWillUnmount() {
// Flushing the coa state is essential to not displaying the same
// data to the user while he's on another edition
//
// BUGFIX: Previously we had this line in the componentWillUnmount of
// CoaDetails, but since we're reloading the edition after performing an ACL action
// on it, this resulted in multiple events occupying the dispatcher, which eventually
// resulted in crashing the app.
CoaActions.flushCoa();
UserStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
},
render() { render() {
let FurtherDetailsType = this.props.furtherDetailsType; let FurtherDetailsType = this.props.furtherDetailsType;
@ -101,13 +72,15 @@ let Edition = React.createClass({
<EditionSummary <EditionSummary
actionPanelButtonListType={this.props.actionPanelButtonListType} actionPanelButtonListType={this.props.actionPanelButtonListType}
edition={this.props.edition} edition={this.props.edition}
currentUser={this.state.currentUser} currentUser={this.props.currentUser}
handleSuccess={this.props.loadEdition}/> handleSuccess={this.props.loadEdition}/>
<CollapsibleParagraph <CollapsibleParagraph
title={getLangText('Certificate of Authenticity')} title={getLangText('Certificate of Authenticity')}
show={this.props.edition.acl.acl_coa === true}> show={this.props.edition.acl.acl_coa === true}>
<CoaDetails <CoaDetails
edition={this.props.edition}/> coa={this.props.edition.coa}
coaError={this.props.coaError}
editionId={this.props.edition.bitcoin_id}/>
</CollapsibleParagraph> </CollapsibleParagraph>
<CollapsibleParagraph <CollapsibleParagraph
@ -133,7 +106,7 @@ let Edition = React.createClass({
<CollapsibleParagraph <CollapsibleParagraph
title="Notes" title="Notes"
show={!!(this.state.currentUser.username show={!!(this.props.currentUser.username
|| this.props.edition.acl.acl_edit || this.props.edition.acl.acl_edit
|| this.props.edition.public_note)}> || this.props.edition.public_note)}>
<Note <Note
@ -144,7 +117,7 @@ let Edition = React.createClass({
editable={true} editable={true}
successMessage={getLangText('Private note saved')} successMessage={getLangText('Private note saved')}
url={ApiUrls.note_private_edition} url={ApiUrls.note_private_edition}
currentUser={this.state.currentUser}/> currentUser={this.props.currentUser}/>
<Note <Note
id={() => {return {'bitcoin_id': this.props.edition.bitcoin_id}; }} id={() => {return {'bitcoin_id': this.props.edition.bitcoin_id}; }}
label={getLangText('Personal note (public)')} label={getLangText('Personal note (public)')}
@ -154,7 +127,7 @@ let Edition = React.createClass({
show={!!this.props.edition.public_note || !!this.props.edition.acl.acl_edit} show={!!this.props.edition.public_note || !!this.props.edition.acl.acl_edit}
successMessage={getLangText('Public edition note saved')} successMessage={getLangText('Public edition note saved')}
url={ApiUrls.note_public_edition} url={ApiUrls.note_public_edition}
currentUser={this.state.currentUser}/> currentUser={this.props.currentUser}/>
</CollapsibleParagraph> </CollapsibleParagraph>
<CollapsibleParagraph <CollapsibleParagraph
title={getLangText('Further Details')} title={getLangText('Further Details')}
@ -246,38 +219,42 @@ let EditionSummary = React.createClass({
let CoaDetails = React.createClass({ let CoaDetails = React.createClass({
propTypes: { propTypes: {
edition: React.PropTypes.object editionId: React.PropTypes.string,
coa: React.PropTypes.object,
coaError: React.PropTypes.object
}, },
getInitialState() { contactOnIntercom() {
return CoaStore.getState(); window.Intercom('showNewMessage', `Hi, I'm having problems generating a Certificate of Authenticity for Edition: ${this.props.editionId}`);
}, console.logGlobal(new Error(`Coa couldn't be created for edition: ${this.props.editionId}`));
componentDidMount() {
let { edition } = this.props;
CoaStore.listen(this.onChange);
if(edition.coa) {
CoaActions.fetchOrCreate(edition.coa, edition.bitcoin_id);
}
else {
CoaActions.create(edition.bitcoin_id);
}
},
componentWillUnmount() {
CoaStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
}, },
render() { render() {
if(this.state.coa && this.state.coa.url_safe) { if(this.props.coaError) {
return (
<div className="text-center">
<p>{getLangText('There was an error generating your Certificate of Authenticity.')}</p>
<p>
{getLangText('Try to refresh the page. If this happens repeatedly, please ')}
<a style={{ cursor: 'pointer' }} onClick={this.contactOnIntercom}>{getLangText('contact us')}</a>.
</p>
</div>
);
}
if(this.props.coa && this.props.coa.url_safe) {
return ( return (
<div> <div>
<p className="text-center ascribe-button-list"> <div
<a href={this.state.coa.url_safe} target="_blank"> className="notification-contract-pdf"
style={{paddingBottom: '1em'}}>
<embed
className="embed-form"
src={this.props.coa.url_safe}
alt="pdf"
pluginspage="http://www.adobe.com/products/acrobat/readstep2.html"/>
</div>
<div className="text-center ascribe-button-list">
<a href={this.props.coa.url_safe} target="_blank">
<button className="btn btn-default btn-xs"> <button className="btn btn-default btn-xs">
{getLangText('Download')} <Glyphicon glyph="cloud-download"/> {getLangText('Download')} <Glyphicon glyph="cloud-download"/>
</button> </button>
@ -288,19 +265,21 @@ let CoaDetails = React.createClass({
</button> </button>
</Link> </Link>
</p> </div>
</div> </div>
); );
} else if(typeof this.state.coa === 'string'){ } else if(typeof this.props.coa === 'string'){
return ( return (
<div className="text-center"> <div className="text-center">
{this.state.coa} {this.props.coa}
</div> </div>
); );
} }
return ( return (
<div className="text-center"> <div className="text-center">
<AscribeSpinner color='dark-blue' size='lg'/> <AscribeSpinner color='dark-blue' size='md'/>
<p>{getLangText("Just a sec, we\'re generating your COA")}</p>
<p>{getLangText('(you may leave the page)')}</p>
</div> </div>
); );
} }

View File

@ -9,12 +9,16 @@ import { ResourceNotFoundError } from '../../models/errors';
import EditionActions from '../../actions/edition_actions'; import EditionActions from '../../actions/edition_actions';
import EditionStore from '../../stores/edition_store'; import EditionStore from '../../stores/edition_store';
import UserActions from '../../actions/user_actions';
import UserStore from '../../stores/user_store';
import Edition from './edition'; import Edition from './edition';
import AscribeSpinner from '../ascribe_spinner'; import AscribeSpinner from '../ascribe_spinner';
import { getLangText } from '../../utils/lang_utils'; import { getLangText } from '../../utils/lang_utils';
import { setDocumentTitle } from '../../utils/dom_utils'; import { setDocumentTitle } from '../../utils/dom_utils';
import { mergeOptions } from '../../utils/general_utils';
/** /**
@ -30,33 +34,37 @@ let EditionContainer = React.createClass({
mixins: [History, ReactError], mixins: [History, ReactError],
getInitialState() { getInitialState() {
return EditionStore.getState(); return mergeOptions(
EditionStore.getState(),
UserStore.getState()
);
}, },
componentDidMount() { componentDidMount() {
EditionStore.listen(this.onChange); EditionStore.listen(this.onChange);
UserStore.listen(this.onChange);
// Every time we're entering the edition detail page, // Every time we're entering the edition detail page,
// just reset the edition that is saved in the edition store // just reset the edition that is saved in the edition store
// as it will otherwise display wrong/old data once the user loads // as it will otherwise display wrong/old data once the user loads
// the edition detail a second time // the edition detail a second time
EditionActions.updateEdition({}); EditionActions.flushEdition();
this.loadEdition(); EditionActions.fetchEdition(this.props.params.editionId);
UserActions.fetchCurrentUser();
}, },
// This is done to update the container when the user clicks on the prev or next // This is done to update the container when the user clicks on the prev or next
// button to update the URL parameter (and therefore to switch pieces) // button to update the URL parameter (and therefore to switch pieces)
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
if(this.props.params.editionId !== nextProps.params.editionId) { if(this.props.params.editionId !== nextProps.params.editionId) {
EditionActions.updateEdition({}); EditionActions.fetchEdition(this.props.params.editionId);
EditionActions.fetchOne(nextProps.params.editionId);
} }
}, },
componentDidUpdate() { componentDidUpdate() {
const { editionError } = this.state; const { editionMeta } = this.state;
if(editionMeta.err && editionMeta.err.json && editionMeta.err.json.status === 404) {
if(editionError && editionError.status === 404) {
this.throws(new ResourceNotFoundError(getLangText("Oops, the edition you're looking for doesn't exist."))); this.throws(new ResourceNotFoundError(getLangText("Oops, the edition you're looking for doesn't exist.")));
} }
}, },
@ -64,34 +72,36 @@ let EditionContainer = React.createClass({
componentWillUnmount() { componentWillUnmount() {
window.clearInterval(this.state.timerId); window.clearInterval(this.state.timerId);
EditionStore.unlisten(this.onChange); EditionStore.unlisten(this.onChange);
UserStore.unlisten(this.onChange);
}, },
onChange(state) { onChange(state) {
this.setState(state); this.setState(state);
if (!state.edition.digital_work) {
return;
}
let isEncoding = state.edition.digital_work.isEncoding;
if (state.edition.digital_work.mime === 'video' && typeof isEncoding === 'number' && isEncoding !== 100 && !this.state.timerId) {
let timerId = window.setInterval(() => EditionActions.fetchOne(this.props.params.editionId), 10000);
this.setState({timerId: timerId});
}
},
loadEdition() { if(state && state.edition && state.edition.digital_work) {
EditionActions.fetchOne(this.props.params.editionId); let isEncoding = state.edition.digital_work.isEncoding;
if (state.edition.digital_work.mime === 'video' && typeof isEncoding === 'number' && isEncoding !== 100 && !this.state.timerId) {
let timerId = window.setInterval(() => EditionActions.fetchOne(this.props.params.editionId), 10000);
this.setState({timerId: timerId});
}
}
}, },
render() { render() {
if(this.state.edition && this.state.edition.id) { const { edition, currentUser, coaMeta } = this.state;
setDocumentTitle([this.state.edition.artist_name, this.state.edition.title].join(', ')); const { actionPanelButtonListType, furtherDetailsType } = this.props;
if (Object.keys(edition).length && edition.id && currentUser && currentUser.email) {
setDocumentTitle([edition.artist_name, edition.title].join(', '));
return ( return (
<Edition <Edition
actionPanelButtonListType={this.props.actionPanelButtonListType} actionPanelButtonListType={actionPanelButtonListType}
furtherDetailsType={this.props.furtherDetailsType} furtherDetailsType={furtherDetailsType}
edition={this.state.edition} edition={edition}
loadEdition={this.loadEdition} /> coaError={coaMeta.err}
currentUser={currentUser}
loadEdition={() => EditionActions.fetchEdition(this.props.params.editionId)} />
); );
} else { } else {
return ( return (

View File

@ -17,7 +17,12 @@ import { setDocumentTitle } from '../utils/dom_utils';
let CoaVerifyContainer = React.createClass({ let CoaVerifyContainer = React.createClass({
propTypes: {
location: React.PropTypes.object
},
render() { render() {
const { message, signature } = this.props.location.query;
setDocumentTitle(getLangText('Verify your Certificate of Authenticity')); setDocumentTitle(getLangText('Verify your Certificate of Authenticity'));
return ( return (
@ -27,7 +32,9 @@ let CoaVerifyContainer = React.createClass({
{getLangText('Verify your Certificate of Authenticity')} {getLangText('Verify your Certificate of Authenticity')}
</div> </div>
<CoaVerifyForm /> <CoaVerifyForm
message={message}
signature={signature}/>
<br /> <br />
<br /> <br />
{getLangText('ascribe is using the following public key for verification')}: {getLangText('ascribe is using the following public key for verification')}:
@ -47,6 +54,11 @@ let CoaVerifyContainer = React.createClass({
let CoaVerifyForm = React.createClass({ let CoaVerifyForm = React.createClass({
propTypes: {
message: React.PropTypes.string,
signature: React.PropTypes.string
},
handleSuccess(response){ handleSuccess(response){
let notification = null; let notification = null;
if (response.verdict) { if (response.verdict) {
@ -56,6 +68,8 @@ let CoaVerifyForm = React.createClass({
}, },
render() { render() {
const { message, signature } = this.props;
return ( return (
<div> <div>
<Form <Form
@ -79,6 +93,7 @@ let CoaVerifyForm = React.createClass({
type="text" type="text"
placeholder={getLangText('Copy paste the message on the bottom of your Certificate of Authenticity')} placeholder={getLangText('Copy paste the message on the bottom of your Certificate of Authenticity')}
autoComplete="on" autoComplete="on"
defaultValue={message}
name="username" name="username"
required/> required/>
</Property> </Property>
@ -90,6 +105,7 @@ let CoaVerifyForm = React.createClass({
<InputTextAreaToggable <InputTextAreaToggable
rows={3} rows={3}
placeholder={getLangText('Copy paste the signature on the bottom of your Certificate of Authenticity')} placeholder={getLangText('Copy paste the signature on the bottom of your Certificate of Authenticity')}
defaultValue={signature}
required/> required/>
</Property> </Property>
<hr /> <hr />

View File

@ -1,18 +0,0 @@
'use strict';
import requests from '../utils/requests';
let CoaFetcher = {
/**
* Fetch one user from the API.
* If no arg is supplied, load the current user
*/
fetchOne(id) {
return requests.get('coa', {'id': id});
},
create(bitcoinId) {
return requests.post('coa_create', {body: {'bitcoin_id': bitcoinId}});
}
};
export default CoaFetcher;

View File

@ -1,15 +0,0 @@
'use strict';
import requests from '../utils/requests';
let EditionFetcher = {
/**
* Fetch one user from the API.
* If no arg is supplied, load the current user
*/
fetchOne(editionId) {
return requests.get('edition', {'bitcoin_id': editionId});
}
};
export default EditionFetcher;

27
js/sources/coa_source.js Normal file
View File

@ -0,0 +1,27 @@
'use strict';
import requests from '../utils/requests';
import EditionActions from '../actions/edition_actions';
const CoaSource = {
lookupCoa: {
remote(state) {
return requests.get('coa', { id: state.edition.coa });
},
success: EditionActions.successFetchCoa,
error: EditionActions.errorCoa
},
performCreateCoa: {
remote(state) {
return requests.post('coa_create', {body: { bitcoin_id: state.edition.bitcoin_id }});
},
success: EditionActions.successFetchCoa,
error: EditionActions.errorCoa
}
};
export default CoaSource;

View File

@ -0,0 +1,19 @@
'use strict';
import requests from '../utils/requests';
import EditionActions from '../actions/edition_actions';
const EditionSource = {
lookupEdition: {
remote(state) {
return requests.get('edition', { bitcoin_id: state.editionMeta.idToFetch });
},
success: EditionActions.successFetchEdition,
error: EditionActions.errorEdition
}
};
export default EditionSource;

View File

@ -1,22 +0,0 @@
'use strict';
import { alt } from '../alt';
import CoaActions from '../actions/coa_actions';
class CoaStore {
constructor() {
this.coa = {};
this.bindActions(CoaActions);
}
onUpdateCoa(coa) {
this.coa = coa;
}
onFlushCoa() {
this.coa = {};
}
}
export default alt.createStore(CoaStore, 'CoaStore');

View File

@ -1,23 +1,77 @@
'use strict'; 'use strict';
import { alt } from '../alt'; import { alt } from '../alt';
import EditionActions from '../actions/edition_actions'; import EditionActions from '../actions/edition_actions';
import EditionSource from '../sources/edition_source';
import CoaSource from '../sources/coa_source';
class EditionStore { class EditionStore {
constructor() { constructor() {
this.edition = {}; this.edition = {};
this.editionError = null; this.editionMeta = {
err: null,
idToFetch: null
};
this.coaMeta = {
err: null
};
this.bindActions(EditionActions); this.bindActions(EditionActions);
this.registerAsync(Object.assign(EditionSource, CoaSource));
} }
onUpdateEdition(edition) { onFetchEdition(idToFetch) {
this.edition = edition; this.editionMeta.idToFetch = idToFetch;
this.editionError = null;
this.getInstance().lookupEdition();
} }
onEditionFailed(error) { onSuccessFetchEdition(res) {
this.editionError = error; if(res && res.edition) {
this.edition = res.edition;
this.editionMeta.err = null;
this.editionMeta.idToFetch = null;
if (this.edition.coa && this.edition.acl.acl_coa &&
typeof this.edition.coa.constructor !== Object) {
this.getInstance().lookupCoa();
} else if(!this.edition.coa && this.edition.acl.acl_coa) {
this.getInstance().performCreateCoa();
}
} else {
this.editionMeta.err = new Error('Problem fetching the edition');
}
}
onSuccessFetchCoa(res) {
if (res && res.coa && Object.keys(this.edition).length) {
this.edition.coa = res.coa;
this.coaMeta.err = null;
} else {
this.coaMeta.err = new Error('Problem generating/fetching the COA');
}
}
onFlushEdition() {
this.edition = {};
this.editionMeta = {
err: null,
idToFetch: null
};
this.coaMeta = {
err: null
};
}
onErrorEdition(err) {
this.editionMeta.err = err;
}
onErrorCoa(err) {
this.coaMeta.err = err;
} }
} }

View File

@ -12,7 +12,20 @@ import { argsToQueryParams } from '../utils/url_utils';
class Requests { class Requests {
unpackResponse(response) { unpackResponse(response) {
if (response.status >= 500) { if (response.status >= 500) {
throw new Error(response.status + ' - ' + response.statusText + ' - on URL:' + response.url); let err = new Error(response.status + ' - ' + response.statusText + ' - on URL:' + response.url);
return response
.text()
.then((resText) => {
const resJson = JSON.parse(resText);
err = new Error(resJson.errors.pop());
// ES6 promises don't have a .finally() clause so
// we fake that here by forcing the .catch() clause
// to run
return Promise.reject();
})
.catch(() => { throw err; });
} }
return Q.Promise((resolve, reject) => { return Q.Promise((resolve, reject) => {
@ -50,9 +63,7 @@ class Requests {
resolve({}); resolve({});
} }
} }
}).catch((err) => { }).catch(reject);
reject(err);
});
}); });
} }