1
0
mirror of https://github.com/ascribe/onion.git synced 2024-12-23 01:39:36 +01:00

Merge pull request #87 from ascribe/AD-1362-implement-portfolio-review-judging-flow-stage-2

AD-1362 Implement portfolio review judging flow stage 2
This commit is contained in:
Brett Sun 2015-12-23 21:15:33 +01:00
commit f819e1313e
11 changed files with 240 additions and 137 deletions

View File

@ -12,7 +12,10 @@ import { getLangText } from '../../utils/lang_utils';
let AccordionListItemPiece = React.createClass({ let AccordionListItemPiece = React.createClass({
propTypes: { propTypes: {
className: React.PropTypes.string, className: React.PropTypes.string,
artistName: React.PropTypes.string, artistName: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.element
]),
piece: React.PropTypes.object.isRequired, piece: React.PropTypes.object.isRequired,
children: React.PropTypes.oneOfType([ children: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.element), React.PropTypes.arrayOf(React.PropTypes.element),

View File

@ -46,7 +46,13 @@ let ModalWrapper = React.createClass({
renderChildren() { renderChildren() {
return ReactAddons.Children.map(this.props.children, (child) => { return ReactAddons.Children.map(this.props.children, (child) => {
return ReactAddons.addons.cloneWithProps(child, { return ReactAddons.addons.cloneWithProps(child, {
handleSuccess: this.handleSuccess handleSuccess: (response) => {
if (typeof child.props.handleSuccess === 'function') {
child.props.handleSuccess(response);
}
this.handleSuccess(response);
}
}); });
}); });
}, },

View File

@ -12,6 +12,8 @@ import SPPieceContainer from './simple_prize/components/ascribe_detail/prize_pie
import SPSettingsContainer from './simple_prize/components/prize_settings_container'; import SPSettingsContainer from './simple_prize/components/prize_settings_container';
import SPApp from './simple_prize/prize_app'; import SPApp from './simple_prize/prize_app';
import SluicePieceContainer from './sluice/components/sluice_detail/sluice_piece_container';
import PRApp from './portfolioreview/pr_app'; import PRApp from './portfolioreview/pr_app';
import PRLanding from './portfolioreview/components/pr_landing'; import PRLanding from './portfolioreview/components/pr_landing';
import PRRegisterPiece from './portfolioreview/components/pr_register_piece'; import PRRegisterPiece from './portfolioreview/components/pr_register_piece';
@ -53,7 +55,7 @@ const ROUTES = {
path='collection' path='collection'
component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(SPPieceList)} component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(SPPieceList)}
headerTitle='COLLECTION'/> headerTitle='COLLECTION'/>
<Route path='pieces/:pieceId' component={SPPieceContainer} /> <Route path='pieces/:pieceId' component={SluicePieceContainer} />
<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} />

View File

@ -10,7 +10,8 @@ class PrizeRatingActions {
this.generateActions( this.generateActions(
'updatePrizeRatings', 'updatePrizeRatings',
'updatePrizeRatingAverage', 'updatePrizeRatingAverage',
'updatePrizeRating' 'updatePrizeRating',
'resetPrizeRatings'
); );
} }

View File

@ -171,23 +171,25 @@ let AccordionListItemPrize = React.createClass({
}, },
render() { render() {
const { children, className, content } = this.props;
const { currentUser } = this.state;
// Only show the artist name if you are the participant or if you are a judge and the piece is shortlisted // Only show the artist name if you are the participant or if you are a judge and the piece is shortlisted
let artistName = ((this.state.currentUser.is_jury && !this.state.currentUser.is_judge) || let artistName = ((currentUser.is_jury && !currentUser.is_judge) || (currentUser.is_judge && !content.selected )) ?
(this.state.currentUser.is_judge && !this.props.content.selected )) ? <span className="glyphicon glyphicon-eye-close" aria-hidden="true"/> : content.artist_name;
<span className="glyphicon glyphicon-eye-close" aria-hidden="true"/> : this.props.content.artist_name;
return ( return (
<div> <div>
<AccordionListItemPiece <AccordionListItemPiece
className={this.props.className} className={className}
piece={this.props.content} piece={content}
artistName={artistName} artistName={artistName}
subsubheading={ subsubheading={
<div> <div>
<span>{Moment(this.props.content.date_created, 'YYYY-MM-DD').year()}</span> <span>{Moment(content.date_created, 'YYYY-MM-DD').year()}</span>
</div>} </div>}
buttons={this.getPrizeButtons()} buttons={this.getPrizeButtons()}
badge={this.getPrizeBadge()}> badge={this.getPrizeBadge()}>
{this.props.children} {children}
</AccordionListItemPiece> </AccordionListItemPiece>
</div> </div>
); );

View File

@ -34,9 +34,7 @@ import CollapsibleParagraph from '../../../../../../components/ascribe_collapsib
import FurtherDetailsFileuploader from '../../../../../ascribe_detail/further_details_fileuploader'; import FurtherDetailsFileuploader from '../../../../../ascribe_detail/further_details_fileuploader';
import InputCheckbox from '../../../../../ascribe_forms/input_checkbox'; import InputCheckbox from '../../../../../ascribe_forms/input_checkbox';
import LoanForm from '../../../../../ascribe_forms/form_loan';
import ListRequestActions from '../../../../../ascribe_forms/list_form_request_actions'; import ListRequestActions from '../../../../../ascribe_forms/list_form_request_actions';
import ModalWrapper from '../../../../../ascribe_modal/modal_wrapper';
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';
@ -52,39 +50,41 @@ import { setDocumentTitle } from '../../../../../../utils/dom_utils';
/** /**
* This is the component that implements resource/data specific functionality * This is the component that implements resource/data specific functionality
*/ */
let PieceContainer = React.createClass({ let PrizePieceContainer = React.createClass({
propTypes: { propTypes: {
params: React.PropTypes.object params: React.PropTypes.object,
selectedPrizeActionButton: React.PropTypes.func
}, },
mixins: [ReactError], mixins: [ReactError],
getInitialState() { getInitialState() {
return mergeOptions( //FIXME: this component uses the PieceStore, but we avoid using the getState() here since it may contain stale data
PieceStore.getState(), // It should instead use something like getInitialState() where that call also resets the state.
UserStore.getState() return UserStore.getState();
); },
componentWillMount() {
// 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
// the piece detail a second time
PieceActions.updatePiece({});
}, },
componentDidMount() { componentDidMount() {
PieceStore.listen(this.onChange); PieceStore.listen(this.onChange);
UserStore.listen(this.onChange); UserStore.listen(this.onChange);
// 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
// the piece detail a second time
PieceActions.updatePiece({});
PieceActions.fetchOne(this.props.params.pieceId);
UserActions.fetchCurrentUser(); UserActions.fetchCurrentUser();
this.loadPiece();
}, },
// This is done to update the container when the user clicks on the prev or next // This is done to update the container when the user clicks on the prev or next
// button to update the URL parameter (and therefore to switch pieces) // button to update the URL parameter (and therefore to switch pieces)
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
if(this.props.params.pieceId !== nextProps.params.pieceId) { if (this.props.params.pieceId !== nextProps.params.pieceId) {
PieceActions.updatePiece({}); PieceActions.updatePiece({});
PieceActions.fetchOne(nextProps.params.pieceId); this.loadPiece(nextProps.params.pieceId);
} }
}, },
@ -101,26 +101,32 @@ let PieceContainer = React.createClass({
UserStore.unlisten(this.onChange); UserStore.unlisten(this.onChange);
}, },
onChange(state) { onChange(state) {
this.setState(state); this.setState(state);
}, },
getActions() { getActions() {
if (this.state.piece && const { currentUser, piece } = this.state;
this.state.piece.notifications &&
this.state.piece.notifications.length > 0) { if (piece && piece.notifications && piece.notifications.length > 0) {
return ( return (
<ListRequestActions <ListRequestActions
pieceOrEditions={this.state.piece} pieceOrEditions={piece}
currentUser={this.state.currentUser} currentUser={currentUser}
handleSuccess={this.loadPiece} handleSuccess={this.loadPiece}
notifications={this.state.piece.notifications}/>); notifications={piece.notifications}/>);
} }
}, },
loadPiece(pieceId = this.props.params.pieceId) {
PieceActions.fetchOne(pieceId);
},
render() { render() {
if(this.state.piece && this.state.piece.id) { const { selectedPrizeActionButton } = this.props;
const { currentUser, piece } = this.state;
if (piece && piece.id) {
/* /*
This really needs a refactor! This really needs a refactor!
@ -129,37 +135,32 @@ let PieceContainer = React.createClass({
*/ */
// Only show the artist name if you are the participant or if you are a judge and the piece is shortlisted // Only show the artist name if you are the participant or if you are a judge and the piece is shortlisted
let artistName = ((this.state.currentUser.is_jury && !this.state.currentUser.is_judge) || let artistName;
(this.state.currentUser.is_judge && !this.state.piece.selected )) ? if ((currentUser.is_jury && !currentUser.is_judge) || (currentUser.is_judge && !piece.selected )) {
null : this.state.piece.artist_name; artistName = <span className="glyphicon glyphicon-eye-close" aria-hidden="true"/>;
setDocumentTitle(piece.title);
} else {
artistName = piece.artist_name;
setDocumentTitle([artistName, piece.title].join(', '));
}
// Only show the artist email if you are a judge and the piece is shortlisted // Only show the artist email if you are a judge and the piece is shortlisted
let artistEmail = (this.state.currentUser.is_judge && this.state.piece.selected ) ? const artistEmail = (currentUser.is_judge && piece.selected ) ?
<DetailProperty label={getLangText('REGISTREE')} value={ this.state.piece.user_registered } /> : null; <DetailProperty label={getLangText('REGISTREE')} value={ piece.user_registered } /> : null;
if (artistName === null) {
setDocumentTitle(this.state.piece.title);
} else {
setDocumentTitle([artistName, this.state.piece.title].join(', '));
}
if (artistName === null) {
artistName = <span className="glyphicon glyphicon-eye-close" aria-hidden="true"/>;
}
return ( return (
<Piece <Piece
piece={this.state.piece} piece={piece}
loadPiece={this.loadPiece} currentUser={currentUser}
header={ header={
<div className="ascribe-detail-header"> <div className="ascribe-detail-header">
<NavigationHeader <NavigationHeader
piece={this.state.piece} piece={piece}
currentUser={this.state.currentUser}/> currentUser={currentUser}/>
<h1 className="ascribe-detail-title">{this.state.piece.title}</h1> <h1 className="ascribe-detail-title">{piece.title}</h1>
<DetailProperty label={getLangText('BY')} value={artistName} /> <DetailProperty label={getLangText('BY')} value={artistName} />
<DetailProperty label={getLangText('DATE')} value={Moment(this.state.piece.date_created, 'YYYY-MM-DD').year()} /> <DetailProperty label={getLangText('DATE')} value={Moment(piece.date_created, 'YYYY-MM-DD').year()} />
{artistEmail} {artistEmail}
{this.getActions()} {this.getActions()}
<hr/> <hr/>
@ -168,10 +169,11 @@ let PieceContainer = React.createClass({
subheader={ subheader={
<PrizePieceRatings <PrizePieceRatings
loadPiece={this.loadPiece} loadPiece={this.loadPiece}
piece={this.state.piece} piece={piece}
currentUser={this.state.currentUser}/> currentUser={currentUser}
selectedPrizeActionButton={selectedPrizeActionButton} />
}> }>
<PrizePieceDetails piece={this.state.piece} /> <PrizePieceDetails piece={piece} />
</Piece> </Piece>
); );
} else { } else {
@ -225,29 +227,26 @@ let PrizePieceRatings = React.createClass({
propTypes: { propTypes: {
loadPiece: React.PropTypes.func, loadPiece: React.PropTypes.func,
piece: React.PropTypes.object, piece: React.PropTypes.object,
currentUser: React.PropTypes.object currentUser: React.PropTypes.object,
selectedPrizeActionButton: React.PropTypes.func
}, },
getInitialState() { getInitialState() {
return mergeOptions( return mergeOptions(
PieceListStore.getState(), PieceListStore.getState(),
PrizeRatingStore.getState() PrizeRatingStore.getInitialState()
); );
}, },
componentDidMount() { componentDidMount() {
PrizeRatingStore.listen(this.onChange); PrizeRatingStore.listen(this.onChange);
PieceListStore.listen(this.onChange);
PrizeRatingActions.fetchOne(this.props.piece.id); PrizeRatingActions.fetchOne(this.props.piece.id);
PrizeRatingActions.fetchAverage(this.props.piece.id); PrizeRatingActions.fetchAverage(this.props.piece.id);
PieceListStore.listen(this.onChange);
}, },
componentWillUnmount() { componentWillUnmount() {
// Every time we're leaving the piece detail page,
// just reset the piece that is saved in the piece store
// as it will otherwise display wrong/old data once the user loads
// the piece detail a second time
PrizeRatingActions.updateRating({});
PrizeRatingStore.unlisten(this.onChange); PrizeRatingStore.unlisten(this.onChange);
PieceListStore.unlisten(this.onChange); PieceListStore.unlisten(this.onChange);
}, },
@ -270,48 +269,23 @@ let PrizePieceRatings = React.createClass({
onRatingClick(event, args) { onRatingClick(event, args) {
event.preventDefault(); event.preventDefault();
PrizeRatingActions.createRating(this.props.piece.id, args.rating).then( PrizeRatingActions
this.refreshPieceData() .createRating(this.props.piece.id, args.rating)
); .then(this.refreshPieceData);
}, },
handleLoanRequestSuccess(message){ getSelectedActionButton() {
let notification = new GlobalNotificationModel(message, 'success', 4000); const { currentUser, piece, selectedPrizeActionButton: SelectedPrizeActionButton } = this.props;
GlobalNotificationActions.appendGlobalNotification(notification);
},
getLoanButton(){ if (piece.selected && SelectedPrizeActionButton) {
let today = new Moment();
let endDate = new Moment();
endDate.add(6, 'months');
return ( return (
<ModalWrapper <span className="pull-right">
trigger={ <SelectedPrizeActionButton
<button className='btn btn-default btn-sm'> piece={piece}
{getLangText('SEND LOAN REQUEST')} currentUser={currentUser} />
</button> </span>
);
} }
handleSuccess={this.handleLoanRequestSuccess}
title='REQUEST LOAN'>
<LoanForm
loanHeading={null}
message={getLangText('Congratulations,\nYou have been selected for the prize.\n' +
'Please accept the loan request to proceed.')}
id={{piece_id: this.props.piece.id}}
url={ApiUrls.ownership_loans_pieces_request}
email={this.props.currentUser.email}
gallery={this.props.piece.prize.name}
startDate={today}
endDate={endDate}
showPersonalMessage={true}
showPassword={false}
handleSuccess={this.handleLoanSuccess} />
</ModalWrapper>);
},
handleShortlistSuccess(message){
let notification = new GlobalNotificationModel(message, 'success', 2000);
GlobalNotificationActions.appendGlobalNotification(notification);
}, },
refreshPieceData() { refreshPieceData() {
@ -321,20 +295,19 @@ let PrizePieceRatings = React.createClass({
}, },
onSelectChange() { onSelectChange() {
PrizeRatingActions.toggleShortlist(this.props.piece.id) PrizeRatingActions
.then( .toggleShortlist(this.props.piece.id)
(res) => { .then((res) => {
this.refreshPieceData(); this.refreshPieceData();
return res;
}) if (res && res.notification) {
.then( const notification = new GlobalNotificationModel(res.notification, 'success', 2000);
(res) => { GlobalNotificationActions.appendGlobalNotification(notification);
this.handleShortlistSuccess(res.notification);
} }
); });
}, },
render(){ render() {
if (this.props.piece && this.props.currentUser && this.props.currentUser.is_judge && this.state.average) { if (this.props.piece && this.props.currentUser && this.props.currentUser.is_judge && this.state.average) {
// Judge sees shortlisting, average and per-jury notes // Judge sees shortlisting, average and per-jury notes
return ( return (
@ -352,9 +325,7 @@ let PrizePieceRatings = React.createClass({
</span> </span>
</InputCheckbox> </InputCheckbox>
</span> </span>
<span className="pull-right"> {this.getSelectedActionButton()}
{this.props.piece.selected ? this.getLoanButton() : null}
</span>
</div> </div>
<hr /> <hr />
</CollapsibleParagraph> </CollapsibleParagraph>
@ -373,13 +344,19 @@ let PrizePieceRatings = React.createClass({
</div> </div>
<hr /> <hr />
{this.state.ratings.map((item, i) => { {this.state.ratings.map((item, i) => {
let note = item.note ? let note = item.note ? (
<div className="rating-note"> <div className="rating-note">
note: {item.note} note: {item.note}
</div> : null; </div>
) : null;
return ( return (
<div className="rating-list"> <div
<div id="list-rating" className="row no-margin"> key={item.user}
className="rating-list">
<div
id="list-rating"
className="row no-margin">
<span className="pull-right"> <span className="pull-right">
<StarRating <StarRating
ref={'rating' + i} ref={'rating' + i}
@ -399,8 +376,7 @@ let PrizePieceRatings = React.createClass({
<hr /> <hr />
</CollapsibleParagraph> </CollapsibleParagraph>
</div>); </div>);
} } else if (this.props.currentUser && this.props.currentUser.is_jury) {
else if (this.props.currentUser && this.props.currentUser.is_jury) {
// Jury can set rating and note // Jury can set rating and note
return ( return (
<CollapsibleParagraph <CollapsibleParagraph
@ -427,9 +403,10 @@ let PrizePieceRatings = React.createClass({
url={ApiUrls.notes} url={ApiUrls.notes}
currentUser={this.props.currentUser}/> currentUser={this.props.currentUser}/>
</CollapsibleParagraph>); </CollapsibleParagraph>);
} } else {
return null; return null;
} }
}
}); });
@ -457,6 +434,7 @@ let PrizePieceDetails = React.createClass({
return ( return (
<Property <Property
key={label}
name={data} name={data}
label={label} label={label}
editable={false} editable={false}
@ -484,4 +462,4 @@ let PrizePieceDetails = React.createClass({
} }
}); });
export default PieceContainer; export default PrizePieceContainer;

View File

@ -48,9 +48,8 @@ let PrizePieceList = React.createClass({
}, },
getButtonSubmit() { getButtonSubmit() {
const { currentUser } = this.state; const { currentUser, prize } = this.state;
if (this.state.prize && this.state.prize.active && if (prize && prize.active && !currentUser.is_jury && !currentUser.is_admin && !currentUser.is_judge) {
!currentUser.is_jury && !currentUser.is_admin && !currentUser.is_judge){
return ( return (
<LinkContainer to="/register_piece"> <LinkContainer to="/register_piece">
<Button> <Button>

View File

@ -135,14 +135,15 @@ let PrizeJurySettings = React.createClass({
handleCreateSuccess(response) { handleCreateSuccess(response) {
PrizeJuryActions.fetchJury(); PrizeJuryActions.fetchJury();
let notification = new GlobalNotificationModel(response.notification, 'success', 5000); this.displayNotification(response);
GlobalNotificationActions.appendGlobalNotification(notification);
this.refs.form.refs.email.refs.input.getDOMNode().value = null; this.refs.form.refs.email.refs.input.getDOMNode().value = null;
}, },
handleActivate(event) { handleActivate(event) {
let email = event.target.getAttribute('data-id'); let email = event.target.getAttribute('data-id');
PrizeJuryActions.activateJury(email).then((response) => { PrizeJuryActions
.activateJury(email)
.then((response) => {
PrizeJuryActions.fetchJury(); PrizeJuryActions.fetchJury();
this.displayNotification(response); this.displayNotification(response);
}); });

View File

@ -6,10 +6,12 @@ import PrizeRatingActions from '../actions/prize_rating_actions';
class PrizeRatingStore { class PrizeRatingStore {
constructor() { constructor() {
this.ratings = []; this.getInitialState();
this.currentRating = null;
this.average = null;
this.bindActions(PrizeRatingActions); this.bindActions(PrizeRatingActions);
this.exportPublicMethods({
getInitialState: this.getInitialState.bind(this)
});
} }
onUpdatePrizeRatings(ratings) { onUpdatePrizeRatings(ratings) {
@ -24,6 +26,22 @@ class PrizeRatingStore {
this.average = data.average; this.average = data.average;
this.ratings = data.ratings; this.ratings = data.ratings;
} }
onResetPrizeRatings() {
this.getInitialState();
}
getInitialState() {
this.ratings = [];
this.currentRating = null;
this.average = null;
return {
ratings: this.ratings,
currentRating: this.currentRating,
average: this.average
};
}
} }
export default alt.createStore(PrizeRatingStore, 'PrizeRatingStore'); export default alt.createStore(PrizeRatingStore, 'PrizeRatingStore');

View File

@ -0,0 +1,70 @@
'use strict'
import React from 'react';
import Moment from 'moment';
import ModalWrapper from '../../../../../ascribe_modal/modal_wrapper';
import LoanForm from '../../../../../ascribe_forms/form_loan';
import GlobalNotificationModel from '../../../../../../models/global_notification_model';
import GlobalNotificationActions from '../../../../../../actions/global_notification_actions';
import ApiUrls from '../../../../../../constants/api_urls';
import { getLangText } from '../../../../../../utils/lang_utils';
const SluiceSelectedPrizeActionButton = React.createClass({
propTypes: {
piece: React.PropTypes.object,
currentUser: React.PropTypes.object,
startLoanDate: React.PropTypes.object,
endLoanDate: React.PropTypes.object,
className: React.PropTypes.string,
handleSuccess: React.PropTypes.func
},
handleSuccess(res) {
const notification = new GlobalNotificationModel(res && res.notification || getLangText('You have successfully requested the loan, pending their confirmation.'), 'success', 4000);
GlobalNotificationActions.appendGlobalNotification(notification);
if (typeof this.props.handleSuccess === 'function') {
this.props.handleSuccess(res);
}
},
render() {
const { currentUser, piece } = this.props;
// Can't use default props since those are only created once
const startLoanDate = this.props.startLoanDate || new Moment();
const endLoanDate = this.props.endLoanDate || (new Moment()).add(6, 'months');
return (
<ModalWrapper
trigger={
<button className='btn btn-default btn-sm'>
{getLangText('SEND LOAN REQUEST')}
</button>
}
handleSuccess={this.handleSuccess}
title={getLangText('REQUEST LOAN')}>
<LoanForm
loanHeading={null}
message={getLangText('Congratulations,\nYou have been selected for the prize.\n' +
'Please accept the loan request to proceed.')}
id={{ piece_id: piece.id }}
url={ApiUrls.ownership_loans_pieces_request}
email={currentUser.email}
gallery={piece.prize.name}
startDate={startLoanDate}
endDate={endLoanDate}
showPersonalMessage={true}
showPassword={false} />
</ModalWrapper>
);
}
});
export default SluiceSelectedPrizeActionButton;

View File

@ -0,0 +1,23 @@
'use strict';
import React from 'react';
import SluiceSelectedPrizeActionButton from '../sluice_buttons/sluice_selected_prize_action_button';
import PrizePieceContainer from '../../../simple_prize/components/ascribe_detail/prize_piece_container';
const SluicePieceContainer = React.createClass({
propTypes: {
params: React.PropTypes.object
},
render() {
return (
<PrizePieceContainer
{...this.props}
selectedPrizeActionButton={SluiceSelectedPrizeActionButton} />
);
}
});
export default SluicePieceContainer;