1
0
mirror of https://github.com/ascribe/onion.git synced 2024-11-15 09:35:10 +01:00

Merge branch 'master' into AD-1340-add-details-video-upload-errors

Conflicts:
	js/components/ascribe_uploader/react_s3_fine_uploader.js
This commit is contained in:
Tim Daubenschütz 2015-11-16 16:33:39 +01:00
commit c100a3b307
88 changed files with 2185 additions and 920 deletions

View File

@ -29,6 +29,8 @@ Additionally, to work on the white labeling functionality, you need to edit your
127.0.0.1 cyland.localhost.com 127.0.0.1 cyland.localhost.com
127.0.0.1 ikonotv.localhost.com 127.0.0.1 ikonotv.localhost.com
127.0.0.1 sluice.localhost.com 127.0.0.1 sluice.localhost.com
127.0.0.1 lumenus.localhost.com
127.0.0.1 portfolioreview.localhost.com
``` ```
@ -41,6 +43,7 @@ For this project, we're using:
* We don't use ES6's class declaration for React components because it does not support Mixins as well as Autobinding ([Blog post about it](http://facebook.github.io/react/blog/2015/01/27/react-v0.13.0-beta-1.html#autobinding)) * We don't use ES6's class declaration for React components because it does not support Mixins as well as Autobinding ([Blog post about it](http://facebook.github.io/react/blog/2015/01/27/react-v0.13.0-beta-1.html#autobinding))
* We don't use camel case for file naming but in everything Javascript related * We don't use camel case for file naming but in everything Javascript related
* We use `let` instead of `var`: [SA Post](http://stackoverflow.com/questions/762011/javascript-let-keyword-vs-var-keyword) * We use `let` instead of `var`: [SA Post](http://stackoverflow.com/questions/762011/javascript-let-keyword-vs-var-keyword)
* We don't use Javascript's `Date` object, as its interface introduced bugs previously and we're including `momentjs` for other dependencies anyways
Branch names Branch names
===================== =====================

View File

@ -1,6 +1,7 @@
'use strict'; 'use strict';
import React from 'react'; import React from 'react';
import Moment from 'moment';
import Glyphicon from 'react-bootstrap/lib/Glyphicon'; import Glyphicon from 'react-bootstrap/lib/Glyphicon';
import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger'; import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger';
@ -129,7 +130,7 @@ let AccordionListItemWallet = React.createClass({
piece={this.props.content} piece={this.props.content}
subsubheading={ subsubheading={
<div className="pull-left"> <div className="pull-left">
<span>{new Date(this.props.content.date_created).getFullYear()}</span> <span>{Moment(this.props.content.date_created, 'YYYY-MM-DD').year()}</span>
{this.getLicences()} {this.getLicences()}
</div>} </div>}
buttons={ buttons={

View File

@ -1,187 +0,0 @@
'use strict';
import React from 'react';
import ConsignForm from '../ascribe_forms/form_consign';
import UnConsignForm from '../ascribe_forms/form_unconsign';
import TransferForm from '../ascribe_forms/form_transfer';
import LoanForm from '../ascribe_forms/form_loan';
import LoanRequestAnswerForm from '../ascribe_forms/form_loan_request_answer';
import ShareForm from '../ascribe_forms/form_share_email';
import ModalWrapper from '../ascribe_modal/modal_wrapper';
import AppConstants from '../../constants/application_constants';
import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions';
import ApiUrls from '../../constants/api_urls';
import { getAclFormMessage } from '../../utils/form_utils';
import { getLangText } from '../../utils/lang_utils';
let AclButton = React.createClass({
propTypes: {
action: React.PropTypes.oneOf(AppConstants.aclList).isRequired,
availableAcls: React.PropTypes.object.isRequired,
pieceOrEditions: React.PropTypes.oneOfType([
React.PropTypes.object,
React.PropTypes.array
]).isRequired,
currentUser: React.PropTypes.object,
buttonAcceptName: React.PropTypes.string,
buttonAcceptClassName: React.PropTypes.string,
handleSuccess: React.PropTypes.func.isRequired,
className: React.PropTypes.string
},
isPiece(){
return this.props.pieceOrEditions.constructor !== Array;
},
actionProperties(){
let message = getAclFormMessage(this.props.action, this.getTitlesString(), this.props.currentUser.username);
if (this.props.action === 'acl_consign'){
return {
title: getLangText('Consign artwork'),
tooltip: getLangText('Have someone else sell the artwork'),
form: (
<ConsignForm
message={message}
id={this.getFormDataId()}
url={ApiUrls.ownership_consigns}/>
),
handleSuccess: this.showNotification
};
}
if (this.props.action === 'acl_unconsign'){
return {
title: getLangText('Unconsign artwork'),
tooltip: getLangText('Have the owner manage his sales again'),
form: (
<UnConsignForm
message={message}
id={this.getFormDataId()}
url={ApiUrls.ownership_unconsigns}/>
),
handleSuccess: this.showNotification
};
}else if (this.props.action === 'acl_transfer') {
return {
title: getLangText('Transfer artwork'),
tooltip: getLangText('Transfer the ownership of the artwork'),
form: (
<TransferForm
message={message}
id={this.getFormDataId()}
url={ApiUrls.ownership_transfers}/>
),
handleSuccess: this.showNotification
};
}
else if (this.props.action === 'acl_loan'){
return {
title: getLangText('Loan artwork'),
tooltip: getLangText('Loan your artwork for a limited period of time'),
form: (<LoanForm
message={message}
id={this.getFormDataId()}
url={this.isPiece() ? ApiUrls.ownership_loans_pieces : ApiUrls.ownership_loans_editions}/>
),
handleSuccess: this.showNotification
};
}
else if (this.props.action === 'acl_loan_request'){
return {
title: getLangText('Loan artwork'),
tooltip: getLangText('Someone requested you to loan your artwork for a limited period of time'),
form: (<LoanRequestAnswerForm
message={message}
id={this.getFormDataId()}
url={ApiUrls.ownership_loans_pieces_request_confirm}/>
),
handleSuccess: this.showNotification
};
}
else if (this.props.action === 'acl_share'){
return {
title: getLangText('Share artwork'),
tooltip: getLangText('Share the artwork'),
form: (
<ShareForm
message={message}
id={this.getFormDataId()}
url={this.isPiece() ? ApiUrls.ownership_shares_pieces : ApiUrls.ownership_shares_editions }/>
),
handleSuccess: this.showNotification
};
} else {
throw new Error('Your specified action did not match a form.');
}
},
showNotification(response){
this.props.handleSuccess();
if(response.notification) {
let notification = new GlobalNotificationModel(response.notification, 'success');
GlobalNotificationActions.appendGlobalNotification(notification);
}
},
// plz move to share form
getTitlesString(){
if (this.isPiece()){
return '\"' + this.props.pieceOrEditions.title + '\"';
}
else {
return this.props.pieceOrEditions.map(function(edition) {
return '- \"' + edition.title + ', ' + getLangText('edition') + ' ' + edition.edition_number + '\"\n';
}).join('');
}
},
getFormDataId(){
if (this.isPiece()) {
return {piece_id: this.props.pieceOrEditions.id};
}
else {
return {bitcoin_id: this.props.pieceOrEditions.map(function(edition){
return edition.bitcoin_id;
}).join()};
}
},
// Removes the acl_ prefix and converts to upper case
sanitizeAction() {
if (this.props.buttonAcceptName) {
return this.props.buttonAcceptName;
}
return this.props.action.split('acl_')[1].toUpperCase();
},
render() {
if (this.props.availableAcls){
let shouldDisplay = this.props.availableAcls[this.props.action];
let aclProps = this.actionProperties();
let buttonClassName = this.props.buttonAcceptClassName ? this.props.buttonAcceptClassName : '';
return (
<ModalWrapper
trigger={
<button
className={shouldDisplay ? 'btn btn-default btn-sm ' + buttonClassName : 'hidden'}>
{this.sanitizeAction()}
</button>
}
handleSuccess={aclProps.handleSuccess}
title={aclProps.title}>
{aclProps.form}
</ModalWrapper>
);
}
return null;
}
});
export default AclButton;

View File

@ -5,21 +5,25 @@ import React from 'react/addons';
import UserActions from '../../actions/user_actions'; import UserActions from '../../actions/user_actions';
import UserStore from '../../stores/user_store'; import UserStore from '../../stores/user_store';
import AclButton from '../ascribe_buttons/acl_button'; import ConsignButton from './acls/consign_button';
import LoanButton from './acls/loan_button';
import LoanRequestButton from './acls/loan_request_button';
import ShareButton from './acls/share_button';
import TransferButton from './acls/transfer_button';
import UnconsignButton from './acls/unconsign_button';
import { mergeOptions } from '../../utils/general_utils'; import { mergeOptions } from '../../utils/general_utils';
let AclButtonList = React.createClass({ let AclButtonList = React.createClass({
propTypes: { propTypes: {
className: React.PropTypes.string, className: React.PropTypes.string,
editions: React.PropTypes.oneOfType([ pieceOrEditions: React.PropTypes.oneOfType([
React.PropTypes.object, React.PropTypes.object,
React.PropTypes.array React.PropTypes.array
]), ]).isRequired,
availableAcls: React.PropTypes.object, availableAcls: React.PropTypes.object.isRequired,
buttonsStyle: React.PropTypes.object, buttonsStyle: React.PropTypes.object,
handleSuccess: React.PropTypes.func, handleSuccess: React.PropTypes.func.isRequired,
children: React.PropTypes.oneOfType([ children: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.element), React.PropTypes.arrayOf(React.PropTypes.element),
React.PropTypes.element React.PropTypes.element
@ -78,7 +82,7 @@ let AclButtonList = React.createClass({
const { className, const { className,
buttonsStyle, buttonsStyle,
availableAcls, availableAcls,
editions, pieceOrEditions,
handleSuccess } = this.props; handleSuccess } = this.props;
const { currentUser } = this.state; const { currentUser } = this.state;
@ -86,34 +90,29 @@ let AclButtonList = React.createClass({
return ( return (
<div className={className}> <div className={className}>
<span ref="buttonList" style={buttonsStyle}> <span ref="buttonList" style={buttonsStyle}>
<AclButton <ShareButton
availableAcls={availableAcls} availableAcls={availableAcls}
action="acl_share" pieceOrEditions={pieceOrEditions}
pieceOrEditions={editions}
currentUser={currentUser} currentUser={currentUser}
handleSuccess={handleSuccess} /> handleSuccess={handleSuccess} />
<AclButton <TransferButton
availableAcls={availableAcls} availableAcls={availableAcls}
action="acl_transfer" pieceOrEditions={pieceOrEditions}
pieceOrEditions={editions}
currentUser={currentUser} currentUser={currentUser}
handleSuccess={handleSuccess}/> handleSuccess={handleSuccess}/>
<AclButton <ConsignButton
availableAcls={availableAcls} availableAcls={availableAcls}
action="acl_consign" pieceOrEditions={pieceOrEditions}
pieceOrEditions={editions}
currentUser={currentUser} currentUser={currentUser}
handleSuccess={handleSuccess} /> handleSuccess={handleSuccess} />
<AclButton <UnconsignButton
availableAcls={availableAcls} availableAcls={availableAcls}
action="acl_unconsign" pieceOrEditions={pieceOrEditions}
pieceOrEditions={editions}
currentUser={currentUser} currentUser={currentUser}
handleSuccess={handleSuccess} /> handleSuccess={handleSuccess} />
<AclButton <LoanButton
availableAcls={availableAcls} availableAcls={availableAcls}
action="acl_loan" pieceOrEditions={pieceOrEditions}
pieceOrEditions={editions}
currentUser={currentUser} currentUser={currentUser}
handleSuccess={handleSuccess} /> handleSuccess={handleSuccess} />
{this.renderChildren()} {this.renderChildren()}

View File

@ -0,0 +1,78 @@
'use strict';
import React from 'react';
import classNames from 'classnames';
import AclProxy from '../../acl_proxy';
import AclFormFactory from '../../ascribe_forms/acl_form_factory';
import ModalWrapper from '../../ascribe_modal/modal_wrapper';
import AppConstants from '../../../constants/application_constants';
export default function ({ action, displayName, title, tooltip }) {
if (AppConstants.aclList.indexOf(action) < 0) {
console.warn('Your specified aclName did not match a an acl class.');
}
return React.createClass({
displayName: displayName,
propTypes: {
availableAcls: React.PropTypes.object.isRequired,
buttonAcceptName: React.PropTypes.string,
buttonAcceptClassName: React.PropTypes.string,
currentUser: React.PropTypes.object.isRequired,
email: React.PropTypes.string,
pieceOrEditions: React.PropTypes.oneOfType([
React.PropTypes.object,
React.PropTypes.array
]).isRequired,
handleSuccess: React.PropTypes.func.isRequired,
className: React.PropTypes.string
},
// Removes the acl_ prefix and converts to upper case
sanitizeAction() {
if (this.props.buttonAcceptName) {
return this.props.buttonAcceptName;
}
return action.split('acl_')[1].toUpperCase();
},
render() {
const {
availableAcls,
buttonAcceptClassName,
currentUser,
email,
pieceOrEditions,
handleSuccess } = this.props;
return (
<AclProxy
aclName={action}
aclObject={availableAcls}>
<ModalWrapper
trigger={
<button
className={classNames('btn', 'btn-default', 'btn-sm', buttonAcceptClassName)}>
{this.sanitizeAction()}
</button>
}
handleSuccess={handleSuccess}
title={title}>
<AclFormFactory
action={action}
currentUser={currentUser}
email={email}
pieceOrEditions={pieceOrEditions}
showNotification />
</ModalWrapper>
</AclProxy>
);
}
});
}

View File

@ -0,0 +1,14 @@
'use strict';
import React from 'react';
import AclButton from './acl_button';
import { getLangText } from '../../../utils/lang_utils';
export default AclButton({
action: 'acl_consign',
displayName: 'ConsignButton',
title: getLangText('Consign artwork'),
tooltip: getLangText('Have someone else sell the artwork')
});

View File

@ -0,0 +1,14 @@
'use strict';
import React from 'react';
import AclButton from './acl_button';
import { getLangText } from '../../../utils/lang_utils';
export default AclButton({
action: 'acl_loan',
displayName: 'LoanButton',
title: getLangText('Loan artwork'),
tooltip: getLangText('Loan your artwork for a limited period of time')
});

View File

@ -0,0 +1,14 @@
'use strict';
import React from 'react';
import AclButton from './acl_button';
import { getLangText } from '../../../utils/lang_utils';
export default AclButton({
action: 'acl_loan_request',
displayName: 'LoanRequestButton',
title: getLangText('Loan artwork'),
tooltip: getLangText('Someone requested you to loan your artwork for a limited period of time')
});

View File

@ -0,0 +1,14 @@
'use strict';
import React from 'react';
import AclButton from './acl_button';
import { getLangText } from '../../../utils/lang_utils';
export default AclButton({
action: 'acl_share',
displayName: 'ShareButton',
title: getLangText('Share artwork'),
tooltip: getLangText('Share the artwork')
});

View File

@ -0,0 +1,14 @@
'use strict';
import React from 'react';
import AclButton from './acl_button';
import { getLangText } from '../../../utils/lang_utils';
export default AclButton({
action: 'acl_transfer',
displayName: 'TransferButton',
title: getLangText('Transfer artwork'),
tooltip: getLangText('Transfer the ownership of the artwork')
});

View File

@ -0,0 +1,14 @@
'use strict';
import React from 'react';
import AclButton from './acl_button';
import { getLangText } from '../../../utils/lang_utils';
export default AclButton({
action: 'acl_unconsign',
displayName: 'UnconsignButton',
title: getLangText('Unconsign artwork'),
tooltip: getLangText('Have the owner manage his sales again')
});

View File

@ -2,6 +2,7 @@
import React from 'react'; import React from 'react';
import { Link, History } from 'react-router'; import { Link, History } from 'react-router';
import Moment from 'moment';
import Row from 'react-bootstrap/lib/Row'; import Row from 'react-bootstrap/lib/Row';
import Col from 'react-bootstrap/lib/Col'; import Col from 'react-bootstrap/lib/Col';
@ -85,7 +86,7 @@ let Edition = React.createClass({
<hr style={{marginTop: 0}}/> <hr style={{marginTop: 0}}/>
<h1 className="ascribe-detail-title">{this.props.edition.title}</h1> <h1 className="ascribe-detail-title">{this.props.edition.title}</h1>
<EditionDetailProperty label="BY" value={this.props.edition.artist_name} /> <EditionDetailProperty label="BY" value={this.props.edition.artist_name} />
<EditionDetailProperty label="DATE" value={ new Date(this.props.edition.date_created).getFullYear() } /> <EditionDetailProperty label="DATE" value={Moment(this.props.edition.date_created, 'YYYY-MM-DD').year()} />
<hr/> <hr/>
</div> </div>
<EditionSummary <EditionSummary

View File

@ -107,7 +107,7 @@ let EditionActionPanel = React.createClass({
<AclButtonList <AclButtonList
className="ascribe-button-list" className="ascribe-button-list"
availableAcls={edition.acl} availableAcls={edition.acl}
editions={[edition]} pieceOrEditions={[edition]}
handleSuccess={this.handleSuccess}> handleSuccess={this.handleSuccess}>
<AclProxy <AclProxy
aclObject={edition.acl} aclObject={edition.acl}

View File

@ -20,8 +20,7 @@ let FurtherDetailsFileuploader = React.createClass({
submitFile: React.PropTypes.func, submitFile: React.PropTypes.func,
isReadyForFormSubmission: React.PropTypes.func, isReadyForFormSubmission: React.PropTypes.func,
editable: React.PropTypes.bool, editable: React.PropTypes.bool,
multiple: React.PropTypes.bool, multiple: React.PropTypes.bool
location: React.PropTypes.object
}, },
getDefaultProps() { getDefaultProps() {
@ -44,6 +43,7 @@ let FurtherDetailsFileuploader = React.createClass({
return ( return (
<Property <Property
name="other_data_key"
label="Additional files"> label="Additional files">
<ReactS3FineUploader <ReactS3FineUploader
uploadStarted={this.props.uploadStarted} uploadStarted={this.props.uploadStarted}
@ -89,8 +89,7 @@ let FurtherDetailsFileuploader = React.createClass({
}} }}
areAssetsDownloadable={true} areAssetsDownloadable={true}
areAssetsEditable={this.props.editable} areAssetsEditable={this.props.editable}
multiple={this.props.multiple} multiple={this.props.multiple} />
location={this.props.location}/>
</Property> </Property>
); );
} }

View File

@ -92,7 +92,7 @@ let MediaContainer = React.createClass({
aclObject={this.props.content.acl} aclObject={this.props.content.acl}
aclName="acl_download"> aclName="acl_download">
<Button bsSize="xsmall" className="ascribe-margin-1px" href={this.props.content.digital_work.url} target="_blank"> <Button bsSize="xsmall" className="ascribe-margin-1px" href={this.props.content.digital_work.url} target="_blank">
Download <Glyphicon glyph="cloud-download"/> Download .{mimetype} <Glyphicon glyph="cloud-download"/>
</Button> </Button>
</AclProxy> </AclProxy>
{embed} {embed}

View File

@ -2,6 +2,7 @@
import React from 'react'; import React from 'react';
import { History } from 'react-router'; import { History } from 'react-router';
import Moment from 'moment';
import PieceActions from '../../actions/piece_actions'; import PieceActions from '../../actions/piece_actions';
import PieceStore from '../../stores/piece_store'; import PieceStore from '../../stores/piece_store';
@ -201,7 +202,7 @@ let PieceContainer = React.createClass({
<AclButtonList <AclButtonList
className="ascribe-button-list" className="ascribe-button-list"
availableAcls={piece.acl} availableAcls={piece.acl}
editions={piece} pieceOrEditions={piece}
handleSuccess={this.loadPiece}> handleSuccess={this.loadPiece}>
<CreateEditionsButton <CreateEditionsButton
label={getLangText('CREATE EDITIONS')} label={getLangText('CREATE EDITIONS')}
@ -236,7 +237,7 @@ let PieceContainer = React.createClass({
<hr style={{marginTop: 0}}/> <hr style={{marginTop: 0}}/>
<h1 className="ascribe-detail-title">{this.state.piece.title}</h1> <h1 className="ascribe-detail-title">{this.state.piece.title}</h1>
<DetailProperty label="BY" value={this.state.piece.artist_name} /> <DetailProperty label="BY" value={this.state.piece.artist_name} />
<DetailProperty label="DATE" value={ new Date(this.state.piece.date_created).getFullYear() } /> <DetailProperty label="DATE" value={Moment(this.state.piece.date_created, 'YYYY-MM-DD').year() } />
{this.state.piece.num_editions > 0 ? <DetailProperty label="EDITIONS" value={ this.state.piece.num_editions } /> : null} {this.state.piece.num_editions > 0 ? <DetailProperty label="EDITIONS" value={ this.state.piece.num_editions } /> : null}
<hr/> <hr/>
</div> </div>

View File

@ -0,0 +1,128 @@
'use strict';
import React from 'react';
import ConsignForm from '../ascribe_forms/form_consign';
import UnConsignForm from '../ascribe_forms/form_unconsign';
import TransferForm from '../ascribe_forms/form_transfer';
import LoanForm from '../ascribe_forms/form_loan';
import LoanRequestAnswerForm from '../ascribe_forms/form_loan_request_answer';
import ShareForm from '../ascribe_forms/form_share_email';
import AppConstants from '../../constants/application_constants';
import ApiUrls from '../../constants/api_urls';
import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions';
import { getAclFormMessage, getAclFormDataId } from '../../utils/form_utils';
let AclFormFactory = React.createClass({
propTypes: {
action: React.PropTypes.oneOf(AppConstants.aclList).isRequired,
currentUser: React.PropTypes.object.isRequired,
email: React.PropTypes.string,
message: React.PropTypes.string,
pieceOrEditions: React.PropTypes.oneOfType([
React.PropTypes.object,
React.PropTypes.array
]).isRequired,
handleSuccess: React.PropTypes.func,
showNotification: React.PropTypes.bool
},
isPiece() {
return this.props.pieceOrEditions.constructor !== Array;
},
getFormDataId() {
return getAclFormDataId(this.isPiece(), this.props.pieceOrEditions);
},
showSuccessNotification(response) {
if (typeof this.props.handleSuccess === 'function') {
this.props.handleSuccess();
}
if (response.notification) {
const notification = new GlobalNotificationModel(response.notification, 'success');
GlobalNotificationActions.appendGlobalNotification(notification);
}
},
render() {
const {
action,
pieceOrEditions,
currentUser,
email,
message,
handleSuccess,
showNotification } = this.props;
const formMessage = message || getAclFormMessage({
aclName: action,
entities: pieceOrEditions,
isPiece: this.isPiece(),
senderName: currentUser.username
});
if (action === 'acl_consign') {
return (
<ConsignForm
email={email}
message={formMessage}
id={this.getFormDataId()}
url={ApiUrls.ownership_consigns}
handleSuccess={showNotification ? this.showSuccessNotification : handleSuccess} />
);
} else if (action === 'acl_unconsign') {
return (
<UnConsignForm
message={formMessage}
id={this.getFormDataId()}
url={ApiUrls.ownership_unconsigns}
handleSuccess={showNotification ? this.showSuccessNotification : handleSuccess} />
);
} else if (action === 'acl_transfer') {
return (
<TransferForm
message={formMessage}
id={this.getFormDataId()}
url={ApiUrls.ownership_transfers}
handleSuccess={showNotification ? this.showSuccessNotification : handleSuccess} />
);
} else if (action === 'acl_loan') {
return (
<LoanForm
email={email}
message={formMessage}
id={this.getFormDataId()}
url={this.isPiece() ? ApiUrls.ownership_loans_pieces
: ApiUrls.ownership_loans_editions}
handleSuccess={showNotification ? this.showSuccessNotification : handleSuccess} />
);
} else if (action === 'acl_loan_request') {
return (
<LoanRequestAnswerForm
message={formMessage}
id={this.getFormDataId()}
url={ApiUrls.ownership_loans_pieces_request_confirm}
handleSuccess={showNotification ? this.showSuccessNotification : handleSuccess} />
);
} else if (action === 'acl_share') {
return (
<ShareForm
message={formMessage}
id={this.getFormDataId()}
url={this.isPiece() ? ApiUrls.ownership_shares_pieces
: ApiUrls.ownership_shares_editions}
handleSuccess={showNotification ? this.showSuccessNotification : handleSuccess} />
);
} else {
throw new Error('Your specified action did not match a form.');
}
}
});
export default AclFormFactory;

View File

@ -12,7 +12,7 @@ import GlobalNotificationActions from '../../actions/global_notification_actions
import requests from '../../utils/requests'; import requests from '../../utils/requests';
import { getLangText } from '../../utils/lang_utils'; import { getLangText } from '../../utils/lang_utils';
import { mergeOptionsWithDuplicates } from '../../utils/general_utils'; import { sanitize } from '../../utils/general_utils';
let Form = React.createClass({ let Form = React.createClass({
@ -124,12 +124,12 @@ let Form = React.createClass({
getFormData() { getFormData() {
let data = {}; let data = {};
for(let ref in this.refs) { for (let ref in this.refs) {
data[this.refs[ref].props.name] = this.refs[ref].state.value; data[this.refs[ref].props.name] = this.refs[ref].state.value;
} }
if(typeof this.props.getFormData === 'function') { if (typeof this.props.getFormData === 'function') {
data = mergeOptionsWithDuplicates(data, this.props.getFormData()); data = Object.assign(data, this.props.getFormData());
} }
return data; return data;
@ -155,7 +155,7 @@ let Form = React.createClass({
}); });
}, },
handleError(err){ handleError(err) {
if (err.json) { if (err.json) {
for (let input in err.json.errors){ for (let input in err.json.errors){
if (this.refs && this.refs[input] && this.refs[input].state) { if (this.refs && this.refs[input] && this.refs[input].state) {
@ -185,7 +185,7 @@ let Form = React.createClass({
this.setState({submitted: false}); this.setState({submitted: false});
}, },
clearErrors(){ clearErrors() {
for(let ref in this.refs){ for(let ref in this.refs){
if (this.refs[ref] && typeof this.refs[ref].clearErrors === 'function'){ if (this.refs[ref] && typeof this.refs[ref].clearErrors === 'function'){
this.refs[ref].clearErrors(); this.refs[ref].clearErrors();
@ -236,12 +236,12 @@ let Form = React.createClass({
}, },
renderChildren() { renderChildren() {
return ReactAddons.Children.map(this.props.children, (child) => { return ReactAddons.Children.map(this.props.children, (child, i) => {
if (child) { if (child) {
return ReactAddons.addons.cloneWithProps(child, { return ReactAddons.addons.cloneWithProps(child, {
handleChange: this.handleChangeChild, handleChange: this.handleChangeChild,
ref: child.props.name, ref: child.props.name,
key: i,
// We need this in order to make editable be overridable when setting it directly // We need this in order to make editable be overridable when setting it directly
// on Property // on Property
editable: child.props.overrideForm ? child.props.editable : !this.props.disabled editable: child.props.overrideForm ? child.props.editable : !this.props.disabled
@ -269,6 +269,83 @@ let Form = React.createClass({
} }
}, },
/**
* Validates a single ref and returns a human-readable error message
* @param {object} refToValidate A customly constructed object to check
* @return {oneOfType([arrayOf(string), bool])} Either an error message or false, saying that
* everything is valid
*/
_hasRefErrors(refToValidate) {
let errors = Object
.keys(refToValidate)
.reduce((a, constraintKey) => {
const contraintValue = refToValidate[constraintKey];
if(!contraintValue) {
switch(constraintKey) {
case 'min' || 'max':
a.push(getLangText('The field you defined is not in the valid range'));
break;
case 'pattern':
a.push(getLangText('The value you defined is not matching the valid pattern'));
break;
case 'required':
a.push(getLangText('This field is required'));
break;
}
}
return a;
}, []);
return errors.length ? errors : false;
},
/**
* This method validates all child inputs of the form.
*
* As of now, it only considers
* - `max`
* - `min`
* - `pattern`
* - `required`
*
* The idea is to enhance this method everytime we need more thorough validation.
* So feel free to add props that additionally should be checked, if they're present
* in the input's props.
*
* @return {[type]} [description]
*/
validate() {
this.clearErrors();
const validatedFormInputs = {};
Object
.keys(this.refs)
.forEach((refName) => {
let refToValidate = {};
const property = this.refs[refName];
const input = property.refs.input;
const value = input.getDOMNode().value || input.state.value;
const { max,
min,
pattern,
required,
type } = input.props;
refToValidate.required = required ? value : true;
refToValidate.pattern = pattern && typeof value === 'string' ? value.match(pattern) : true;
refToValidate.max = type === 'number' ? parseInt(value, 10) <= max : true;
refToValidate.min = type === 'number' ? parseInt(value, 10) >= min : true;
const validatedRef = this._hasRefErrors(refToValidate);
validatedFormInputs[refName] = validatedRef;
});
const errorMessagesForRefs = sanitize(validatedFormInputs, (val) => !val);
this.handleError({ json: { errors: errorMessagesForRefs } });
return !Object.keys(errorMessagesForRefs).length;
},
render() { render() {
let className = 'ascribe-form'; let className = 'ascribe-form';

View File

@ -28,8 +28,7 @@ let CreateContractForm = React.createClass({
fileClassToUpload: React.PropTypes.shape({ fileClassToUpload: React.PropTypes.shape({
singular: React.PropTypes.string, singular: React.PropTypes.string,
plural: React.PropTypes.string plural: React.PropTypes.string
}), })
location: React.PropTypes.object
}, },
getInitialState() { getInitialState() {
@ -87,8 +86,7 @@ let CreateContractForm = React.createClass({
areAssetsEditable={true} areAssetsEditable={true}
setIsUploadReady={this.setIsUploadReady} setIsUploadReady={this.setIsUploadReady}
isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile} isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile}
fileClassToUpload={this.props.fileClassToUpload} fileClassToUpload={this.props.fileClassToUpload} />
location={this.props.location}/>
</Property> </Property>
<Property <Property
name='name' name='name'

View File

@ -26,12 +26,15 @@ let RegisterPieceForm = React.createClass({
isFineUploaderActive: React.PropTypes.bool, isFineUploaderActive: React.PropTypes.bool,
isFineUploaderEditable: React.PropTypes.bool, isFineUploaderEditable: React.PropTypes.bool,
enableLocalHashing: React.PropTypes.bool, enableLocalHashing: React.PropTypes.bool,
children: React.PropTypes.element,
onLoggedOut: React.PropTypes.func, onLoggedOut: React.PropTypes.func,
// For this form to work with SlideContainer, we sometimes have to disable it // For this form to work with SlideContainer, we sometimes have to disable it
disabled: React.PropTypes.bool, disabled: React.PropTypes.bool,
location: React.PropTypes.object location: React.PropTypes.object,
children: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.element),
React.PropTypes.element
])
}, },
getDefaultProps() { getDefaultProps() {
@ -116,7 +119,7 @@ let RegisterPieceForm = React.createClass({
onLoggedOut={this.props.onLoggedOut} onLoggedOut={this.props.onLoggedOut}
disabled={!this.props.isFineUploaderEditable} disabled={!this.props.isFineUploaderEditable}
enableLocalHashing={enableLocalHashing} enableLocalHashing={enableLocalHashing}
location={this.props.location}/> uploadMethod={this.props.location.query.method} />
</Property> </Property>
<Property <Property
name='artist_name' name='artist_name'

View File

@ -2,10 +2,13 @@
import React from 'react'; import React from 'react';
import AclButton from './../ascribe_buttons/acl_button';
import ActionPanel from '../ascribe_panel/action_panel';
import Form from './form'; import Form from './form';
import LoanRequestButton from '../ascribe_buttons/acls/loan_request_button';
import UnconsignButton from '../ascribe_buttons/acls/unconsign_button';
import ActionPanel from '../ascribe_panel/action_panel';
import NotificationActions from '../../actions/notification_actions'; import NotificationActions from '../../actions/notification_actions';
import GlobalNotificationModel from '../../models/global_notification_model'; import GlobalNotificationModel from '../../models/global_notification_model';
@ -13,9 +16,9 @@ import GlobalNotificationActions from '../../actions/global_notification_actions
import ApiUrls from '../../constants/api_urls'; import ApiUrls from '../../constants/api_urls';
import { getAclFormDataId } from '../../utils/form_utils';
import { getLangText } from '../../utils/lang_utils.js'; import { getLangText } from '../../utils/lang_utils.js';
let RequestActionForm = React.createClass({ let RequestActionForm = React.createClass({
propTypes: { propTypes: {
pieceOrEditions: React.PropTypes.oneOfType([ pieceOrEditions: React.PropTypes.oneOfType([
@ -27,26 +30,26 @@ let RequestActionForm = React.createClass({
handleSuccess: React.PropTypes.func handleSuccess: React.PropTypes.func
}, },
isPiece(){ isPiece() {
return this.props.pieceOrEditions.constructor !== Array; return this.props.pieceOrEditions.constructor !== Array;
}, },
getUrls() { getUrls() {
let urls = {}; let urls = {};
if (this.props.notifications.action === 'consign'){ if (this.props.notifications.action === 'consign') {
urls.accept = ApiUrls.ownership_consigns_confirm; urls.accept = ApiUrls.ownership_consigns_confirm;
urls.deny = ApiUrls.ownership_consigns_deny; urls.deny = ApiUrls.ownership_consigns_deny;
} else if (this.props.notifications.action === 'unconsign'){ } else if (this.props.notifications.action === 'unconsign') {
urls.accept = ApiUrls.ownership_unconsigns; urls.accept = ApiUrls.ownership_unconsigns;
urls.deny = ApiUrls.ownership_unconsigns_deny; urls.deny = ApiUrls.ownership_unconsigns_deny;
} else if (this.props.notifications.action === 'loan' && !this.isPiece()){ } else if (this.props.notifications.action === 'loan' && !this.isPiece()) {
urls.accept = ApiUrls.ownership_loans_confirm; urls.accept = ApiUrls.ownership_loans_confirm;
urls.deny = ApiUrls.ownership_loans_deny; urls.deny = ApiUrls.ownership_loans_deny;
} else if (this.props.notifications.action === 'loan' && this.isPiece()){ } else if (this.props.notifications.action === 'loan' && this.isPiece()) {
urls.accept = ApiUrls.ownership_loans_pieces_confirm; urls.accept = ApiUrls.ownership_loans_pieces_confirm;
urls.deny = ApiUrls.ownership_loans_pieces_deny; urls.deny = ApiUrls.ownership_loans_pieces_deny;
} else if (this.props.notifications.action === 'loan_request' && this.isPiece()){ } else if (this.props.notifications.action === 'loan_request' && this.isPiece()) {
urls.accept = ApiUrls.ownership_loans_pieces_request_confirm; urls.accept = ApiUrls.ownership_loans_pieces_request_confirm;
urls.deny = ApiUrls.ownership_loans_pieces_request_deny; urls.deny = ApiUrls.ownership_loans_pieces_request_deny;
} }
@ -54,37 +57,28 @@ let RequestActionForm = React.createClass({
return urls; return urls;
}, },
getFormData(){ getFormData() {
if (this.isPiece()) { return getAclFormDataId(this.isPiece(), this.props.pieceOrEditions);
return {piece_id: this.props.pieceOrEditions.id};
}
else {
return {bitcoin_id: this.props.pieceOrEditions.map(function(edition){
return edition.bitcoin_id;
}).join()};
}
}, },
showNotification(option, action, owner) { showNotification(option, action, owner) {
return () => { return () => {
let message = getLangText('You have successfully') + ' ' + option + ' the ' + action + ' request ' + getLangText('from') + ' ' + owner; const message = getLangText('You have successfully %s the %s request from %s', getLangText(option), getLangText(action), owner);
const notifications = new GlobalNotificationModel(message, 'success');
let notifications = new GlobalNotificationModel(message, 'success');
GlobalNotificationActions.appendGlobalNotification(notifications); GlobalNotificationActions.appendGlobalNotification(notifications);
this.handleSuccess(); this.handleSuccess();
}; };
}, },
handleSuccess() { handleSuccess() {
if (this.isPiece()){ if (this.isPiece()) {
NotificationActions.fetchPieceListNotifications(); NotificationActions.fetchPieceListNotifications();
} } else {
else {
NotificationActions.fetchEditionListNotifications(); NotificationActions.fetchEditionListNotifications();
} }
if(this.props.handleSuccess) {
if (typeof this.props.handleSuccess === 'function') {
this.props.handleSuccess(); this.props.handleSuccess();
} }
}, },
@ -98,21 +92,19 @@ let RequestActionForm = React.createClass({
}, },
getAcceptButtonForm(urls) { getAcceptButtonForm(urls) {
if(this.props.notifications.action === 'unconsign') { if (this.props.notifications.action === 'unconsign') {
return ( return (
<AclButton <UnconsignButton
availableAcls={{'acl_unconsign': true}} availableAcls={{'acl_unconsign': true}}
action="acl_unconsign"
buttonAcceptClassName='inline pull-right btn-sm ascribe-margin-1px' buttonAcceptClassName='inline pull-right btn-sm ascribe-margin-1px'
pieceOrEditions={this.props.pieceOrEditions} pieceOrEditions={this.props.pieceOrEditions}
currentUser={this.props.currentUser} currentUser={this.props.currentUser}
handleSuccess={this.handleSuccess} /> handleSuccess={this.handleSuccess} />
); );
} else if(this.props.notifications.action === 'loan_request') { } else if (this.props.notifications.action === 'loan_request') {
return ( return (
<AclButton <LoanRequestButton
availableAcls={{'acl_loan_request': true}} availableAcls={{'acl_loan_request': true}}
action="acl_loan_request"
buttonAcceptName="LOAN" buttonAcceptName="LOAN"
buttonAcceptClassName='inline pull-right btn-sm ascribe-margin-1px' buttonAcceptClassName='inline pull-right btn-sm ascribe-margin-1px'
pieceOrEditions={this.props.pieceOrEditions} pieceOrEditions={this.props.pieceOrEditions}
@ -125,7 +117,7 @@ let RequestActionForm = React.createClass({
url={urls.accept} url={urls.accept}
getFormData={this.getFormData} getFormData={this.getFormData}
handleSuccess={ handleSuccess={
this.showNotification(getLangText('accepted'), this.props.notifications.action, this.props.notifications.by) this.showNotification('accepted', this.props.notifications.action, this.props.notifications.by)
} }
isInline={true} isInline={true}
className='inline pull-right'> className='inline pull-right'>
@ -140,8 +132,8 @@ let RequestActionForm = React.createClass({
}, },
getButtonForm() { getButtonForm() {
let urls = this.getUrls(); const urls = this.getUrls();
let acceptButtonForm = this.getAcceptButtonForm(urls); const acceptButtonForm = this.getAcceptButtonForm(urls);
return ( return (
<div> <div>
@ -150,7 +142,7 @@ let RequestActionForm = React.createClass({
isInline={true} isInline={true}
getFormData={this.getFormData} getFormData={this.getFormData}
handleSuccess={ handleSuccess={
this.showNotification(getLangText('denied'), this.props.notifications.action, this.props.notifications.by) this.showNotification('denied', this.props.notifications.action, this.props.notifications.by)
} }
className='inline pull-right'> className='inline pull-right'>
<button <button
@ -168,7 +160,7 @@ let RequestActionForm = React.createClass({
return ( return (
<ActionPanel <ActionPanel
content={this.getContent()} content={this.getContent()}
buttons={this.getButtonForm()}/> buttons={this.getButtonForm()} />
); );
} }
}); });

View File

@ -3,64 +3,80 @@
import React from 'react'; import React from 'react';
import ReactS3FineUploader from '../ascribe_uploader/react_s3_fine_uploader'; import ReactS3FineUploader from '../ascribe_uploader/react_s3_fine_uploader';
import FileDragAndDrop from '../ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop';
import AppConstants from '../../constants/application_constants'; import AppConstants from '../../constants/application_constants';
import { getCookie } from '../../utils/fetch_api_utils'; import { getCookie } from '../../utils/fetch_api_utils';
let InputFineUploader = React.createClass({
const { func, bool, object, shape, string, number, arrayOf } = React.PropTypes;
const InputFineUploader = React.createClass({
propTypes: { propTypes: {
setIsUploadReady: React.PropTypes.func, setIsUploadReady: func,
isReadyForFormSubmission: React.PropTypes.func, isReadyForFormSubmission: func,
submitFileName: React.PropTypes.func, submitFileName: func,
fileInputElement: func,
areAssetsDownloadable: React.PropTypes.bool, areAssetsDownloadable: bool,
onClick: React.PropTypes.func, keyRoutine: shape({
keyRoutine: React.PropTypes.shape({ url: string,
url: React.PropTypes.string, fileClass: string
fileClass: React.PropTypes.string
}), }),
createBlobRoutine: React.PropTypes.shape({ createBlobRoutine: shape({
url: React.PropTypes.string url: string
}), }),
validation: React.PropTypes.shape({ validation: shape({
itemLimit: React.PropTypes.number, itemLimit: number,
sizeLimit: React.PropTypes.string, sizeLimit: string,
allowedExtensions: React.PropTypes.arrayOf(React.PropTypes.string) allowedExtensions: arrayOf(string)
}), }),
// isFineUploaderActive is used to lock react fine uploader in case // isFineUploaderActive is used to lock react fine uploader in case
// a user is actually not logged in already to prevent him from droping files // a user is actually not logged in already to prevent him from droping files
// before login in // before login in
isFineUploaderActive: React.PropTypes.bool, isFineUploaderActive: bool,
onLoggedOut: React.PropTypes.func, onLoggedOut: func,
enableLocalHashing: React.PropTypes.bool, enableLocalHashing: bool,
uploadMethod: string,
// provided by Property // provided by Property
disabled: React.PropTypes.bool, disabled: bool,
// A class of a file the user has to upload // A class of a file the user has to upload
// Needs to be defined both in singular as well as in plural // Needs to be defined both in singular as well as in plural
fileClassToUpload: React.PropTypes.shape({ fileClassToUpload: shape({
singular: React.PropTypes.string, singular: string,
plural: React.PropTypes.string plural: string
}), })
location: React.PropTypes.object },
getDefaultProps() {
return {
fileInputElement: FileDragAndDrop
};
}, },
getInitialState() { getInitialState() {
return { return {
value: null value: null,
file: null
}; };
}, },
submitFile(file){ submitFile(file) {
this.setState({ this.setState({
file,
value: file.key value: file.key
}); });
if(this.state.value && typeof this.props.onChange === 'function') {
this.props.onChange({ target: { value: this.state.value } });
}
if(typeof this.props.submitFileName === 'function') { if(typeof this.props.submitFileName === 'function') {
this.props.submitFileName(file.originalName); this.props.submitFileName(file.originalName);
} }
@ -70,7 +86,25 @@ let InputFineUploader = React.createClass({
this.refs.fineuploader.reset(); this.refs.fineuploader.reset();
}, },
createBlobRoutine() {
const { fineuploader } = this.refs;
const { file } = this.state;
fineuploader.createBlob(file);
},
render() { render() {
const { fileInputElement,
keyRoutine,
createBlobRoutine,
validation,
setIsUploadReady,
isReadyForFormSubmission,
areAssetsDownloadable,
onLoggedOut,
enableLocalHashing,
fileClassToUpload,
location } = this.props;
let editable = this.props.isFineUploaderActive; let editable = this.props.isFineUploaderActive;
// if disabled is actually set by property, we want to override // if disabled is actually set by property, we want to override
@ -82,14 +116,14 @@ let InputFineUploader = React.createClass({
return ( return (
<ReactS3FineUploader <ReactS3FineUploader
ref="fineuploader" ref="fineuploader"
onClick={this.props.onClick} fileInputElement={fileInputElement}
keyRoutine={this.props.keyRoutine} keyRoutine={keyRoutine}
createBlobRoutine={this.props.createBlobRoutine} createBlobRoutine={createBlobRoutine}
validation={this.props.validation} validation={validation}
submitFile={this.submitFile} submitFile={this.submitFile}
setIsUploadReady={this.props.setIsUploadReady} setIsUploadReady={setIsUploadReady}
isReadyForFormSubmission={this.props.isReadyForFormSubmission} isReadyForFormSubmission={isReadyForFormSubmission}
areAssetsDownloadable={this.props.areAssetsDownloadable} areAssetsDownloadable={areAssetsDownloadable}
areAssetsEditable={editable} areAssetsEditable={editable}
signature={{ signature={{
endpoint: AppConstants.serverUrl + 's3/signature/', endpoint: AppConstants.serverUrl + 's3/signature/',
@ -107,8 +141,8 @@ let InputFineUploader = React.createClass({
}} }}
onInactive={this.props.onLoggedOut} onInactive={this.props.onLoggedOut}
enableLocalHashing={this.props.enableLocalHashing} enableLocalHashing={this.props.enableLocalHashing}
fileClassToUpload={this.props.fileClassToUpload} uploadMethod={this.props.uploadMethod}
location={this.props.location}/> fileClassToUpload={this.props.fileClassToUpload} />
); );
} }
}); });

View File

@ -181,9 +181,7 @@ let Property = React.createClass({
setErrors(errors){ setErrors(errors){
this.setState({ this.setState({
errors: errors.map((error) => { errors: errors.pop()
return <span className="pull-right" key={error}>{error}</span>;
})
}); });
}, },
@ -255,8 +253,10 @@ let Property = React.createClass({
placement="top" placement="top"
overlay={tooltip}> overlay={tooltip}>
<div className={'ascribe-property ' + this.props.className}> <div className={'ascribe-property ' + this.props.className}>
{this.state.errors} <p>
<span>{this.props.label}</span> <span className="pull-left">{this.props.label}</span>
<span className="pull-right">{this.state.errors}</span>
</p>
{this.renderChildren(style)} {this.renderChildren(style)}
{footer} {footer}
</div> </div>

View File

@ -50,25 +50,35 @@ let Other = React.createClass({
let Image = React.createClass({ let Image = React.createClass({
propTypes: { propTypes: {
url: React.PropTypes.string.isRequired, url: React.PropTypes.string,
preview: React.PropTypes.string.isRequired preview: React.PropTypes.string.isRequired
}, },
mixins: [InjectInHeadMixin], mixins: [InjectInHeadMixin],
componentDidMount() { componentDidMount() {
if(this.props.url) {
this.inject('https://code.jquery.com/jquery-2.1.4.min.js') this.inject('https://code.jquery.com/jquery-2.1.4.min.js')
.then(() => .then(() =>
Q.all([ Q.all([
this.inject(AppConstants.baseUrl + 'static/thirdparty/shmui/shmui.css'), this.inject(AppConstants.baseUrl + 'static/thirdparty/shmui/shmui.css'),
this.inject(AppConstants.baseUrl + 'static/thirdparty/shmui/jquery.shmui.js') this.inject(AppConstants.baseUrl + 'static/thirdparty/shmui/jquery.shmui.js')
]).then(() => { window.jQuery('.shmui-ascribe').shmui(); })); ]).then(() => { window.jQuery('.shmui-ascribe').shmui(); }));
}
}, },
render() { render() {
const { url, preview } = this.props;
if(url) {
return ( return (
<img className="shmui-ascribe" src={this.props.preview} data-large-src={this.props.url}/> <img className="shmui-ascribe" src={preview} data-large-src={url}/>
); );
} else {
return (
<img src={preview}/>
);
}
} }
}); });
@ -202,26 +212,50 @@ let MediaPlayer = React.createClass({
}, },
render() { render() {
if (this.props.mimetype === 'video' && this.props.encodingStatus !== undefined && this.props.encodingStatus !== 100) { const { mimetype,
preview,
url,
extraData,
encodingStatus } = this.props;
if (mimetype === 'video' && encodingStatus !== undefined && encodingStatus !== 100) {
return ( return (
<div className="ascribe-detail-header ascribe-media-player"> <div className="ascribe-detail-header ascribe-media-player">
<p> <p>
<em>We successfully received your video and it is now being encoded. <em>We successfully received your video and it is now being encoded.
<br />You can leave this page and check back on the status later.</em> <br />You can leave this page and check back on the status later.</em>
</p> </p>
<ProgressBar now={this.props.encodingStatus} <ProgressBar now={encodingStatus}
label="%(percent)s%" label="%(percent)s%"
className="ascribe-progress-bar" /> className="ascribe-progress-bar" />
</div> </div>
); );
} else { } else {
let Component = resourceMap[this.props.mimetype] || Other; let Component = resourceMap[mimetype] || Other;
let componentProps = {
preview,
url,
extraData,
encodingStatus
};
// Since the launch of the portfolio whitelabel submission,
// we allow the user to specify a thumbnail upon piece-registration.
// As the `Component` is chosen according to its filetype but could potentially
// have a manually submitted thumbnail, we match if the to `Mediaplayer` submitted thumbnail
// is not the generally used fallback `url` (ascribe_spiral.png).
//
// If this is the case, we disable shmui by deleting the original `url` prop and replace
// the assigned component to `Image`.
if(!decodeURIComponent(preview).match(/https:\/\/.*\/media\/thumbnails\/ascribe_spiral.png/) &&
Component === Other) {
Component = resourceMap.image;
delete componentProps.url;
}
return ( return (
<div className="ascribe-media-player"> <div className="ascribe-media-player">
<Component preview={this.props.preview} <Component {...componentProps}/>
url={this.props.url}
extraData={this.props.extraData}
encodingStatus={this.props.encodingStatus} />
</div> </div>
); );
} }

View File

@ -117,7 +117,7 @@ let PieceListBulkModal = React.createClass({
<div className="row-fluid"> <div className="row-fluid">
<AclButtonList <AclButtonList
availableAcls={availableAcls} availableAcls={availableAcls}
editions={selectedEditions} pieceOrEditions={selectedEditions}
handleSuccess={this.handleSuccess} handleSuccess={this.handleSuccess}
className="text-center ascribe-button-list collapse-group"> className="text-center ascribe-button-list collapse-group">
<DeleteButton <DeleteButton

View File

@ -31,6 +31,8 @@ export default function AuthProxyHandler({to, when}) {
return (Component) => { return (Component) => {
return React.createClass({ return React.createClass({
displayName: 'AuthProxyHandler',
propTypes: { propTypes: {
location: object location: object
}, },

View File

@ -20,8 +20,7 @@ import { getLangText } from '../../utils/lang_utils';
let ContractSettingsUpdateButton = React.createClass({ let ContractSettingsUpdateButton = React.createClass({
propTypes: { propTypes: {
contract: React.PropTypes.object, contract: React.PropTypes.object
location: React.PropTypes.object
}, },
submitFile(file) { submitFile(file) {
@ -56,7 +55,6 @@ let ContractSettingsUpdateButton = React.createClass({
render() { render() {
return ( return (
<ReactS3FineUploader <ReactS3FineUploader
ref="fineuploader"
fileInputElement={UploadButton} fileInputElement={UploadButton}
keyRoutine={{ keyRoutine={{
url: AppConstants.serverUrl + 's3/key/', url: AppConstants.serverUrl + 's3/key/',
@ -90,8 +88,7 @@ let ContractSettingsUpdateButton = React.createClass({
plural: getLangText('UPDATE') plural: getLangText('UPDATE')
}} }}
isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile} isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile}
submitFile={this.submitFile} submitFile={this.submitFile} />
location={this.props.location}/>
); );
} }
}); });

View File

@ -27,6 +27,7 @@ let FileDragAndDrop = React.createClass({
areAssetsEditable: React.PropTypes.bool, areAssetsEditable: React.PropTypes.bool,
enableLocalHashing: React.PropTypes.bool, enableLocalHashing: React.PropTypes.bool,
uploadMethod: React.PropTypes.string,
// triggers a FileDragAndDrop-global spinner // triggers a FileDragAndDrop-global spinner
hashingProgress: React.PropTypes.number, hashingProgress: React.PropTypes.number,
@ -41,8 +42,7 @@ let FileDragAndDrop = React.createClass({
plural: React.PropTypes.string plural: React.PropTypes.string
}), }),
allowedExtensions: React.PropTypes.string, allowedExtensions: React.PropTypes.string
location: React.PropTypes.object
}, },
clearSelection() { clearSelection() {
@ -141,19 +141,19 @@ let FileDragAndDrop = React.createClass({
}, },
render: function () { render: function () {
let { filesToUpload, const {
filesToUpload,
dropzoneInactive, dropzoneInactive,
className, className,
hashingProgress, hashingProgress,
handleCancelHashing, handleCancelHashing,
multiple, multiple,
enableLocalHashing, enableLocalHashing,
uploadMethod,
fileClassToUpload, fileClassToUpload,
areAssetsDownloadable, areAssetsDownloadable,
areAssetsEditable, areAssetsEditable,
allowedExtensions, allowedExtensions } = this.props;
location
} = this.props;
// has files only is true if there are files that do not have the status deleted or canceled // has files only is true if there are files that do not have the status deleted or canceled
let hasFiles = filesToUpload.filter((file) => file.status !== 'deleted' && file.status !== 'canceled' && file.size !== -1).length > 0; let hasFiles = filesToUpload.filter((file) => file.status !== 'deleted' && file.status !== 'canceled' && file.size !== -1).length > 0;
@ -189,8 +189,8 @@ let FileDragAndDrop = React.createClass({
hasFiles={hasFiles} hasFiles={hasFiles}
onClick={this.handleOnClick} onClick={this.handleOnClick}
enableLocalHashing={enableLocalHashing} enableLocalHashing={enableLocalHashing}
fileClassToUpload={fileClassToUpload} uploadMethod={uploadMethod}
location={location}/> fileClassToUpload={fileClassToUpload} />
<FileDragAndDropPreviewIterator <FileDragAndDropPreviewIterator
files={filesToUpload} files={filesToUpload}
handleDeleteFile={this.handleDeleteFile} handleDeleteFile={this.handleDeleteFile}

View File

@ -3,30 +3,28 @@
import React from 'react'; import React from 'react';
import { Link } from 'react-router'; import { Link } from 'react-router';
import { getLangText } from '../../../utils/lang_utils';
import { dragAndDropAvailable } from '../../../utils/feature_detection_utils'; import { dragAndDropAvailable } from '../../../utils/feature_detection_utils';
import { getLangText } from '../../../utils/lang_utils';
import { getCurrentQueryParams } from '../../../utils/url_utils';
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,
enableLocalHashing: React.PropTypes.bool, enableLocalHashing: React.PropTypes.bool,
uploadMethod: React.PropTypes.string,
onClick: React.PropTypes.func,
// A class of a file the user has to upload // A class of a file the user has to upload
// Needs to be defined both in singular as well as in plural // Needs to be defined both in singular as well as in plural
fileClassToUpload: React.PropTypes.shape({ fileClassToUpload: React.PropTypes.shape({
singular: React.PropTypes.string, singular: React.PropTypes.string,
plural: React.PropTypes.string plural: React.PropTypes.string
}), })
location: React.PropTypes.object
}, },
getDragDialog(fileClass) { getDragDialog(fileClass) {
if(dragAndDropAvailable) { if (dragAndDropAvailable) {
return [ return [
<p>{getLangText('Drag %s here', fileClass)}</p>, <p>{getLangText('Drag %s here', fileClass)}</p>,
<p>{getLangText('or')}</p> <p>{getLangText('or')}</p>
@ -37,26 +35,31 @@ let FileDragAndDropDialog = React.createClass({
}, },
render() { render() {
const queryParams = this.props.location.query; const {
hasFiles,
multipleFiles,
enableLocalHashing,
uploadMethod,
fileClassToUpload,
onClick } = this.props;
if(this.props.hasFiles) { if (hasFiles) {
return null; return null;
} else { } else {
if(this.props.enableLocalHashing && !queryParams.method) { if (enableLocalHashing && !uploadMethod) {
const currentQueryParams = getCurrentQueryParams();
let queryParamsHash = Object.assign({}, queryParams); const queryParamsHash = Object.assign({}, currentQueryParams);
queryParamsHash.method = 'hash'; queryParamsHash.method = 'hash';
let queryParamsUpload = Object.assign({}, queryParams); const queryParamsUpload = Object.assign({}, currentQueryParams);
queryParamsUpload.method = 'upload'; queryParamsUpload.method = 'upload';
let { location } = this.props;
return ( return (
<div className="file-drag-and-drop-dialog present-options"> <div className="file-drag-and-drop-dialog present-options">
<p>{getLangText('Would you rather')}</p> <p>{getLangText('Would you rather')}</p>
<Link <Link
to={location.pathname} to={window.location.pathname}
query={queryParamsHash}> query={queryParamsHash}>
<span className="btn btn-default btn-sm"> <span className="btn btn-default btn-sm">
{getLangText('Hash your work')} {getLangText('Hash your work')}
@ -66,7 +69,7 @@ let FileDragAndDropDialog = React.createClass({
<span> or </span> <span> or </span>
<Link <Link
to={location.pathname} to={window.location.pathname}
query={queryParamsUpload}> query={queryParamsUpload}>
<span className="btn btn-default btn-sm"> <span className="btn btn-default btn-sm">
{getLangText('Upload and hash your work')} {getLangText('Upload and hash your work')}
@ -75,26 +78,27 @@ let FileDragAndDropDialog = React.createClass({
</div> </div>
); );
} else { } else {
if(this.props.multipleFiles) { if (multipleFiles) {
return ( return (
<span className="file-drag-and-drop-dialog"> <span className="file-drag-and-drop-dialog">
{this.getDragDialog(this.props.fileClassToUpload.plural)} {this.getDragDialog(fileClassToUpload.plural)}
<span <span
className="btn btn-default" className="btn btn-default"
onClick={this.props.onClick}> onClick={onClick}>
{getLangText('choose %s to upload', this.props.fileClassToUpload.plural)} {getLangText('choose %s to upload', fileClassToUpload.plural)}
</span> </span>
</span> </span>
); );
} else { } else {
let dialog = queryParams.method === 'hash' ? getLangText('choose a %s to hash', this.props.fileClassToUpload.singular) : getLangText('choose a %s to upload', this.props.fileClassToUpload.singular); const dialog = uploadMethod === 'hash' ? getLangText('choose a %s to hash', fileClassToUpload.singular)
: getLangText('choose a %s to upload', fileClassToUpload.singular);
return ( return (
<span className="file-drag-and-drop-dialog"> <span className="file-drag-and-drop-dialog">
{this.getDragDialog(this.props.fileClassToUpload.singular)} {this.getDragDialog(fileClassToUpload.singular)}
<span <span
className="btn btn-default" className="btn btn-default"
onClick={this.props.onClick}> onClick={onClick}>
{dialog} {dialog}
</span> </span>
</span> </span>

View File

@ -2,24 +2,30 @@
import React from 'react'; import React from 'react';
import Glyphicon from 'react-bootstrap/lib/Glyphicon';
import { displayValidProgressFilesFilter } from '../react_s3_fine_uploader_utils'; import { displayValidProgressFilesFilter } from '../react_s3_fine_uploader_utils';
import { getLangText } from '../../../utils/lang_utils'; import { getLangText } from '../../../utils/lang_utils';
import { truncateTextAtCharIndex } from '../../../utils/general_utils';
const { func, array, bool, shape, string } = React.PropTypes;
let UploadButton = React.createClass({ let UploadButton = React.createClass({
propTypes: { propTypes: {
onDrop: React.PropTypes.func.isRequired, onDrop: func.isRequired,
filesToUpload: React.PropTypes.array, filesToUpload: array,
multiple: React.PropTypes.bool, multiple: bool,
// For simplification purposes we're just going to use this prop as a // For simplification purposes we're just going to use this prop as a
// label for the upload button // label for the upload button
fileClassToUpload: React.PropTypes.shape({ fileClassToUpload: shape({
singular: React.PropTypes.string, singular: string,
plural: React.PropTypes.string plural: string
}), }),
allowedExtensions: React.PropTypes.string allowedExtensions: string,
handleCancelFile: func // provided by ReactS3FineUploader
}, },
handleDrop(event) { handleDrop(event) {
@ -37,11 +43,20 @@ let UploadButton = React.createClass({
return this.props.filesToUpload.filter((file) => file.status === 'uploading'); return this.props.filesToUpload.filter((file) => file.status === 'uploading');
}, },
handleOnClick() { getUploadedFile() {
let uploadingFiles = this.getUploadingFiles(); return this.props.filesToUpload.filter((file) => file.status === 'upload successful')[0];
},
// We only want the button to be clickable if there are no files currently uploading handleOnClick() {
const uploadingFiles = this.getUploadingFiles();
const uploadedFile = this.getUploadedFile();
if(uploadedFile) {
this.props.handleCancelFile(uploadedFile.id);
}
if(uploadingFiles.length === 0) { if(uploadingFiles.length === 0) {
// We only want the button to be clickable if there are no files currently uploading
// Firefox only recognizes the simulated mouse click if bubbles is set to true, // Firefox only recognizes the simulated mouse click if bubbles is set to true,
// but since Google Chrome propagates the event much further than needed, we // but since Google Chrome propagates the event much further than needed, we
// need to stop propagation as soon as the event is created // need to stop propagation as soon as the event is created
@ -62,24 +77,43 @@ let UploadButton = React.createClass({
// filter invalid files that might have been deleted or canceled... // filter invalid files that might have been deleted or canceled...
filesToUpload = filesToUpload.filter(displayValidProgressFilesFilter); filesToUpload = filesToUpload.filter(displayValidProgressFilesFilter);
// Depending on wether there is an upload going on or not we if(this.getUploadingFiles().length !== 0) {
// display the progress
if(filesToUpload.length > 0) {
return getLangText('Upload progress') + ': ' + Math.ceil(filesToUpload[0].progress) + '%'; return getLangText('Upload progress') + ': ' + Math.ceil(filesToUpload[0].progress) + '%';
} else { } else {
return fileClassToUpload.singular; return fileClassToUpload.singular;
} }
}, },
render() { getUploadedFileLabel() {
let { const uploadedFile = this.getUploadedFile();
multiple,
fileClassToUpload,
allowedExtensions
} = this.props;
if(uploadedFile) {
return ( return (
<button <span>
<Glyphicon glyph="ok" />
{' ' + truncateTextAtCharIndex(uploadedFile.name, 40)}
</span>
);
} else {
return (
<span>{getLangText('No file chosen')}</span>
);
}
},
render() {
let { multiple,
allowedExtensions } = this.props;
/*
* We do not want a button that submits here.
* As UploadButton could be used in forms that want to be submitted independent
* of clicking the selector.
* Therefore the wrapping component needs to be an `anchor` tag instead of a `button`
*/
return (
<div className="upload-button-wrapper">
<a
onClick={this.handleOnClick} onClick={this.handleOnClick}
className="btn btn-default btn-sm margin-left-2px" className="btn btn-default btn-sm margin-left-2px"
disabled={this.getUploadingFiles().length !== 0}> disabled={this.getUploadingFiles().length !== 0}>
@ -95,7 +129,9 @@ let UploadButton = React.createClass({
}} }}
onChange={this.handleDrop} onChange={this.handleDrop}
accept={allowedExtensions}/> accept={allowedExtensions}/>
</button> </a>
{this.getUploadedFileLabel()}
</div>
); );
} }
}); });

View File

@ -18,7 +18,6 @@ import { displayValidFilesFilter, transformAllowedExtensionsToInputAcceptProp }
import { getCookie } from '../../utils/fetch_api_utils'; import { getCookie } from '../../utils/fetch_api_utils';
import { getLangText } from '../../utils/lang_utils'; import { getLangText } from '../../utils/lang_utils';
let ReactS3FineUploader = React.createClass({ let ReactS3FineUploader = React.createClass({
propTypes: { propTypes: {
keyRoutine: React.PropTypes.shape({ keyRoutine: React.PropTypes.shape({
@ -107,11 +106,14 @@ let ReactS3FineUploader = React.createClass({
// One solution we found in the process of tackling this problem was to hash // One solution we found in the process of tackling this problem was to hash
// the file in the browser using md5 and then uploading the resulting text document instead // the file in the browser using md5 and then uploading the resulting text document instead
// of the actual file. // of the actual file.
// This boolean essentially enables that behavior //
// This boolean and string essentially enable that behavior.
// Right now, we determine which upload method to use by appending a query parameter,
// which should be passed into 'uploadMethod':
// 'hash': upload using the hash
// 'upload': upload full file (default if not specified)
enableLocalHashing: React.PropTypes.bool, enableLocalHashing: React.PropTypes.bool,
uploadMethod: React.PropTypes.oneOf(['hash', 'upload']),
// automatically injected by React-Router
query: React.PropTypes.object,
// A class of a file the user has to upload // A class of a file the user has to upload
// Needs to be defined both in singular as well as in plural // Needs to be defined both in singular as well as in plural
@ -126,9 +128,7 @@ let ReactS3FineUploader = React.createClass({
fileInputElement: React.PropTypes.oneOfType([ fileInputElement: React.PropTypes.oneOfType([
React.PropTypes.func, React.PropTypes.func,
React.PropTypes.element React.PropTypes.element
]), ])
location: React.PropTypes.object
}, },
getDefaultProps() { getDefaultProps() {
@ -314,18 +314,27 @@ let ReactS3FineUploader = React.createClass({
resolve(res.key); resolve(res.key);
}) })
.catch((err) => { .catch((err) => {
console.logGlobal(err, false, { this.onErrorPromiseProxy(err);
files: this.state.filesToUpload,
chunks: this.state.chunks
});
reject(err); reject(err);
}); });
}); });
}, },
createBlob(file) { createBlob(file) {
const { createBlobRoutine } = this.props;
return Q.Promise((resolve, reject) => { return Q.Promise((resolve, reject) => {
window.fetch(this.props.createBlobRoutine.url, {
// if createBlobRoutine is not defined,
// we're progressing right away without posting to S3
// so that this can be done manually by the form
if(!createBlobRoutine) {
// still we warn the user of this component
console.warn('createBlobRoutine was not defined for ReactS3FineUploader. Continuing without creating the blob on the server.');
resolve();
}
window.fetch(createBlobRoutine.url, {
method: 'post', method: 'post',
headers: { headers: {
'Accept': 'application/json', 'Accept': 'application/json',
@ -336,7 +345,7 @@ let ReactS3FineUploader = React.createClass({
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': createBlobRoutine.pieceId
}) })
}) })
.then((res) => { .then((res) => {
@ -352,16 +361,16 @@ let ReactS3FineUploader = React.createClass({
} else if(res.contractblob) { } else if(res.contractblob) {
file.s3Url = res.contractblob.url_safe; file.s3Url = res.contractblob.url_safe;
file.s3UrlSafe = res.contractblob.url_safe; file.s3UrlSafe = res.contractblob.url_safe;
} else if(res.thumbnail) {
file.s3Url = res.thumbnail.url_safe;
file.s3UrlSafe = res.thumbnail.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); resolve(res);
}) })
.catch((err) => { .catch((err) => {
console.logGlobal(err, false, { this.onErrorPromiseProxy(err);
files: this.state.filesToUpload,
chunks: this.state.chunks
});
reject(err); reject(err);
}); });
}); });
@ -370,7 +379,6 @@ let ReactS3FineUploader = React.createClass({
/* FineUploader specific callback function handlers */ /* FineUploader specific callback function handlers */
onUploadChunk(id, name, chunkData) { onUploadChunk(id, name, chunkData) {
let chunks = this.state.chunks; let chunks = this.state.chunks;
chunks[id + '-' + chunkData.startByte + '-' + chunkData.endByte] = { chunks[id + '-' + chunkData.startByte + '-' + chunkData.endByte] = {
@ -386,7 +394,6 @@ let ReactS3FineUploader = React.createClass({
}, },
onUploadChunkSuccess(id, chunkData, responseJson, xhr) { onUploadChunkSuccess(id, chunkData, responseJson, xhr) {
let chunks = this.state.chunks; let chunks = this.state.chunks;
let chunkKey = id + '-' + chunkData.startByte + '-' + chunkData.endByte; let chunkKey = id + '-' + chunkData.startByte + '-' + chunkData.endByte;
@ -430,7 +437,7 @@ let ReactS3FineUploader = React.createClass({
if(this.props.submitFile) { if(this.props.submitFile) {
this.props.submitFile(files[id]); this.props.submitFile(files[id]);
} else { } else {
console.warn('You didn\'t define submitFile in as a prop in react-s3-fine-uploader'); console.warn('You didn\'t define submitFile as a prop in react-s3-fine-uploader');
} }
// for explanation, check comment of if statement above // for explanation, check comment of if statement above
@ -447,17 +454,21 @@ let ReactS3FineUploader = React.createClass({
console.warn('You didn\'t define the functions isReadyForFormSubmission and/or setIsUploadReady in as a prop in react-s3-fine-uploader'); console.warn('You didn\'t define the functions isReadyForFormSubmission and/or setIsUploadReady in as a prop in react-s3-fine-uploader');
} }
}) })
.catch((err) => { .catch(this.onErrorPromiseProxy);
console.logGlobal(err, false, {
files: this.state.filesToUpload,
chunks: this.state.chunks
});
let notification = new GlobalNotificationModel(err.message, 'danger', 5000);
GlobalNotificationActions.appendGlobalNotification(notification);
});
} }
}, },
/**
* We want to channel all errors in this component through one single method.
* As fineuploader's `onError` method cannot handle the callback parameters of
* a promise we define this proxy method to crunch them into the correct form.
*
* @param {error} err a plain Javascript error
*/
onErrorPromiseProxy(err) {
this.onError(null, null, err.message);
},
onError(id, name, errorReason, xhr) { onError(id, name, errorReason, xhr) {
console.logGlobal(errorReason, false, { console.logGlobal(errorReason, false, {
files: this.state.filesToUpload, files: this.state.filesToUpload,
@ -465,6 +476,7 @@ let ReactS3FineUploader = React.createClass({
xhr: this.getXhrErrorComment(xhr) xhr: this.getXhrErrorComment(xhr)
}); });
this.props.setIsUploadReady(true);
this.cancelUploads(); this.cancelUploads();
let notification = new GlobalNotificationModel(errorReason || this.props.defaultErrorMessage, 'danger', 5000); let notification = new GlobalNotificationModel(errorReason || this.props.defaultErrorMessage, 'danger', 5000);
@ -628,7 +640,6 @@ let ReactS3FineUploader = React.createClass({
} else { } else {
throw new Error(getLangText('File upload could not be paused.')); throw new Error(getLangText('File upload could not be paused.'));
} }
}, },
handleResumeFile(fileId) { handleResumeFile(fileId) {
@ -640,6 +651,10 @@ let ReactS3FineUploader = React.createClass({
}, },
handleUploadFile(files) { handleUploadFile(files) {
// While files are being uploaded, the form cannot be ready
// for submission
this.props.setIsUploadReady(false);
// If multiple set and user already uploaded its work, // If multiple set and user already uploaded its work,
// cancel upload // cancel upload
if(!this.props.multiple && this.state.filesToUpload.filter(displayValidFilesFilter).length > 0) { if(!this.props.multiple && this.state.filesToUpload.filter(displayValidFilesFilter).length > 0) {
@ -679,16 +694,14 @@ let ReactS3FineUploader = React.createClass({
// md5 hash of a file locally and just upload a txt file containing that hash. // md5 hash of a file locally and just upload a txt file containing that hash.
// //
// In the view this only happens when the user is allowed to do local hashing as well // In the view this only happens when the user is allowed to do local hashing as well
// as when the correct query parameter is present in the url ('hash' and not 'upload') // as when the correct method prop is present ('hash' and not 'upload')
let queryParams = this.props.location.query; if (this.props.enableLocalHashing && this.props.uploadMethod === 'hash') {
if(this.props.enableLocalHashing && queryParams && queryParams.method === 'hash') { const convertedFilePromises = [];
let convertedFilePromises = [];
let overallFileSize = 0; let overallFileSize = 0;
// "files" is not a classical Javascript array but a Javascript FileList, therefore // "files" is not a classical Javascript array but a Javascript FileList, therefore
// we can not use map to convert values // we can not use map to convert values
for(let i = 0; i < files.length; i++) { for(let i = 0; i < files.length; i++) {
// for calculating the overall progress of all submitted files // for calculating the overall progress of all submitted files
// we'll need to calculate the overall sum of all files' sizes // we'll need to calculate the overall sum of all files' sizes
overallFileSize += files[i].size; overallFileSize += files[i].size;
@ -700,7 +713,6 @@ let ReactS3FineUploader = React.createClass({
// we're using promises to handle that // we're using promises to handle that
let hashedFilePromise = computeHashOfFile(files[i]); let hashedFilePromise = computeHashOfFile(files[i]);
convertedFilePromises.push(hashedFilePromise); convertedFilePromises.push(hashedFilePromise);
} }
// To react after the computation of all files, we define the resolvement // To react after the computation of all files, we define the resolvement
@ -708,7 +720,6 @@ let ReactS3FineUploader = React.createClass({
// with their txt representative // with their txt representative
Q.all(convertedFilePromises) Q.all(convertedFilePromises)
.progress(({index, value: {progress, reject}}) => { .progress(({index, value: {progress, reject}}) => {
// hashing progress has been aborted from outside // hashing progress has been aborted from outside
// To get out of the executing, we need to call reject from the // To get out of the executing, we need to call reject from the
// inside of the promise's execution. // inside of the promise's execution.
@ -728,18 +739,14 @@ let ReactS3FineUploader = React.createClass({
// currently hashing files // currently hashing files
let overallHashingProgress = 0; let overallHashingProgress = 0;
for(let i = 0; i < files.length; i++) { for(let i = 0; i < files.length; i++) {
let filesSliceOfOverall = files[i].size / overallFileSize; let filesSliceOfOverall = files[i].size / overallFileSize;
overallHashingProgress += filesSliceOfOverall * files[i].progress; overallHashingProgress += filesSliceOfOverall * files[i].progress;
} }
// Multiply by 100, since react-progressbar expects decimal numbers // Multiply by 100, since react-progressbar expects decimal numbers
this.setState({ hashingProgress: overallHashingProgress * 100}); this.setState({ hashingProgress: overallHashingProgress * 100});
}) })
.then((convertedFiles) => { .then((convertedFiles) => {
// clear hashing progress, since its done // clear hashing progress, since its done
this.setState({ hashingProgress: -2}); this.setState({ hashingProgress: -2});
@ -860,15 +867,13 @@ let ReactS3FineUploader = React.createClass({
}, },
isDropzoneInactive() { isDropzoneInactive() {
let filesToDisplay = this.state.filesToUpload.filter((file) => file.status !== 'deleted' && file.status !== 'canceled' && file.size !== -1); const filesToDisplay = this.state.filesToUpload.filter((file) => file.status !== 'deleted' && file.status !== 'canceled' && file.size !== -1);
let queryParams = this.props.location.query;
if((this.props.enableLocalHashing && !queryParams.method) || !this.props.areAssetsEditable || !this.props.multiple && filesToDisplay.length > 0) { if ((this.props.enableLocalHashing && !this.props.uploadMethod) || !this.props.areAssetsEditable || !this.props.multiple && filesToDisplay.length > 0) {
return true; return true;
} else { } else {
return false; return false;
} }
}, },
getAllowedExtensions() { getAllowedExtensions() {
@ -889,9 +894,8 @@ let ReactS3FineUploader = React.createClass({
onInactive, onInactive,
enableLocalHashing, enableLocalHashing,
fileClassToUpload, fileClassToUpload,
validation,
fileInputElement: FileInputElement, fileInputElement: FileInputElement,
location } = this.props; uploadMethod } = this.props;
const props = { const props = {
multiple, multiple,
@ -899,8 +903,8 @@ let ReactS3FineUploader = React.createClass({
areAssetsEditable, areAssetsEditable,
onInactive, onInactive,
enableLocalHashing, enableLocalHashing,
uploadMethod,
fileClassToUpload, fileClassToUpload,
location,
onDrop: this.handleUploadFile, onDrop: this.handleUploadFile,
filesToUpload: this.state.filesToUpload, filesToUpload: this.state.filesToUpload,
handleDeleteFile: this.handleDeleteFile, handleDeleteFile: this.handleDeleteFile,

View File

@ -40,12 +40,6 @@ let RegisterPiece = React.createClass( {
mixins: [History], mixins: [History],
getDefaultProps() {
return {
canSpecifyEditions: true
};
},
getInitialState(){ getInitialState(){
return mergeOptions( return mergeOptions(
UserStore.getState(), UserStore.getState(),

View File

@ -1,33 +0,0 @@
'use strict';
import { alt } from '../../../../alt';
import Q from 'q';
import PrizeFetcher from '../fetchers/prize_fetcher';
class PrizeActions {
constructor() {
this.generateActions(
'updatePrize'
);
}
fetchPrize() {
return Q.Promise((resolve, reject) => {
PrizeFetcher
.fetch()
.then((res) => {
this.actions.updatePrize({
prize: res.prize
});
resolve(res);
})
.catch((err) => {
console.logGlobal(err);
reject(err);
});
});
}
}
export default alt.createActions(PrizeActions);

View File

@ -1,91 +0,0 @@
'use strict';
import React from 'react';
import PrizeActions from '../actions/prize_actions';
import PrizeStore from '../stores/prize_store';
import RegisterPiece from '../../../register_piece';
import Property from '../../../ascribe_forms/property';
import InputTextAreaToggable from '../../../ascribe_forms/input_textarea_toggable';
import InputCheckbox from '../../../ascribe_forms/input_checkbox';
import { getLangText } from '../../../../utils/lang_utils';
import { setDocumentTitle } from '../../../../utils/dom_utils';
let PrizeRegisterPiece = React.createClass({
getInitialState() {
return PrizeStore.getState();
},
componentDidMount() {
PrizeStore.listen(this.onChange);
PrizeActions.fetchPrize();
},
componentWillUnmount() {
PrizeStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
},
render() {
setDocumentTitle(getLangText('Submit to the prize'));
if(this.state.prize && this.state.prize.active){
return (
<RegisterPiece
enableLocalHashing={false}
headerMessage={getLangText('Submit to the prize')}
submitMessage={getLangText('Submit')}>
<Property
name='artist_statement'
label={getLangText('Artist statement')}
editable={true}
overrideForm={true}>
<InputTextAreaToggable
rows={1}
placeholder={getLangText('Enter your statement')}
required />
</Property>
<Property
name='work_description'
label={getLangText('Work description')}
editable={true}
overrideForm={true}>
<InputTextAreaToggable
rows={1}
placeholder={getLangText('Enter the description for your work')}
required />
</Property>
<Property
name="terms"
className="ascribe-property-collapsible-toggle"
style={{paddingBottom: 0}}>
<InputCheckbox>
<span>
{' ' + getLangText('I agree to the Terms of Service the art price') + ' '}
(<a href="https://s3-us-west-2.amazonaws.com/ascribe0/whitelabel/sluice/terms.pdf" target="_blank" style={{fontSize: '0.9em', color: 'rgba(0,0,0,0.7)'}}>
{getLangText('read')}
</a>)
</span>
</InputCheckbox>
</Property>
</RegisterPiece>);
}
else {
return (
<div className='row'>
<div style={{textAlign: 'center'}}>
{getLangText('The prize is no longer active')}
</div>
</div>
);
}
}
});
export default PrizeRegisterPiece;

View File

@ -2,6 +2,7 @@
import AppPrizeConstants from './prize_application_constants'; import AppPrizeConstants from './prize_application_constants';
function getPrizeApiUrls(subdomain) { function getPrizeApiUrls(subdomain) {
return { return {
'users_login': AppPrizeConstants.prizeApiEndpoint + subdomain + '/users/login/', 'users_login': AppPrizeConstants.prizeApiEndpoint + subdomain + '/users/login/',
@ -18,10 +19,9 @@ function getPrizeApiUrls(subdomain) {
'ratings': AppPrizeConstants.prizeApiEndpoint + subdomain + '/ratings/', 'ratings': AppPrizeConstants.prizeApiEndpoint + subdomain + '/ratings/',
'rating': AppPrizeConstants.prizeApiEndpoint + subdomain + '/ratings/${piece_id}/', 'rating': AppPrizeConstants.prizeApiEndpoint + subdomain + '/ratings/${piece_id}/',
'rating_average': AppPrizeConstants.prizeApiEndpoint + subdomain + '/ratings/${piece_id}/average/', 'rating_average': AppPrizeConstants.prizeApiEndpoint + subdomain + '/ratings/${piece_id}/average/',
'select_piece' : AppPrizeConstants.prizeApiEndpoint + subdomain + '/ratings/${piece_id}/select/', 'select_piece': AppPrizeConstants.prizeApiEndpoint + subdomain + '/ratings/${piece_id}/select/',
'notes': AppPrizeConstants.prizeApiEndpoint + subdomain + '/notes/', 'notes': AppPrizeConstants.prizeApiEndpoint + subdomain + '/notes/',
'note': AppPrizeConstants.prizeApiEndpoint + subdomain + '/notes/${piece_id}/' 'note': AppPrizeConstants.prizeApiEndpoint + subdomain + '/notes/${piece_id}/'
}; };
} }

View File

@ -0,0 +1,375 @@
'use strict';
import React from 'react';
import { History } from 'react-router';
import Form from '../../../../../ascribe_forms/form';
import Property from '../../../../../ascribe_forms/property';
import InputTextAreaToggable from '../../../../../ascribe_forms/input_textarea_toggable';
import UploadButton from '../../../../../ascribe_uploader/ascribe_upload_button/upload_button';
import InputFineuploader from '../../../../../ascribe_forms/input_fineuploader';
import AscribeSpinner from '../../../../../ascribe_spinner';
import GlobalNotificationModel from '../../../../../../models/global_notification_model';
import GlobalNotificationActions from '../../../../../../actions/global_notification_actions';
import AppConstants from '../../../../../../constants/application_constants';
import ApiUrls from '../../../../../../constants/api_urls';
import requests from '../../../../../../utils/requests';
import { getLangText } from '../../../../../../utils/lang_utils';
import { setCookie } from '../../../../../../utils/fetch_api_utils';
import { formSubmissionValidation } from '../../../../../ascribe_uploader/react_s3_fine_uploader_utils';
const { object } = React.PropTypes;
const PRRegisterPieceForm = React.createClass({
propTypes: {
location: object,
history: object,
currentUser: object
},
mixins: [History],
getInitialState(){
return {
digitalWorkKeyReady: true,
thumbnailKeyReady: true,
// we set this to true, as it is not required
supportingMaterialsReady: true,
proofOfPaymentReady: true,
piece: null,
submitted: false
};
},
/**
* In this method, we're composing all fields on the page
* in two steps, first submitting the registration of the piece and
* second adding all the additional details
*/
submit() {
if(!this.validateForms()) {
return;
} else {
// disable the submission button right after the user
// clicks on it to avoid double submission
this.setState({
submitted: true
});
}
const { currentUser } = this.props;
const { registerPieceForm,
additionalDataForm,
uploadersForm } = this.refs;
const { digitalWorkKey,
thumbnailKey,
supportingMaterials,
proofOfPayment } = uploadersForm.refs;
const additionalDataFormData = additionalDataForm.getFormData();
// composing data for piece registration
let registerPieceFormData = registerPieceForm.getFormData();
registerPieceFormData.digital_work_key = digitalWorkKey.state.value;
registerPieceFormData.thumbnail_file = thumbnailKey.state.value;
registerPieceFormData.terms = true;
// submitting the piece
requests
.post(ApiUrls.pieces_list, { body: registerPieceFormData })
.then(({ success, piece, notification }) => {
if(success) {
this.setState({
piece
}, () => {
supportingMaterials.refs.input.createBlobRoutine();
proofOfPayment.refs.input.createBlobRoutine();
});
setCookie(currentUser.email, piece.id);
return requests.post(ApiUrls.piece_extradata, {
body: {
extradata: additionalDataFormData,
piece_id: piece.id
},
piece_id: piece.id
});
} else {
const notificationMessage = new GlobalNotificationModel(notification, 'danger', 5000);
GlobalNotificationActions.appendGlobalNotification(notificationMessage);
}
})
.then(() => this.history.pushState(null, `/pieces/${this.state.piece.id}`))
.catch(() => {
const notificationMessage = new GlobalNotificationModel(getLangText("Ups! We weren't able to send your submission. Contact: support@ascribe.io"), 'danger', 5000);
GlobalNotificationActions.appendGlobalNotification(notificationMessage);
});
},
validateForms() {
const { registerPieceForm,
additionalDataForm,
uploadersForm } = this.refs;
const registerPieceFormValidation = registerPieceForm.validate();
const additionalDataFormValidation = additionalDataForm.validate();
const uploaderFormValidation = uploadersForm.validate();
return registerPieceFormValidation && additionalDataFormValidation && uploaderFormValidation;
},
getCreateBlobRoutine() {
const { piece } = this.state;
if(piece && piece.id) {
return {
url: ApiUrls.blob_otherdatas,
pieceId: piece.id
};
} else {
return null;
}
},
/**
* This method is overloaded so that we can track the ready-state
* of each uploader in the component
* @param {string} uploaderKey Name of the uploader's key to track
*/
setIsUploadReady(uploaderKey) {
return (isUploadReady) => {
this.setState({
[uploaderKey]: isUploadReady
});
};
},
getSubmitButton() {
const { digitalWorkKeyReady,
thumbnailKeyReady,
supportingMaterialsReady,
proofOfPaymentReady,
submitted } = this.state;
if(submitted) {
return (
<span disabled className="btn btn-default btn-wide btn-spinner">
<AscribeSpinner color="dark-blue" size="md" />
</span>
);
} else {
return (
<button
type="submit"
className="btn btn-default btn-wide"
disabled={!(digitalWorkKeyReady && thumbnailKeyReady && proofOfPaymentReady && supportingMaterialsReady)}
onClick={this.submit}>
{getLangText('Submit to Portfolio Review')}
</button>
);
}
},
render() {
const { location } = this.props;
return (
<div className="register-piece--form">
<Form
buttons={{}}
className="ascribe-form-bordered"
ref="registerPieceForm">
<Property
name='artist_name'
label={getLangText('Full name')}>
<input
type="text"
placeholder="(e.g. Andy Warhol)"
required/>
</Property>
<Property
name='title'
label={getLangText('Title of the Work')}>
<input
type="text"
placeholder="(e.g. 32 Campbell's Soup Cans)"
required/>
</Property>
<Property
name='date_created'
label={getLangText('Year of creation')}>
<input
type="number"
placeholder="(e.g. 1962)"
min={1}
required/>
</Property>
<Property
name='artist_statement'
label={getLangText("Artist's statement")}>
<InputTextAreaToggable
rows={1}
placeholder={getLangText('Enter your statement')}/>
</Property>
</Form>
<Form
buttons={{}}
className="ascribe-form-bordered"
ref="additionalDataForm">
<Property
name='artist_bio'
label={getLangText('Biography')}>
<InputTextAreaToggable
rows={1}
placeholder={getLangText('Enter your biography')}/>
</Property>
<Property
name='exhibition'
label={getLangText('Exhibition / Publication history (optional)')}>
<InputTextAreaToggable
rows={1}
placeholder={getLangText('Enter exhibitions and publication history')}/>
</Property>
<Property
name='contact_information'
label={getLangText('Contact information')}>
<InputTextAreaToggable
rows={1}
placeholder={getLangText('Enter your contact information (phone/website)')}/>
</Property>
</Form>
<Form
buttons={{}}
className="ascribe-form-bordered"
ref="uploadersForm">
<Property
name="digitalWorkKey"
label={getLangText('Select the PDF with your work')}>
<InputFineuploader
fileInputElement={UploadButton}
isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile}
setIsUploadReady={this.setIsUploadReady('digitalWorkKeyReady')}
createBlobRoutine={{
url: ApiUrls.blob_digitalworks
}}
keyRoutine={{
url: AppConstants.serverUrl + 's3/key/',
fileClass: 'digitalwork'
}}
validation={{
itemLimit: AppConstants.fineUploader.validation.registerWork.itemLimit,
sizeLimit: AppConstants.fineUploader.validation.additionalData.sizeLimit,
allowedExtensions: ['pdf']
}}
location={location}
fileClassToUpload={{
singular: getLangText('Select the Portfolio'),
plural: getLangText('Select the Portfolios')
}}
required/>
</Property>
<Property
name="thumbnailKey"
label={getLangText('Featured Cover photo')}>
<InputFineuploader
fileInputElement={UploadButton}
createBlobRoutine={{
url: ApiUrls.blob_thumbnails
}}
isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile}
setIsUploadReady={this.setIsUploadReady('thumbnailKeyReady')}
keyRoutine={{
url: AppConstants.serverUrl + 's3/key/',
fileClass: 'thumbnail'
}}
validation={{
itemLimit: AppConstants.fineUploader.validation.registerWork.itemLimit,
sizeLimit: AppConstants.fineUploader.validation.additionalData.sizeLimit,
allowedExtensions: ['png', 'jpg', 'jpeg', 'gif']
}}
location={location}
fileClassToUpload={{
singular: getLangText('Select cover photo'),
plural: getLangText('Select cover photos')
}}
required/>
</Property>
<Property
name="supportingMaterials"
label={getLangText('Supporting Materials (Optional)')}>
<InputFineuploader
fileInputElement={UploadButton}
isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile}
setIsUploadReady={this.setIsUploadReady('supportingMaterialsReady')}
createBlobRoutine={this.getCreateBlobRoutine()}
keyRoutine={{
url: AppConstants.serverUrl + 's3/key/',
fileClass: 'other_data'
}}
validation={{
itemLimit: AppConstants.fineUploader.validation.registerWork.itemLimit,
sizeLimit: AppConstants.fineUploader.validation.additionalData.sizeLimit
}}
location={location}
fileClassToUpload={{
singular: getLangText('Select supporting material'),
plural: getLangText('Select supporting materials')
}}/>
</Property>
<Property
name="proofOfPayment"
label={getLangText('Proof of payment')}>
<InputFineuploader
fileInputElement={UploadButton}
isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile}
setIsUploadReady={this.setIsUploadReady('proofOfPaymentReady')}
createBlobRoutine={this.getCreateBlobRoutine()}
keyRoutine={{
url: AppConstants.serverUrl + 's3/key/',
fileClass: 'other_data'
}}
validation={{
itemLimit: AppConstants.fineUploader.validation.registerWork.itemLimit,
sizeLimit: AppConstants.fineUploader.validation.additionalData.sizeLimit,
allowedExtensions: ['png', 'jpg', 'jpeg', 'gif']
}}
location={location}
fileClassToUpload={{
singular: getLangText('Select Screenshot'),
plural: getLangText('Select Screenshots')
}}
required/>
</Property>
</Form>
<Form
buttons={{}}
className="ascribe-form-bordered">
<Property
name="terms"
className="ascribe-property-collapsible-toggle"
style={{paddingBottom: 0}}>
<span>
{getLangText('By submitting this form, you agree to the') + ' '}
<a
href="https://s3-us-west-2.amazonaws.com/ascribe0/whitelabel/portfolioreview/tos-portfolioreview.pdf"
target="_blank">
{getLangText('Terms of Service')}
</a>
{' of Portfolio Review.'}
</span>
</Property>
</Form>
{this.getSubmitButton()}
</div>
);
}
});
export default PRRegisterPieceForm;

View File

@ -0,0 +1,42 @@
'use strict';
import React from 'react';
import UserStore from '../../../../../stores/user_store';
import UserActions from '../../../../../actions/user_actions';
import Glyphicon from 'react-bootstrap/lib/Glyphicon';
const PRHero = React.createClass({
getInitialState() {
return UserStore.getState();
},
componentDidMount() {
UserStore.listen(this.onChange);
UserActions.fetchCurrentUser();
},
componentWillUnmount() {
UserStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
},
render() {
const { currentUser } = this.state;
return (
<div className="piece--hero">
<h2><Glyphicon glyph="ok" /> Congratulations {currentUser.email}!</h2>
<h1>You have successfully submitted to Portfolio Review 2016</h1>
<p>See below, your uploaded portfolio:</p>
</div>
);
}
});
export default PRHero;

View File

@ -0,0 +1,128 @@
'use strict';
import React from 'react';
import { History } from 'react-router';
import PrizeActions from '../../simple_prize/actions/prize_actions';
import PrizeStore from '../../simple_prize/stores/prize_store';
import Button from 'react-bootstrap/lib/Button';
import ButtonGroup from 'react-bootstrap/lib/ButtonGroup';
import LinkContainer from 'react-router-bootstrap/lib/LinkContainer';
import UserStore from '../../../../../stores/user_store';
import UserActions from '../../../../../actions/user_actions';
import { mergeOptions } from '../../../../../utils/general_utils';
import { getLangText } from '../../../../../utils/lang_utils';
const PRLanding = React.createClass({
propTypes: {
location: React.PropTypes.object
},
mixins: [History],
getInitialState() {
return mergeOptions(
PrizeStore.getState(),
UserStore.getState()
);
},
componentDidMount() {
const { location } = this.props;
UserStore.listen(this.onChange);
UserActions.fetchCurrentUser();
PrizeStore.listen(this.onChange);
PrizeActions.fetchPrize();
if(location && location.query && location.query.redirect) {
let queryCopy = JSON.parse(JSON.stringify(location.query));
delete queryCopy.redirect;
window.setTimeout(() => this.history.replaceState(null, `/${location.query.redirect}`, queryCopy));
}
},
componentWillUnmount() {
UserStore.unlisten(this.onChange);
PrizeStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
},
getButtons() {
if (this.state.prize && this.state.prize.active){
return (
<ButtonGroup className="enter" bsSize="large" vertical>
<LinkContainer to="/signup">
<Button>
{getLangText('Sign up to submit')}
</Button>
</LinkContainer>
<p>
{getLangText('or, already an ascribe user?')}
</p>
<LinkContainer to="/login">
<Button>
{getLangText('Log in to submit')}
</Button>
</LinkContainer>
</ButtonGroup>
);
}
return (
<ButtonGroup className="enter" bsSize="large" vertical>
<a className="btn btn-default" href="https://www.ascribe.io/app/signup">
{getLangText('Sign up to ascribe')}
</a>
<p>
{getLangText('or, already an ascribe user?')}
</p>
<LinkContainer to="/login">
<Button>
{getLangText('Log in')}
</Button>
</LinkContainer>
</ButtonGroup>
);
},
getTitle() {
if (this.state.prize && this.state.prize.active){
return (
<p>
{getLangText('This is the submission page for Portfolio Review 2016.')}
</p>
);
}
return (
<p>
{getLangText('Submissions for Portfolio Review 2016 are now closed.')}
</p>
);
},
render() {
return (
<div className="container">
<div className="row">
<div className="col-xs-12 wp-landing-wrapper">
<h1>
{getLangText('Welcome to Portfolio Review 2016')}
</h1>
{this.getTitle()}
{this.getButtons()}
</div>
</div>
</div>
);
}
});
export default PRLanding;

View File

@ -0,0 +1,82 @@
'use strict';
import React from 'react';
import { Link, History } from 'react-router';
import Col from 'react-bootstrap/lib/Col';
import Row from 'react-bootstrap/lib/Row';
import UserStore from '../../../../../stores/user_store';
import UserActions from '../../../../../actions/user_actions';
import PRRegisterPieceForm from './pr_forms/pr_register_piece_form';
import { getLangText } from '../../../../../utils/lang_utils';
import { setDocumentTitle } from '../../../../../utils/dom_utils';
import { getCookie } from '../../../../../utils/fetch_api_utils';
const { object } = React.PropTypes;
const PRRegisterPiece = React.createClass({
propTypes: {
location: object
},
mixins: [History],
getInitialState() {
return UserStore.getState();
},
componentDidMount() {
UserStore.listen(this.onChange);
UserActions.fetchCurrentUser();
},
componentDidUpdate() {
const { currentUser } = this.state;
if(currentUser && currentUser.email) {
const submittedPieceId = getCookie(currentUser.email);
if(submittedPieceId) {
this.history.pushState(null, `/pieces/${submittedPieceId}`);
}
}
},
componentWillUnmount() {
UserStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
},
render() {
const { currentUser } = this.state;
const { location } = this.props;
setDocumentTitle(getLangText('Submit to Portfolio Review'));
return (
<Row>
<Col xs={6}>
<div className="register-piece--info">
<h1>Portfolio Review</h1>
<h2>{getLangText('Submission closing on %s', ' 22 Dec 2015')}</h2>
<p style={{marginTop: '1em'}}>
{getLangText("You're submitting as %s. ", currentUser.email)}
<Link to="/logout">{getLangText('Change account?')}</Link>
</p>
</div>
</Col>
<Col xs={6}>
<PRRegisterPieceForm
location={location}
currentUser={currentUser}/>
</Col>
</Row>
);
}
});
export default PRRegisterPiece;

View File

@ -0,0 +1,65 @@
'use strict';
import React from 'react';
import GlobalNotification from '../../../global_notification';
import Hero from './components/pr_hero';
import UserStore from '../../../../stores/user_store';
import UserActions from '../../../../actions/user_actions';
import { getSubdomain } from '../../../../utils/general_utils';
import { getCookie } from '../../../../utils/fetch_api_utils';
let PRApp = React.createClass({
propTypes: {
children: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.element),
React.PropTypes.element
]),
history: React.PropTypes.object,
routes: React.PropTypes.arrayOf(React.PropTypes.object)
},
getInitialState() {
return UserStore.getState();
},
componentDidMount() {
UserStore.listen(this.onChange);
UserActions.fetchCurrentUser();
},
componentWillUnmount() {
UserStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
},
render() {
const { history, children } = this.props;
const { currentUser } = this.state;
let subdomain = getSubdomain();
let header;
if (currentUser && currentUser.email && history.isActive(`/pieces/${getCookie(currentUser.email)}`)) {
header = <Hero />;
}
return (
<div>
{header}
<div className={'container ascribe-prize-app client--' + subdomain}>
{children}
<GlobalNotification />
<div id="modal" className="container"></div>
</div>
</div>
);
}
});
export default PRApp;

View File

@ -3,58 +3,94 @@
import React from 'react'; import React from 'react';
import { Route, IndexRoute } from 'react-router'; import { Route, IndexRoute } from 'react-router';
import Landing from './components/prize_landing'; import SPLanding from './simple_prize/components/prize_landing';
import LoginContainer from './components/prize_login_container'; import SPLoginContainer from './simple_prize/components/prize_login_container';
import LogoutContainer from '../../../components/logout_container'; import SPSignupContainer from './simple_prize/components/prize_signup_container';
import SignupContainer from './components/prize_signup_container'; import SPRegisterPiece from './simple_prize/components/prize_register_piece';
import PasswordResetContainer from '../../../components/password_reset_container'; import SPPieceList from './simple_prize/components/prize_piece_list';
import PrizeRegisterPiece from './components/prize_register_piece'; import SPPieceContainer from './simple_prize/components/ascribe_detail/prize_piece_container';
import PrizePieceList from './components/prize_piece_list'; import SPSettingsContainer from './simple_prize/components/prize_settings_container';
import PrizePieceContainer from './components/ascribe_detail/prize_piece_container'; import SPApp from './simple_prize/prize_app';
import EditionContainer from '../../ascribe_detail/edition_container';
import SettingsContainer from './components/prize_settings_container';
import CoaVerifyContainer from '../../../components/coa_verify_container';
import ErrorNotFoundPage from '../../../components/error_not_found_page';
import App from './prize_app'; import PRApp from './portfolioreview/pr_app';
import PRLanding from './portfolioreview/components/pr_landing';
import PRRegisterPiece from './portfolioreview/components/pr_register_piece';
import EditionContainer from '../../ascribe_detail/edition_container';
import LogoutContainer from '../../logout_container';
import PasswordResetContainer from '../../password_reset_container';
import CoaVerifyContainer from '../../coa_verify_container';
import ErrorNotFoundPage from '../../error_not_found_page';
import AuthProxyHandler from '../../../components/ascribe_routes/proxy_routes/auth_proxy_handler'; import AuthProxyHandler from '../../../components/ascribe_routes/proxy_routes/auth_proxy_handler';
function getRoutes() { const ROUTES = {
return ( sluice: (
<Route path='/' component={App}> <Route path='/' component={SPApp}>
<IndexRoute component={Landing} /> <IndexRoute component={SPLanding} />
<Route <Route
path='login' path='login'
component={AuthProxyHandler({to: '/collection', when: 'loggedIn'})(LoginContainer)} /> component={AuthProxyHandler({to: '/collection', when: 'loggedIn'})(SPLoginContainer)} />
<Route <Route
path='logout' path='logout'
component={AuthProxyHandler({to: '/', when: 'loggedOut'})(LogoutContainer)}/> component={AuthProxyHandler({to: '/', when: 'loggedOut'})(LogoutContainer)}/>
<Route <Route
path='signup' path='signup'
component={AuthProxyHandler({to: '/collection', when: 'loggedIn'})(SignupContainer)} /> component={AuthProxyHandler({to: '/collection', when: 'loggedIn'})(SPSignupContainer)} />
<Route <Route
path='password_reset' path='password_reset'
component={AuthProxyHandler({to: '/collection', when: 'loggedIn'})(PasswordResetContainer)} /> component={AuthProxyHandler({to: '/collection', when: 'loggedIn'})(PasswordResetContainer)} />
<Route <Route
path='settings' path='settings'
component={AuthProxyHandler({to: '/login', when: 'loggedOut'})(SettingsContainer)}/> component={AuthProxyHandler({to: '/login', when: 'loggedOut'})(SPSettingsContainer)}/>
<Route <Route
path='register_piece' path='register_piece'
component={AuthProxyHandler({to: '/login', when: 'loggedOut'})(PrizeRegisterPiece)} component={AuthProxyHandler({to: '/login', when: 'loggedOut'})(SPRegisterPiece)}
headerTitle='+ NEW WORK'/> headerTitle='+ NEW WORK'/>
<Route <Route
path='collection' path='collection'
component={AuthProxyHandler({to: '/login', when: 'loggedOut'})(PrizePieceList)} component={AuthProxyHandler({to: '/login', when: 'loggedOut'})(SPPieceList)}
headerTitle='COLLECTION'/> headerTitle='COLLECTION'/>
<Route path='pieces/:pieceId' component={PrizePieceContainer} /> <Route path='pieces/:pieceId' component={SPPieceContainer} />
<Route path='editions/:editionId' component={EditionContainer} /> <Route path='editions/:editionId' component={EditionContainer} />
<Route path='verify' component={CoaVerifyContainer} /> <Route path='verify' component={CoaVerifyContainer} />
<Route path='*' component={ErrorNotFoundPage} /> <Route path='*' component={ErrorNotFoundPage} />
</Route> </Route>
); ),
portfolioreview: (
<Route path='/' component={PRApp}>
<IndexRoute component={PRLanding} />
<Route
path='register_piece'
component={AuthProxyHandler({to: '/login', when: 'loggedOut'})(PRRegisterPiece)}
headerTitle='+ NEW WORK'/>
<Route
path='login'
component={AuthProxyHandler({to: '/register_piece', when: 'loggedIn'})(SPLoginContainer)} />
<Route
path='logout'
component={AuthProxyHandler({to: '/', when: 'loggedOut'})(LogoutContainer)}/>
<Route
path='signup'
component={AuthProxyHandler({to: '/register_piece', when: 'loggedIn'})(SPSignupContainer)} />
<Route
path='password_reset'
component={AuthProxyHandler({to: '/register_piece', when: 'loggedIn'})(PasswordResetContainer)} />
<Route path='pieces/:pieceId' component={SPPieceContainer} />
<Route path='*' component={ErrorNotFoundPage} />
</Route>
)
};
function getRoutes(commonRoutes, subdomain) {
if(subdomain in ROUTES) {
return ROUTES[subdomain];
} else {
throw new Error('Subdomain wasn\'t specified in the wallet app.');
}
} }

View File

@ -0,0 +1,28 @@
'use strict';
import { alt } from '../../../../../alt';
import PrizeFetcher from '../fetchers/prize_fetcher';
class PrizeActions {
constructor() {
this.generateActions(
'updatePrize'
);
}
fetchPrize() {
PrizeFetcher
.fetch()
.then((res) => {
this.actions.updatePrize({
prize: res.prize
});
})
.catch((err) => {
console.logGlobal(err);
});
}
}
export default alt.createActions(PrizeActions);

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
import { alt } from '../../../../alt'; import { alt } from '../../../../../alt';
import Q from 'q'; import Q from 'q';
import PrizeJuryFetcher from '../fetchers/prize_jury_fetcher'; import PrizeJuryFetcher from '../fetchers/prize_jury_fetcher';

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
import { alt } from '../../../../alt'; import { alt } from '../../../../../alt';
import Q from 'q'; import Q from 'q';
import PrizeRatingFetcher from '../fetchers/prize_rating_fetcher'; import PrizeRatingFetcher from '../fetchers/prize_rating_fetcher';

View File

@ -3,26 +3,27 @@
import React from 'react'; import React from 'react';
import { Link } from 'react-router'; import { Link } from 'react-router';
import StarRating from 'react-star-rating'; import StarRating from 'react-star-rating';
import Moment from 'moment';
import PieceListActions from '../../../../../actions/piece_list_actions'; import PieceListActions from '../../../../../../actions/piece_list_actions';
import PieceListStore from '../../../../../stores/piece_list_store'; import PieceListStore from '../../../../../../stores/piece_list_store';
import PrizeRatingActions from '../../actions/prize_rating_actions'; import PrizeRatingActions from '../../actions/prize_rating_actions';
import UserStore from '../../../../../stores/user_store'; import UserStore from '../../../../../../stores/user_store';
import InputCheckbox from '../../../../ascribe_forms/input_checkbox'; import InputCheckbox from '../../../../../ascribe_forms/input_checkbox';
import AccordionListItemPiece from '../../../../ascribe_accordion_list/accordion_list_item_piece'; import AccordionListItemPiece from '../../../../../ascribe_accordion_list/accordion_list_item_piece';
import GlobalNotificationModel from '../../../../../models/global_notification_model'; import GlobalNotificationModel from '../../../../../../models/global_notification_model';
import GlobalNotificationActions from '../../../../../actions/global_notification_actions'; import GlobalNotificationActions from '../../../../../../actions/global_notification_actions';
import AclProxy from '../../../../acl_proxy'; import AclProxy from '../../../../../acl_proxy';
import SubmitToPrizeButton from './../ascribe_buttons/submit_to_prize_button'; import SubmitToPrizeButton from './../ascribe_buttons/submit_to_prize_button';
import { getLangText } from '../../../../../utils/lang_utils'; import { getLangText } from '../../../../../../utils/lang_utils';
import { mergeOptions } from '../../../../../utils/general_utils'; import { mergeOptions } from '../../../../../../utils/general_utils';
let AccordionListItemPrize = React.createClass({ let AccordionListItemPrize = React.createClass({
@ -182,7 +183,7 @@ let AccordionListItemPrize = React.createClass({
artistName={artistName} artistName={artistName}
subsubheading={ subsubheading={
<div> <div>
<span>{new Date(this.props.content.date_created).getFullYear()}</span> <span>{Moment(this.props.content.date_created, 'YYYY-MM-DD').year()}</span>
</div>} </div>}
buttons={this.getPrizeButtons()} buttons={this.getPrizeButtons()}
badge={this.getPrizeBadge()}> badge={this.getPrizeBadge()}>

View File

@ -3,10 +3,10 @@
import React from 'react'; import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import ModalWrapper from '../../../../ascribe_modal/modal_wrapper'; import ModalWrapper from '../../../../../ascribe_modal/modal_wrapper';
import PieceSubmitToPrizeForm from '../../../../ascribe_forms/form_submit_to_prize'; import PieceSubmitToPrizeForm from '../../../../../ascribe_forms/form_submit_to_prize';
import { getLangText } from '../../../../../utils/lang_utils'; import { getLangText } from '../../../../../../utils/lang_utils';
let SubmitToPrizeButton = React.createClass({ let SubmitToPrizeButton = React.createClass({
propTypes: { propTypes: {

View File

@ -6,41 +6,44 @@ import Moment from 'moment';
import StarRating from 'react-star-rating'; import StarRating from 'react-star-rating';
import PieceActions from '../../../../../actions/piece_actions'; import PieceActions from '../../../../../../actions/piece_actions';
import PieceStore from '../../../../../stores/piece_store'; import PieceStore from '../../../../../../stores/piece_store';
import PieceListStore from '../../../../../stores/piece_list_store'; import PieceListStore from '../../../../../../stores/piece_list_store';
import PieceListActions from '../../../../../actions/piece_list_actions'; import PieceListActions from '../../../../../../actions/piece_list_actions';
import PrizeRatingActions from '../../actions/prize_rating_actions'; import PrizeRatingActions from '../../actions/prize_rating_actions';
import PrizeRatingStore from '../../stores/prize_rating_store'; import PrizeRatingStore from '../../stores/prize_rating_store';
import UserStore from '../../../../../stores/user_store'; import UserStore from '../../../../../../stores/user_store';
import UserActions from '../../../../../../actions/user_actions';
import Piece from '../../../../../components/ascribe_detail/piece'; import Piece from '../../../../../../components/ascribe_detail/piece';
import Note from '../../../../../components/ascribe_detail/note'; import Note from '../../../../../../components/ascribe_detail/note';
import AscribeSpinner from '../../../../ascribe_spinner'; import AscribeSpinner from '../../../../../ascribe_spinner';
import Form from '../../../../../components/ascribe_forms/form'; import Form from '../../../../../../components/ascribe_forms/form';
import Property from '../../../../../components/ascribe_forms/property'; import Property from '../../../../../../components/ascribe_forms/property';
import InputTextAreaToggable from '../../../../../components/ascribe_forms/input_textarea_toggable'; import InputTextAreaToggable from '../../../../../../components/ascribe_forms/input_textarea_toggable';
import CollapsibleParagraph from '../../../../../components/ascribe_collapsible/collapsible_paragraph'; import CollapsibleParagraph from '../../../../../../components/ascribe_collapsible/collapsible_paragraph';
import InputCheckbox from '../../../../ascribe_forms/input_checkbox'; import FurtherDetailsFileuploader from '../../../../../ascribe_detail/further_details_fileuploader';
import LoanForm from '../../../../ascribe_forms/form_loan';
import ListRequestActions from '../../../../ascribe_forms/list_form_request_actions';
import ModalWrapper from '../../../../ascribe_modal/modal_wrapper';
import GlobalNotificationModel from '../../../../../models/global_notification_model'; import InputCheckbox from '../../../../../ascribe_forms/input_checkbox';
import GlobalNotificationActions from '../../../../../actions/global_notification_actions'; import LoanForm from '../../../../../ascribe_forms/form_loan';
import ListRequestActions from '../../../../../ascribe_forms/list_form_request_actions';
import ModalWrapper from '../../../../../ascribe_modal/modal_wrapper';
import DetailProperty from '../../../../ascribe_detail/detail_property'; import GlobalNotificationModel from '../../../../../../models/global_notification_model';
import GlobalNotificationActions from '../../../../../../actions/global_notification_actions';
import ApiUrls from '../../../../../constants/api_urls'; import DetailProperty from '../../../../../ascribe_detail/detail_property';
import { mergeOptions } from '../../../../../utils/general_utils';
import { getLangText } from '../../../../../utils/lang_utils'; import ApiUrls from '../../../../../../constants/api_urls';
import { setDocumentTitle } from '../../../../../utils/dom_utils'; import { mergeOptions } from '../../../../../../utils/general_utils';
import { getLangText } from '../../../../../../utils/lang_utils';
import { setDocumentTitle } from '../../../../../../utils/dom_utils';
/** /**
@ -48,7 +51,8 @@ import { setDocumentTitle } from '../../../../../utils/dom_utils';
*/ */
let PieceContainer = React.createClass({ let PieceContainer = React.createClass({
propTypes: { propTypes: {
params: React.PropTypes.object params: React.PropTypes.object,
location: React.PropTypes.object
}, },
getInitialState() { getInitialState() {
@ -62,6 +66,7 @@ let PieceContainer = React.createClass({
PieceStore.listen(this.onChange); PieceStore.listen(this.onChange);
PieceActions.fetchOne(this.props.params.pieceId); PieceActions.fetchOne(this.props.params.pieceId);
UserStore.listen(this.onChange); UserStore.listen(this.onChange);
UserActions.fetchCurrentUser();
// Every time we enter the piece detail page, just reset the piece // Every time we enter the piece detail page, just reset the piece
// store as it will otherwise display wrong/old data once the user loads // store as it will otherwise display wrong/old data once the user loads
@ -142,10 +147,10 @@ let PieceContainer = React.createClass({
<NavigationHeader <NavigationHeader
piece={this.state.piece} piece={this.state.piece}
currentUser={this.state.currentUser}/> currentUser={this.state.currentUser}/>
<hr/>
<h1 className="ascribe-detail-title">{this.state.piece.title}</h1> <h1 className="ascribe-detail-title">{this.state.piece.title}</h1>
<DetailProperty label={getLangText('BY')} value={artistName} /> <DetailProperty label={getLangText('BY')} value={artistName} />
<DetailProperty label={getLangText('DATE')} value={new Date(this.state.piece.date_created).getFullYear()} /> <DetailProperty label={getLangText('DATE')} value={Moment(this.state.piece.date_created, 'YYYY-MM-DD').year()} />
{artistEmail} {artistEmail}
{this.getActions()} {this.getActions()}
<hr/> <hr/>
@ -157,7 +162,7 @@ let PieceContainer = React.createClass({
piece={this.state.piece} piece={this.state.piece}
currentUser={this.state.currentUser}/> currentUser={this.state.currentUser}/>
}> }>
<PrizePieceDetails piece={this.state.piece}/> <PrizePieceDetails piece={this.state.piece} location={this.props.location}/>
</Piece> </Piece>
); );
} else { } else {
@ -177,24 +182,28 @@ let NavigationHeader = React.createClass({
}, },
render() { render() {
if (this.props.currentUser && this.props.currentUser.email && this.props.piece && this.props.piece.navigation) { const { currentUser, piece } = this.props;
let nav = this.props.piece.navigation;
if (currentUser && currentUser.email && currentUser.is_judge && currentUser.is_jury &&
!currentUser.is_admin && piece && piece.navigation) {
let nav = piece.navigation;
return ( return (
<div style={{marginBottom: '1em'}}> <div style={{marginBottom: '1em'}}>
<div className="row no-margin"> <div className="row no-margin">
<Link className="disable-select" to={`/pieces/${ nav.prev_index || this.props.piece.id }`}> <Link className="disable-select" to={`/pieces/${ nav.prev_index || piece.id }`}>
<span className="glyphicon glyphicon-chevron-left pull-left link-ascribe" aria-hidden="true"> <span className="glyphicon glyphicon-chevron-left pull-left link-ascribe" aria-hidden="true">
{getLangText('Previous')} {getLangText('Previous')}
</span> </span>
</Link> </Link>
<Link className="disable-select" to={`/pieces/${ nav.next_index || this.props.piece.id }`}> <Link className="disable-select" to={`/pieces/${ nav.next_index || piece.id }`}>
<span className="pull-right link-ascribe"> <span className="pull-right link-ascribe">
{getLangText('Next')} {getLangText('Next')}
<span className="glyphicon glyphicon-chevron-right" aria-hidden="true"></span> <span className="glyphicon glyphicon-chevron-right" aria-hidden="true"></span>
</span> </span>
</Link> </Link>
</div> </div>
<hr/>
</div> </div>
); );
} }
@ -417,7 +426,8 @@ let PrizePieceRatings = React.createClass({
let PrizePieceDetails = React.createClass({ let PrizePieceDetails = React.createClass({
propTypes: { propTypes: {
piece: React.PropTypes.object piece: React.PropTypes.object,
location: React.PropTypes.object
}, },
render() { render() {
@ -432,6 +442,8 @@ let PrizePieceDetails = React.createClass({
<Form ref='form'> <Form ref='form'>
{Object.keys(this.props.piece.extra_data).map((data) => { {Object.keys(this.props.piece.extra_data).map((data) => {
let label = data.replace('_', ' '); let label = data.replace('_', ' ');
const value = this.props.piece.extra_data[data] || 'N/A';
return ( return (
<Property <Property
name={data} name={data}
@ -440,11 +452,20 @@ let PrizePieceDetails = React.createClass({
overrideForm={true}> overrideForm={true}>
<InputTextAreaToggable <InputTextAreaToggable
rows={1} rows={1}
defaultValue={this.props.piece.extra_data[data]}/> defaultValue={value}/>
</Property>); </Property>
} );
)} })}
<hr /> <FurtherDetailsFileuploader
submitFile={() => {}}
setIsUploadReady={() => {}}
isReadyForFormSubmission={() => {}}
editable={false}
overrideForm={true}
pieceId={this.props.piece.id}
otherData={this.props.piece.other_data}
multiple={true}
location={location}/>
</Form> </Form>
</CollapsibleParagraph> </CollapsibleParagraph>
); );

View File

@ -1,7 +1,7 @@
'use strict'; 'use strict';
import React from 'react'; import React from 'react';
import constants from '../../../../constants/application_constants'; import constants from '../../../../../constants/application_constants';
let Hero = React.createClass({ let Hero = React.createClass({

View File

@ -11,11 +11,11 @@ import ButtonGroup from 'react-bootstrap/lib/ButtonGroup';
import LinkContainer from 'react-router-bootstrap/lib/LinkContainer'; import LinkContainer from 'react-router-bootstrap/lib/LinkContainer';
import UserStore from '../../../../stores/user_store'; import UserStore from '../../../../../stores/user_store';
import UserActions from '../../../../actions/user_actions'; import UserActions from '../../../../../actions/user_actions';
import { mergeOptions } from '../../../../utils/general_utils'; import { mergeOptions } from '../../../../../utils/general_utils';
import { getLangText } from '../../../../utils/lang_utils'; import { getLangText } from '../../../../../utils/lang_utils';
let Landing = React.createClass({ let Landing = React.createClass({

View File

@ -3,10 +3,10 @@
import React from 'react'; import React from 'react';
import { Link } from 'react-router'; import { Link } from 'react-router';
import LoginForm from '../../../ascribe_forms/form_login'; import LoginForm from '../../../../ascribe_forms/form_login';
import { getLangText } from '../../../../utils/lang_utils'; import { getLangText } from '../../../../../utils/lang_utils';
import { setDocumentTitle } from '../../../../utils/dom_utils'; import { setDocumentTitle } from '../../../../../utils/dom_utils';
let LoginContainer = React.createClass({ let LoginContainer = React.createClass({

View File

@ -1,10 +1,10 @@
'use strict'; 'use strict';
import React from 'react'; import React from 'react';
import PieceList from '../../../piece_list'; import PieceList from '../../../../piece_list';
import UserActions from '../../../../actions/user_actions'; import UserActions from '../../../../../actions/user_actions';
import UserStore from '../../../../stores/user_store'; import UserStore from '../../../../../stores/user_store';
import PrizeActions from '../actions/prize_actions'; import PrizeActions from '../actions/prize_actions';
import PrizeStore from '../stores/prize_store'; import PrizeStore from '../stores/prize_store';
@ -15,9 +15,9 @@ import LinkContainer from 'react-router-bootstrap/lib/LinkContainer';
import AccordionListItemPrize from './ascribe_accordion_list/accordion_list_item_prize'; import AccordionListItemPrize from './ascribe_accordion_list/accordion_list_item_prize';
import { mergeOptions } from '../../../../utils/general_utils'; import { mergeOptions } from '../../../../../utils/general_utils';
import { getLangText } from '../../../../utils/lang_utils'; import { getLangText } from '../../../../../utils/lang_utils';
import { setDocumentTitle } from '../../../../utils/dom_utils'; import { setDocumentTitle } from '../../../../../utils/dom_utils';
let PrizePieceList = React.createClass({ let PrizePieceList = React.createClass({
propTypes: { propTypes: {

View File

@ -0,0 +1,101 @@
'use strict';
import React from 'react';
import PrizeActions from '../actions/prize_actions';
import PrizeStore from '../stores/prize_store';
import RegisterPiece from '../../../../register_piece';
import Property from '../../../../ascribe_forms/property';
import InputTextAreaToggable from '../../../../ascribe_forms/input_textarea_toggable';
import InputCheckbox from '../../../../ascribe_forms/input_checkbox';
import { getLangText } from '../../../../../utils/lang_utils';
import { setDocumentTitle } from '../../../../../utils/dom_utils';
let PrizeRegisterPiece = React.createClass({
propTypes: {
location: React.PropTypes.object
},
getInitialState() {
return PrizeStore.getState();
},
componentDidMount() {
PrizeStore.listen(this.onChange);
PrizeActions.fetchPrize();
},
componentWillUnmount() {
PrizeStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
},
render() {
const { location } = this.props;
setDocumentTitle(getLangText('Submit to the prize'));
if(this.state.prize && this.state.prize.active){
return (
<div>
<RegisterPiece
enableLocalHashing={false}
headerMessage={''}
submitMessage={getLangText('Submit')}
location={location}>
<Property
name='artist_statement'
label={getLangText('Artist statement')}
editable={true}
overrideForm={true}>
<InputTextAreaToggable
rows={1}
placeholder={getLangText('Enter your statement')}
required />
</Property>
<Property
name='work_description'
label={getLangText('Work description')}
editable={true}
overrideForm={true}>
<InputTextAreaToggable
rows={1}
placeholder={getLangText('Enter the description for your work')}
required />
</Property>
<Property
name="terms"
className="ascribe-property-collapsible-toggle"
style={{paddingBottom: 0}}>
<InputCheckbox>
<span>
{' ' + getLangText('I agree to the Terms of Service the art price') + ' '}
(<a href="https://s3-us-west-2.amazonaws.com/ascribe0/whitelabel/sluice/terms.pdf" target="_blank" style={{fontSize: '0.9em', color: 'rgba(0,0,0,0.7)'}}>
{getLangText('read')}
</a>)
</span>
</InputCheckbox>
</Property>
</RegisterPiece>
</div>
);
}
else {
return (
<div className='row'>
<div style={{textAlign: 'center'}}>
{getLangText('The prize is no longer active')}
</div>
</div>
);
}
}
});
export default PrizeRegisterPiece;

View File

@ -2,29 +2,29 @@
import React from 'react'; import React from 'react';
import UserStore from '../../../../stores/user_store'; import UserStore from '../../../../../stores/user_store';
import UserActions from '../../../../actions/user_actions'; import UserActions from '../../../../../actions/user_actions';
import PrizeActions from '../actions/prize_actions'; import PrizeActions from '../actions/prize_actions';
import PrizeStore from '../stores/prize_store'; import PrizeStore from '../stores/prize_store';
import PrizeJuryActions from '../actions/prize_jury_actions'; import PrizeJuryActions from '../actions/prize_jury_actions';
import PrizeJuryStore from '../stores/prize_jury_store'; import PrizeJuryStore from '../stores/prize_jury_store';
import SettingsContainer from '../../../ascribe_settings/settings_container'; import SettingsContainer from '../../../../ascribe_settings/settings_container';
import CollapsibleParagraph from '../../../ascribe_collapsible/collapsible_paragraph'; import CollapsibleParagraph from '../../../../ascribe_collapsible/collapsible_paragraph';
import Form from '../../../ascribe_forms/form'; import Form from '../../../../ascribe_forms/form';
import Property from '../../../ascribe_forms/property'; import Property from '../../../../ascribe_forms/property';
import ActionPanel from '../../../ascribe_panel/action_panel'; import ActionPanel from '../../../../ascribe_panel/action_panel';
import GlobalNotificationModel from '../../../../models/global_notification_model'; import GlobalNotificationModel from '../../../../../models/global_notification_model';
import GlobalNotificationActions from '../../../../actions/global_notification_actions'; import GlobalNotificationActions from '../../../../../actions/global_notification_actions';
import AscribeSpinner from '../../../ascribe_spinner'; import AscribeSpinner from '../../../../ascribe_spinner';
import ApiUrls from '../../../../constants/api_urls'; import ApiUrls from '../../../../../constants/api_urls';
import { getLangText } from '../../../../utils/lang_utils'; import { getLangText } from '../../../../../utils/lang_utils';
import { setDocumentTitle } from '../../../../utils/dom_utils'; import { setDocumentTitle } from '../../../../../utils/dom_utils';
let Settings = React.createClass({ let Settings = React.createClass({

View File

@ -1,10 +1,10 @@
'use strict'; 'use strict';
import React from 'react'; import React from 'react';
import SignupForm from '../../../ascribe_forms/form_signup'; import SignupForm from '../../../../ascribe_forms/form_signup';
import { getLangText } from '../../../../utils/lang_utils'; import { getLangText } from '../../../../../utils/lang_utils';
import { setDocumentTitle } from '../../../../utils/dom_utils'; import { setDocumentTitle } from '../../../../../utils/dom_utils';
let SignupContainer = React.createClass({ let SignupContainer = React.createClass({
propTypes: { propTypes: {

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
import requests from '../../../../utils/requests'; import requests from '../../../../../utils/requests';
let PrizeFetcher = { let PrizeFetcher = {

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
import requests from '../../../../utils/requests'; import requests from '../../../../../utils/requests';
let PrizeJuryFetcher = { let PrizeJuryFetcher = {

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
import requests from '../../../../utils/requests'; import requests from '../../../../../utils/requests';
let PrizeRatingFetcher = { let PrizeRatingFetcher = {

View File

@ -2,11 +2,11 @@
import React from 'react'; import React from 'react';
import Hero from './components/prize_hero'; import Hero from './components/prize_hero';
import Header from '../../header'; import Header from '../../../header';
import Footer from '../../footer'; import Footer from '../../../footer';
import GlobalNotification from '../../global_notification'; import GlobalNotification from '../../../global_notification';
import { getSubdomain } from '../../../utils/general_utils'; import { getSubdomain } from '../../../../utils/general_utils';
let PrizeApp = React.createClass({ let PrizeApp = React.createClass({

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
import { alt } from '../../../../alt'; import { alt } from '../../../../../alt';
import PrizeJuryActions from '../actions/prize_jury_actions'; import PrizeJuryActions from '../actions/prize_jury_actions';

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
import { alt } from '../../../../alt'; import { alt } from '../../../../../alt';
import PrizeRatingActions from '../actions/prize_rating_actions'; import PrizeRatingActions from '../actions/prize_rating_actions';

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
import { alt } from '../../../../alt'; import { alt } from '../../../../../alt';
import PrizeActions from '../actions/prize_actions'; import PrizeActions from '../actions/prize_actions';

View File

@ -47,7 +47,7 @@ let WalletActionPanel = React.createClass({
<AclButtonList <AclButtonList
className="text-center ascribe-button-list" className="text-center ascribe-button-list"
availableAcls={availableAcls} availableAcls={availableAcls}
editions={this.props.piece} pieceOrEditions={this.props.piece}
handleSuccess={this.props.loadPiece}> handleSuccess={this.props.loadPiece}>
<AclProxy <AclProxy
aclObject={this.props.currentUser.acl} aclObject={this.props.currentUser.acl}

View File

@ -1,6 +1,7 @@
'use strict'; 'use strict';
import React from 'react'; import React from 'react';
import Moment from 'moment';
import Piece from '../../../../../components/ascribe_detail/piece'; import Piece from '../../../../../components/ascribe_detail/piece';
@ -39,7 +40,7 @@ let WalletPieceContainer = React.createClass({
<hr style={{marginTop: 0}}/> <hr style={{marginTop: 0}}/>
<h1 className="ascribe-detail-title">{this.props.piece.title}</h1> <h1 className="ascribe-detail-title">{this.props.piece.title}</h1>
<DetailProperty label="BY" value={this.props.piece.artist_name} /> <DetailProperty label="BY" value={this.props.piece.artist_name} />
<DetailProperty label="DATE" value={new Date(this.props.piece.date_created).getFullYear()} /> <DetailProperty label="DATE" value={Moment(this.props.piece.date_created, 'YYYY-MM-DD').year()} />
<hr/> <hr/>
</div> </div>
} }

View File

@ -1,6 +1,7 @@
'use strict'; 'use strict';
import React from 'react'; import React from 'react';
import Moment from 'moment';
import AccordionListItemPiece from '../../../../../ascribe_accordion_list/accordion_list_item_piece'; import AccordionListItemPiece from '../../../../../ascribe_accordion_list/accordion_list_item_piece';
@ -100,7 +101,7 @@ let CylandAccordionListItem = React.createClass({
piece={this.props.content} piece={this.props.content}
subsubheading={ subsubheading={
<div className="pull-left"> <div className="pull-left">
<span>{new Date(this.props.content.date_created).getFullYear()}</span> <span>{Moment(this.props.content.date_created, 'YYYY-MM-DD').year()}</span>
</div>} </div>}
buttons={this.getSubmitButtons()}> buttons={this.getSubmitButtons()}>
{this.props.children} {this.props.children}

View File

@ -33,7 +33,6 @@ import { mergeOptions } from '../../../../../../utils/general_utils';
let CylandPieceContainer = React.createClass({ let CylandPieceContainer = React.createClass({
propTypes: { propTypes: {
location: React.PropTypes.object,
params: React.PropTypes.object params: React.PropTypes.object
}, },
@ -106,8 +105,7 @@ let CylandPieceContainer = React.createClass({
<CylandAdditionalDataForm <CylandAdditionalDataForm
piece={this.state.piece} piece={this.state.piece}
disabled={!this.state.piece.acl.acl_edit} disabled={!this.state.piece.acl.acl_edit}
isInline={true} isInline={true} />
location={this.props.location}/>
</CollapsibleParagraph> </CollapsibleParagraph>
</WalletPieceContainer> </WalletPieceContainer>
); );

View File

@ -26,8 +26,7 @@ let CylandAdditionalDataForm = React.createClass({
handleSuccess: React.PropTypes.func, handleSuccess: React.PropTypes.func,
piece: React.PropTypes.object.isRequired, piece: React.PropTypes.object.isRequired,
disabled: React.PropTypes.bool, disabled: React.PropTypes.bool,
isInline: React.PropTypes.bool, isInline: React.PropTypes.bool
location: React.PropTypes.object
}, },
getDefaultProps() { getDefaultProps() {
@ -78,7 +77,7 @@ let CylandAdditionalDataForm = React.createClass({
}, },
render() { render() {
let { piece, isInline, disabled, handleSuccess } = this.props; let { piece, isInline, disabled, handleSuccess, location } = this.props;
let buttons, spinner, heading; let buttons, spinner, heading;
if(!isInline) { if(!isInline) {
@ -122,29 +121,76 @@ let CylandAdditionalDataForm = React.createClass({
{heading} {heading}
<Property <Property
name='artist_bio' name='artist_bio'
label={getLangText('Artist Biography')}> label={getLangText('Artist Biography')}
hidden={disabled && !piece.extra_data.artist_bio}>
<InputTextAreaToggable <InputTextAreaToggable
rows={1} rows={1}
defaultValue={piece.extra_data.artist_bio} defaultValue={piece.extra_data.artist_bio}
placeholder={getLangText('Enter the artist\'s biography...')}/> placeholder={getLangText('Enter the artist\'s biography...')}/>
</Property> </Property>
<Property
name='artist_contact_information'
label={getLangText('Artist Contact Information')}
hidden={disabled && !piece.extra_data.artist_contact_information}>
<InputTextAreaToggable
rows={1}
defaultValue={piece.extra_data.artist_contact_information}
placeholder={getLangText('Enter the artist\'s contact information...')}/>
</Property>
<Property <Property
name='conceptual_overview' name='conceptual_overview'
label={getLangText('Conceptual Overview')}> label={getLangText('Conceptual Overview')}
hidden={disabled && !piece.extra_data.conceptual_overview}>
<InputTextAreaToggable <InputTextAreaToggable
rows={1} rows={1}
defaultValue={piece.extra_data.conceptual_overview} defaultValue={piece.extra_data.conceptual_overview}
placeholder={getLangText('Enter a conceptual overview...')}/> placeholder={getLangText('Enter a conceptual overview...')}/>
</Property> </Property>
<Property
name='medium'
label={getLangText('Medium (technical specifications)')}
hidden={disabled && !piece.extra_data.medium}>
<InputTextAreaToggable
rows={1}
defaultValue={piece.extra_data.medium}
placeholder={getLangText('Enter the medium (and other technical specifications)...')}/>
</Property>
<Property
name='size_duration'
label={getLangText('Size / Duration')}
hidden={disabled && !piece.extra_data.size_duration}>
<InputTextAreaToggable
rows={1}
defaultValue={piece.extra_data.size_duration}
placeholder={getLangText('Enter the size / duration...')}/>
</Property>
<Property
name='display_instructions'
label={getLangText('Display instructions')}
hidden={disabled && !piece.extra_data.display_instructions}>
<InputTextAreaToggable
rows={1}
defaultValue={piece.extra_data.display_instructions}
placeholder={getLangText('Enter the display instructions...')}/>
</Property>
<Property
name='additional_details'
label={getLangText('Additional details')}
hidden={disabled && !piece.extra_data.additional_details}>
<InputTextAreaToggable
rows={1}
defaultValue={piece.extra_data.additional_details}
placeholder={getLangText('Enter additional details...')}/>
</Property>
<FurtherDetailsFileuploader <FurtherDetailsFileuploader
label={getLangText('Additional files (e.g. still images, pdf)')}
uploadStarted={this.uploadStarted} uploadStarted={this.uploadStarted}
submitFile={this.submitFile} submitFile={this.submitFile}
setIsUploadReady={this.setIsUploadReady} setIsUploadReady={this.setIsUploadReady}
isReadyForFormSubmission={formSubmissionValidation.fileOptional} isReadyForFormSubmission={formSubmissionValidation.fileOptional}
pieceId={piece.id} pieceId={piece.id}
otherData={piece.other_data} otherData={piece.other_data}
multiple={true} multiple={true} />
location={this.props.location}/>
</Form> </Form>
); );
} else { } else {

View File

@ -210,8 +210,7 @@ let CylandRegisterPiece = React.createClass({
<CylandAdditionalDataForm <CylandAdditionalDataForm
disabled={this.state.step > 1} disabled={this.state.step > 1}
handleSuccess={this.handleAdditionalDataSuccess} handleSuccess={this.handleAdditionalDataSuccess}
piece={this.state.piece} piece={this.state.piece} />
location={this.props.location}/>
</Col> </Col>
</Row> </Row>
</div> </div>
@ -220,7 +219,12 @@ let CylandRegisterPiece = React.createClass({
<Col xs={12} sm={10} md={8} smOffset={1} mdOffset={2}> <Col xs={12} sm={10} md={8} smOffset={1} mdOffset={2}>
<LoanForm <LoanForm
loanHeading={getLangText('Loan to Cyland archive')} loanHeading={getLangText('Loan to Cyland archive')}
message={getAclFormMessage('acl_loan', '\"' + this.state.piece.title + '\"', this.state.currentUser.username)} message={getAclFormMessage({
aclName: 'acl_loan',
entities: this.state.piece,
isPiece: true,
senderName: this.state.currentUser.username
})}
id={{piece_id: this.state.piece.id}} id={{piece_id: this.state.piece.id}}
url={ApiUrls.ownership_loans_pieces} url={ApiUrls.ownership_loans_pieces}
email={this.state.whitelabel.user} email={this.state.whitelabel.user}

View File

@ -1,6 +1,7 @@
'use strict'; 'use strict';
import React from 'react'; import React from 'react';
import Moment from 'moment';
import AccordionListItemPiece from '../../../../../ascribe_accordion_list/accordion_list_item_piece'; import AccordionListItemPiece from '../../../../../ascribe_accordion_list/accordion_list_item_piece';
@ -106,7 +107,7 @@ let IkonotvAccordionListItem = React.createClass({
piece={this.props.content} piece={this.props.content}
subsubheading={ subsubheading={
<div className="pull-left"> <div className="pull-left">
<span>{new Date(this.props.content.date_created).getFullYear()}</span> <span>{Moment(this.props.content.date_created, 'YYYY-MM-DD').year()}</span>
</div>} </div>}
buttons={this.getSubmitButtons()}> buttons={this.getSubmitButtons()}>
{this.props.children} {this.props.children}

View File

@ -14,6 +14,7 @@ let ApiUrls = {
'blob_digitalworks': AppConstants.apiEndpoint + 'blob/digitalworks/', 'blob_digitalworks': AppConstants.apiEndpoint + 'blob/digitalworks/',
'blob_otherdatas': AppConstants.apiEndpoint + 'blob/otherdatas/', 'blob_otherdatas': AppConstants.apiEndpoint + 'blob/otherdatas/',
'blob_contracts': AppConstants.apiEndpoint + 'blob/contracts/', 'blob_contracts': AppConstants.apiEndpoint + 'blob/contracts/',
'blob_thumbnails': AppConstants.apiEndpoint + 'blob/thumbnails/',
'coa': AppConstants.apiEndpoint + 'coa/${id}/', 'coa': AppConstants.apiEndpoint + 'coa/${id}/',
'coa_create': AppConstants.apiEndpoint + 'coa/', 'coa_create': AppConstants.apiEndpoint + 'coa/',
'coa_verify': AppConstants.apiEndpoint + 'coa/verify_coa/', 'coa_verify': AppConstants.apiEndpoint + 'coa/verify_coa/',

View File

@ -11,7 +11,7 @@ let constants = {
'serverUrl': window.SERVER_URL, 'serverUrl': window.SERVER_URL,
'baseUrl': window.BASE_URL, 'baseUrl': window.BASE_URL,
'aclList': ['acl_coa', 'acl_consign', 'acl_delete', 'acl_download', 'acl_edit', 'acl_create_editions', 'acl_view_editions', 'aclList': ['acl_coa', 'acl_consign', 'acl_delete', 'acl_download', 'acl_edit', 'acl_create_editions', 'acl_view_editions',
'acl_loan', 'acl_share', 'acl_transfer', 'acl_unconsign', 'acl_unshare', 'acl_view', 'acl_loan', 'acl_loan_request', 'acl_share', 'acl_transfer', 'acl_unconsign', 'acl_unshare', 'acl_view',
'acl_withdraw_transfer', 'acl_wallet_submit'], 'acl_withdraw_transfer', 'acl_wallet_submit'],
'version': 0.1, 'version': 0.1,
@ -46,6 +46,13 @@ let constants = {
'logo': 'https://s3-us-west-2.amazonaws.com/ascribe0/whitelabel/ikonotv/ikono-logo-black.png', 'logo': 'https://s3-us-west-2.amazonaws.com/ascribe0/whitelabel/ikonotv/ikono-logo-black.png',
'permissions': ['register', 'edit', 'share', 'del_from_collection'], 'permissions': ['register', 'edit', 'share', 'del_from_collection'],
'type': 'wallet' 'type': 'wallet'
},
{
'subdomain': 'portfolioreview',
'name': 'Portfolio Review',
'logo': 'http://notfoundlogo.de',
'permissions': ['register', 'edit', 'share', 'del_from_collection'],
'type': 'prize'
} }
], ],
'defaultDomain': { 'defaultDomain': {

View File

@ -2,8 +2,8 @@
import requests from '../utils/requests'; import requests from '../utils/requests';
import { generateOrderingQueryParams } from '../utils/fetch_api_utils';
import { mergeOptions } from '../utils/general_utils'; import { mergeOptions } from '../utils/general_utils';
import { generateOrderingQueryParams } from '../utils/url_utils';
let EditionListFetcher = { let EditionListFetcher = {
/** /**

View File

@ -3,7 +3,7 @@
import requests from '../utils/requests'; import requests from '../utils/requests';
import { mergeOptions } from '../utils/general_utils'; import { mergeOptions } from '../utils/general_utils';
import { generateOrderingQueryParams } from '../utils/fetch_api_utils'; import { generateOrderingQueryParams } from '../utils/url_utils';
let PieceListFetcher = { let PieceListFetcher = {
/** /**

View File

@ -1,64 +1,12 @@
'use strict'; 'use strict';
import Q from 'q'; import Q from 'q';
import moment from 'moment';
import { sanitize } from './general_utils';
import AppConstants from '../constants/application_constants'; import AppConstants from '../constants/application_constants';
// TODO: Create Unittests that test all functions // TODO: Create Unittests that test all functions
/**
* Takes a key-value object of this form:
*
* {
* 'page': 1,
* 'pageSize': 10
* }
*
* and converts it to a query-parameter, which you can append to your URL.
* The return looks like this:
*
* ?page=1&page_size=10
*
* CamelCase gets converted to snake_case!
*
*/
export function argsToQueryParams(obj) {
obj = sanitize(obj);
return Object
.keys(obj)
.map((key, i) => {
let s = '';
if(i === 0) {
s += '?';
} else {
s += '&';
}
let snakeCaseKey = key.replace(/[A-Z]/, (match) => '_' + match.toLowerCase());
return s + snakeCaseKey + '=' + encodeURIComponent(obj[key]);
})
.join('');
}
/**
* Takes a string and a boolean and generates a string query parameter for
* an API call.
*/
export function generateOrderingQueryParams(orderBy, orderAsc) {
let interpolation = '';
if(!orderAsc) {
interpolation += '-';
}
return interpolation + orderBy;
}
export function status(response) { export function status(response) {
if (response.status >= 200 && response.status < 300) { if (response.status >= 200 && response.status < 300) {
return response; return response;
@ -70,12 +18,19 @@ export function getCookie(name) {
let parts = document.cookie.split(';'); let parts = document.cookie.split(';');
for(let i = 0; i < parts.length; i++) { for(let i = 0; i < parts.length; i++) {
if(parts[i].indexOf(AppConstants.csrftoken + '=') > -1) { if(parts[i].indexOf(name + '=') > -1) {
return parts[i].split('=').pop(); return parts[i].split('=').pop();
} }
} }
} }
export function setCookie(key, value, days) {
const exdate = moment();
exdate.add(days, 'days');
value = window.escape(value) + ((days === null) ? '' : `; expires= ${exdate.utc()}`);
document.cookie = `${key}=${value}`;
}
/* /*
Given a url for an image, this method fetches it and returns a promise that resolves to Given a url for an image, this method fetches it and returns a promise that resolves to

View File

@ -2,6 +2,7 @@
import Q from 'q'; import Q from 'q';
import SparkMD5 from 'spark-md5'; import SparkMD5 from 'spark-md5';
import Moment from 'moment';
import { getLangText } from './lang_utils'; import { getLangText } from './lang_utils';
@ -37,7 +38,7 @@ export function computeHashOfFile(file) {
let spark = new SparkMD5.ArrayBuffer(); let spark = new SparkMD5.ArrayBuffer();
let fileReader = new FileReader(); let fileReader = new FileReader();
let startTime = new Date(); let startTime = new Moment();
// comment: We should convert this to es6 at some point, however if so please consider that // comment: We should convert this to es6 at some point, however if so please consider that
// an arrow function will get rid of the function's scope... // an arrow function will get rid of the function's scope...
@ -53,7 +54,7 @@ export function computeHashOfFile(file) {
console.info('computed hash %s (took %d s)', console.info('computed hash %s (took %d s)',
fileHash, fileHash,
Math.round(((new Date() - startTime) / 1000) % 60)); // Compute hash Math.round(((new Moment() - startTime) / 1000) % 60)); // Compute hash
let blobTextFile = makeTextFile(fileHash, file); let blobTextFile = makeTextFile(fileHash, file);
resolve(blobTextFile); resolve(blobTextFile);

View File

@ -2,14 +2,40 @@
import { getLangText } from './lang_utils'; import { getLangText } from './lang_utils';
/**
* Get the data ids of the given piece or editions.
* @param {boolean} isPiece Is the given entities parameter a piece? (False: array of editions)
* @param {(object|object[])} pieceOrEditions Piece or array of editions
* @return {(object|object[])} Data IDs of the pieceOrEditions for the form
*/
export function getAclFormDataId(isPiece, pieceOrEditions) {
if (isPiece) {
return {piece_id: pieceOrEditions.id};
} else {
return {bitcoin_id: pieceOrEditions.map(function(edition){
return edition.bitcoin_id;
}).join()};
}
}
/** /**
* Generates a message for submitting a form * Generates a message for submitting a form
* @param {string} aclName Enum name of a acl * @param {object} options Options object for creating the message:
* @param {string} entities Already computed name of entities * @param {string} options.aclName Enum name of an acl
* @param {string} senderName Name of the sender * @param {(object|object[])} options.entities Piece or array of Editions
* @param {boolean} options.isPiece Is the given entities parameter a piece? (False: array of editions)
* @param {string} [options.senderName] Name of the sender
* @return {string} Completed message * @return {string} Completed message
*/ */
export function getAclFormMessage(aclName, entities, senderName) { export function getAclFormMessage(options) {
if (!options || options.aclName === undefined || options.isPiece === undefined ||
!(typeof options.entities === 'object' || options.entities.constructor === Array)) {
throw new Error('You must specify an acl class, entities in the correct format, and entity type');
}
let aclName = options.aclName;
let entityTitles = options.isPiece ? getTitlesStringOfPiece(options.entities)
: getTitlesStringOfEditions(options.entities);
let message = ''; let message = '';
message += getLangText('Hi'); message += getLangText('Hi');
@ -32,7 +58,7 @@ export function getAclFormMessage(aclName, entities, senderName) {
} }
message += ':\n'; message += ':\n';
message += entities; message += entityTitles;
if(aclName === 'acl_transfer' || aclName === 'acl_loan' || aclName === 'acl_consign') { if(aclName === 'acl_transfer' || aclName === 'acl_loan' || aclName === 'acl_consign') {
message += getLangText('to you'); message += getLangText('to you');
@ -44,10 +70,22 @@ export function getAclFormMessage(aclName, entities, senderName) {
throw new Error('Your specified aclName did not match a an acl class.'); throw new Error('Your specified aclName did not match a an acl class.');
} }
if (options.senderName) {
message += '\n\n'; message += '\n\n';
message += getLangText('Truly yours,'); message += getLangText('Truly yours,');
message += '\n'; message += '\n';
message += senderName; message += options.senderName;
}
return message; return message;
} }
function getTitlesStringOfPiece(piece){
return '\"' + piece.title + '\"';
}
function getTitlesStringOfEditions(editions) {
return editions.map(function(edition) {
return '- \"' + edition.title + ', ' + getLangText('edition') + ' ' + edition.edition_number + '\"\n';
}).join('');
}

View File

@ -1,29 +1,21 @@
'use strict'; 'use strict';
/** /**
* Takes an object and deletes all keys that are * Takes an object and returns a shallow copy without any keys
* * that fail the passed in filter function.
* tagged as false by the passed in filter function * Does not modify the passed in object.
* *
* @param {object} obj regular javascript object * @param {object} obj regular javascript object
* @return {object} regular javascript object without null values or empty strings * @return {object} regular javascript object without null values or empty strings
*/ */
export function sanitize(obj, filterFn) { export function sanitize(obj, filterFn) {
if(!filterFn) { if (!filterFn) {
// By matching null with a double equal, we can match undefined and null // By matching null with a double equal, we can match undefined and null
// http://stackoverflow.com/a/15992131 // http://stackoverflow.com/a/15992131
filterFn = (val) => val == null || val === ''; filterFn = (val) => val == null || val === '';
} }
Object return omitFromObject(obj, filterFn);
.keys(obj)
.map((key) => {
if(filterFn(obj[key])) {
delete obj[key];
}
});
return obj;
} }
/** /**
@ -82,8 +74,8 @@ export function formatText() {
}); });
} }
/* /**
Checks a list of objects for key duplicates and returns a boolean * Checks a list of objects for key duplicates and returns a boolean
*/ */
function _doesObjectListHaveDuplicates(l) { function _doesObjectListHaveDuplicates(l) {
let mergedList = []; let mergedList = [];
@ -121,35 +113,7 @@ export function mergeOptions(...l) {
throw new Error('The objects you submitted for merging have duplicates. Merge aborted.'); throw new Error('The objects you submitted for merging have duplicates. Merge aborted.');
} }
let newObj = {}; return Object.assign({}, ...l);
for(let i = 1; i < l.length; i++) {
newObj = _mergeOptions(newObj, _mergeOptions(l[i - 1], l[i]));
}
return newObj;
}
/**
* Merges a number of objects even if there're having duplicates.
*
* DOES NOT RETURN AN ERROR!
*
* Takes a list of object and merges their keys to one object.
* Uses mergeOptions for two objects.
* @param {[type]} l [description]
* @return {[type]} [description]
*/
export function mergeOptionsWithDuplicates(...l) {
// If the objects submitted in the list have duplicates,in their key names,
// abort the merge and tell the function's user to check his objects.
let newObj = {};
for(let i = 1; i < l.length; i++) {
newObj = _mergeOptions(newObj, _mergeOptions(l[i - 1], l[i]));
}
return newObj;
} }
/** /**
@ -165,25 +129,6 @@ export function update(a, ...l) {
return a; return a;
} }
/**
* Overwrites obj1's values with obj2's and adds obj2's if non existent in obj1
* @param obj1
* @param obj2
* @returns obj3 a new object based on obj1 and obj2
* Taken from: http://stackoverflow.com/a/171256/1263876
*/
function _mergeOptions(obj1, obj2) {
let obj3 = {};
for (let attrname in obj1) {
obj3[attrname] = obj1[attrname];
}
for (let attrname in obj2) {
obj3[attrname] = obj2[attrname];
}
return obj3;
}
/** /**
* Escape HTML in a string so it can be injected safely using * Escape HTML in a string so it can be injected safely using
* React's `dangerouslySetInnerHTML` * React's `dangerouslySetInnerHTML`
@ -196,14 +141,41 @@ export function escapeHTML(s) {
return document.createElement('div').appendChild(document.createTextNode(s)).parentNode.innerHTML; return document.createElement('div').appendChild(document.createTextNode(s)).parentNode.innerHTML;
} }
export function excludePropFromObject(obj, propList){ /**
let clonedObj = mergeOptions({}, obj); * Returns a copy of the given object's own and inherited enumerable
for (let item in propList){ * properties, omitting any keys that pass the given filter function.
if (clonedObj[propList[item]]){ */
delete clonedObj[propList[item]]; function filterObjOnFn(obj, filterFn) {
const filteredObj = {};
for (let key in obj) {
const val = obj[key];
if (filterFn == null || !filterFn(val, key)) {
filteredObj[key] = val;
} }
} }
return clonedObj;
return filteredObj;
}
/**
* Similar to lodash's _.omit(), this returns a copy of the given object's
* own and inherited enumerable properties, omitting any keys that are
* in the given array or whose value pass the given filter function.
* @param {object} obj Source object
* @param {array|function} filter Array of key names to omit or function to invoke per iteration
* @return {object} The new object
*/
export function omitFromObject(obj, filter) {
if (filter && filter.constructor === Array) {
return filterObjOnFn(obj, (_, key) => {
return filter.indexOf(key) >= 0;
});
} else if (filter && typeof filter === 'function') {
return filterObjOnFn(obj, filter);
} else {
throw new Error('The given filter is not an array or function. Exclude aborted');
}
} }
/** /**

View File

@ -22,15 +22,15 @@ export function getLangText(s, ...args) {
let lang = getLang(); let lang = getLang();
try { try {
if(lang in languages) { if(lang in languages) {
return formatText(languages[lang][s], args); return formatText(languages[lang][s], ...args);
} else { } else {
// just use the english language // just use the english language
return formatText(languages['en-US'][s], args); return formatText(languages['en-US'][s], ...args);
} }
} catch(err) { } catch(err) {
//if(!(s in languages[lang])) { //if(!(s in languages[lang])) {
//console.warn('Language-string is not in constants file. Add: "' + s + '" to the "' + lang + '" language file. Defaulting to keyname'); //console.warn('Language-string is not in constants file. Add: "' + s + '" to the "' + lang + '" language file. Defaulting to keyname');
return formatText(s, args); return formatText(s, ...args);
//} else { //} else {
// console.error(err); // console.error(err);
//} //}

View File

@ -2,24 +2,14 @@
import Q from 'q'; import Q from 'q';
import { argsToQueryParams, getCookie } from '../utils/fetch_api_utils';
import AppConstants from '../constants/application_constants'; import AppConstants from '../constants/application_constants';
import {excludePropFromObject} from '../utils/general_utils'; import { getCookie } from '../utils/fetch_api_utils';
import { omitFromObject } from '../utils/general_utils';
import { argsToQueryParams } from '../utils/url_utils';
class Requests { class Requests {
_merge(defaults, options) {
let merged = {};
for (let key in defaults) {
merged[key] = defaults[key];
}
for (let key in options) {
merged[key] = options[key];
}
return merged;
}
unpackResponse(response) { unpackResponse(response) {
if (response.status >= 500) { if (response.status >= 500) {
throw new Error(response.status + ' - ' + response.statusText + ' - on URL:' + response.url); throw new Error(response.status + ' - ' + response.statusText + ' - on URL:' + response.url);
@ -112,7 +102,7 @@ class Requests {
request(verb, url, options) { request(verb, url, options) {
options = options || {}; options = options || {};
let merged = this._merge(this.httpOptions, options); let merged = Object.assign({}, this.httpOptions, options);
let csrftoken = getCookie(AppConstants.csrftoken); let csrftoken = getCookie(AppConstants.csrftoken);
if (csrftoken) { if (csrftoken) {
merged.headers['X-CSRFToken'] = csrftoken; merged.headers['X-CSRFToken'] = csrftoken;
@ -124,23 +114,23 @@ class Requests {
} }
get(url, params) { get(url, params) {
if (url === undefined){ if (url === undefined) {
throw new Error('Url undefined'); throw new Error('Url undefined');
} }
let paramsCopy = this._merge(params); let paramsCopy = Object.assign({}, params);
let newUrl = this.prepareUrl(url, paramsCopy, true); let newUrl = this.prepareUrl(url, paramsCopy, true);
return this.request('get', newUrl); return this.request('get', newUrl);
} }
delete(url, params) { delete(url, params) {
let paramsCopy = this._merge(params); let paramsCopy = Object.assign({}, params);
let newUrl = this.prepareUrl(url, paramsCopy, true); let newUrl = this.prepareUrl(url, paramsCopy, true);
return this.request('delete', newUrl); return this.request('delete', newUrl);
} }
_putOrPost(url, paramsAndBody, method){ _putOrPost(url, paramsAndBody, method) {
let paramsCopy = this._merge(paramsAndBody); let paramsCopy = Object.assign({}, paramsAndBody);
let params = excludePropFromObject(paramsAndBody, ['body']); let params = omitFromObject(paramsAndBody, ['body']);
let newUrl = this.prepareUrl(url, params); let newUrl = this.prepareUrl(url, params);
let body = null; let body = null;
if (paramsCopy && paramsCopy.body) { if (paramsCopy && paramsCopy.body) {
@ -153,11 +143,11 @@ class Requests {
return this._putOrPost(url, params, 'post'); return this._putOrPost(url, params, 'post');
} }
put(url, params){ put(url, params) {
return this._putOrPost(url, params, 'put'); return this._putOrPost(url, params, 'put');
} }
patch(url, params){ patch(url, params) {
return this._putOrPost(url, params, 'patch'); return this._putOrPost(url, params, 'patch');
} }

83
js/utils/url_utils.js Normal file
View File

@ -0,0 +1,83 @@
'use strict'
import camelCase from 'camelcase';
import decamelize from 'decamelize';
import qs from 'qs';
import { sanitize } from './general_utils';
// TODO: Create Unittests that test all functions
/**
* Takes a key-value dictionary of this form:
*
* {
* 'page': 1,
* 'pageSize': 10
* }
*
* and converts it to a query-parameter, which you can append to your URL.
* The return looks like this:
*
* ?page=1&page_size=10
*
* CamelCase gets converted to snake_case!
*
* @param {object} obj Query params dictionary
* @return {string} Query params string
*/
export function argsToQueryParams(obj) {
const sanitizedObj = sanitize(obj);
const queryParamObj = {};
Object
.keys(sanitizedObj)
.forEach((key) => {
queryParamObj[decamelize(key)] = sanitizedObj[key];
});
// Use bracket arrayFormat as history.js and react-router use it
return '?' + qs.stringify(queryParamObj, { arrayFormat: 'brackets' });
}
/**
* Get the current url's query params as an key-val dictionary.
* snake_case gets converted to CamelCase!
* @return {object} Query params dictionary
*/
export function getCurrentQueryParams() {
return queryParamsToArgs(window.location.search.substring(1));
}
/**
* Convert the given query param string into a key-val dictionary.
* snake_case gets converted to CamelCase!
* @param {string} queryParamString Query params string
* @return {object} Query params dictionary
*/
export function queryParamsToArgs(queryParamString) {
const qsQueryParamObj = qs.parse(queryParamString);
const camelCaseParamObj = {};
Object
.keys(qsQueryParamObj)
.forEach((key) => {
camelCaseParamObj[camelCase(key)] = qsQueryParamObj[key];
});
return camelCaseParamObj;
}
/**
* Takes a string and a boolean and generates a string query parameter for
* an API call.
*/
export function generateOrderingQueryParams(orderBy, orderAsc) {
let interpolation = '';
if(!orderAsc) {
interpolation += '-';
}
return interpolation + orderBy;
}

View File

@ -46,8 +46,10 @@
"browser-sync": "^2.7.5", "browser-sync": "^2.7.5",
"browserify": "^9.0.8", "browserify": "^9.0.8",
"browserify-shim": "^3.8.10", "browserify-shim": "^3.8.10",
"camelcase": "^1.2.1",
"classnames": "^1.2.2", "classnames": "^1.2.2",
"compression": "^1.4.4", "compression": "^1.4.4",
"decamelize": "^1.1.1",
"envify": "^3.4.0", "envify": "^3.4.0",
"eslint": "^0.22.1", "eslint": "^0.22.1",
"eslint-plugin-react": "^2.5.0", "eslint-plugin-react": "^2.5.0",
@ -73,6 +75,7 @@
"object-assign": "^2.0.0", "object-assign": "^2.0.0",
"opn": "^3.0.2", "opn": "^3.0.2",
"q": "^1.4.1", "q": "^1.4.1",
"qs": "^4.0.0",
"raven-js": "^1.1.19", "raven-js": "^1.1.19",
"react": "0.13.2", "react": "0.13.2",
"react-bootstrap": "0.25.1", "react-bootstrap": "0.25.1",

View File

@ -508,7 +508,10 @@ fieldset[disabled] .btn-secondary.active {
> pre, > pre,
> select, > select,
> span:not(.glyphicon), > span:not(.glyphicon),
> p,
> p > span,
> textarea { > textarea {
color: $ascribe-dark-blue;
font-family: $ascribe--font; font-family: $ascribe--font;
font-weight: $ascribe--font-weight-light; font-weight: $ascribe--font-weight-light;
} }

View File

@ -30,11 +30,14 @@ $ascribe-red-error: rgb(169, 68, 66);
border-left: 3px solid rgba($ascribe-red-error, 1); border-left: 3px solid rgba($ascribe-red-error, 1);
> div { > div {
> p {
> span { > span {
color: rgba($ascribe-red-error, 1); color: rgba($ascribe-red-error, 1);
font-size: .9em; font-size: .9em;
margin-right: 1em; margin-right: 1em;
} }
}
> input, > input,
> textarea { > textarea {
@ -86,11 +89,14 @@ $ascribe-red-error: rgb(169, 68, 66);
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
} }
> p {
height: 20px;
margin-bottom: 0;
> span { > span {
color: rgba(0, 0, 0, .5);
font-size: .9em; font-size: .9em;
font-weight: normal; font-weight: normal;
} }
}
> div { > div {
> div:not(.file-drag-and-drop div) { > div:not(.file-drag-and-drop div) {
@ -107,6 +113,11 @@ $ascribe-red-error: rgb(169, 68, 66);
margin-top: 0 !important; margin-top: 0 !important;
} }
> .upload-button-wrapper {
margin-top: 1em;
margin-bottom: 1em;
}
> input, > input,
> pre, > pre,
> textarea, > textarea,
@ -139,7 +150,7 @@ $ascribe-red-error: rgb(169, 68, 66);
padding: 0; padding: 0;
} }
> textarea{ > textarea {
color: #666; color: #666;
margin-top: 1em; margin-top: 1em;
padding: 0; padding: 0;

View File

@ -177,3 +177,15 @@
height: 12px; height: 12px;
} }
.upload-button-wrapper {
text-align: left;
.btn {
font-size: 1em;
margin-right: 1em;
}
span + .btn {
margin-left: 1em;
}
}

View File

@ -1,4 +1,5 @@
@import 'sluice/sluice_custom_style'; @import 'simple_prize/simple_prize_custom_style';
@import 'portfolioreview/portfolioreview_custom_style';
.ascribe-prize-app { .ascribe-prize-app {
border-radius: 0; border-radius: 0;

View File

@ -0,0 +1,100 @@
$pr--nav-fg-prim-color: black;
$pr--button-color: $pr--nav-fg-prim-color;
.client--portfolioreview {
padding-top: 0 !important;
.btn-wide,
.btn-default {
background-color: $pr--button-color;
border-color: $pr--button-color;
&:hover,
&:active,
&:focus,
&:active:hover,
&:active:focus,
&:active.focus,
&.active:hover,
&.active:focus,
&.active.focus {
background-color: lighten($pr--button-color, 20%);
border-color: lighten($pr--button-color, 20%);
}
}
.ascribe-property {
> p > span:not(> .span),
> textarea,
> input {
color: $pr--nav-fg-prim-color;
}
}
.ascribe-property-wrapper:hover {
border-left-color: lighten($pr--nav-fg-prim-color, 60%);
}
.is-focused {
border-left-color: $pr--nav-fg-prim-color !important;
background-color: lighten($pr--nav-fg-prim-color, 95%);
}
.register-piece--info {
text-align: center;
h1, h2 {
font-variant: small-caps;
}
h1 {
font-size: 5em;
color: #757575;
}
h2 {
font-size: 1.25em;
}
p {
margin-bottom: 0;
}
p + p {
margin-top: 0;
}
p:last-child {
margin-bottom: 1.5em;
}
}
.register-piece--form {
margin-top: 2em;
margin-bottom: 3em;
form {
border-top: none;
border-bottom: none;
}
}
.piece--hero {
text-align: center;
padding: 1em 0 1em 0;
margin-bottom: 3em;
border-bottom: 1px solid rgba(0, 0, 0, .1);
background-color: white;
h2 {
margin-top: 0;
}
}
.ascribe-property {
> p > span:not(> .span) {
text-transform: capitalize;
}
}
}

View File

@ -178,7 +178,7 @@ $ikono--font: 'Helvetica Neue', 'Helvetica', sans-serif !important;
border: none; border: none;
} }
.ascribe-property > span { .ascribe-property > p > span {
color: white; color: white;
} }