Merged in AD-661-due-1208-implement-jury-rating-an (pull request #40)

Ad 661 due 1208 implement jury rating an
This commit is contained in:
diminator 2015-08-27 13:48:16 +02:00
commit b229dbb79f
32 changed files with 653 additions and 150 deletions

View File

@ -0,0 +1,38 @@
'use strict';
import alt from '../alt';
import OwnershipFetcher from '../fetchers/ownership_fetcher';
class OwnershipActions {
constructor() {
this.generateActions(
'updateLoanPieceRequestList',
'updateLoanPieceRequest',
'flushLoanPieceRequest'
);
}
fetchLoanRequestList() {
OwnershipFetcher.fetchLoanPieceRequestList()
.then((data) => {
this.actions.updateLoanPieceRequestList(data.loan_requests);
})
.catch((err) => {
console.logGlobal(err);
this.actions.updateLoanPieceRequestList(null);
});
}
fetchLoanRequest(pieceId) {
OwnershipFetcher.fetchLoanPieceRequestList()
.then((data) => {
this.actions.updateLoanPieceRequest({loanRequests: data.loan_requests, pieceId: pieceId});
})
.catch((err) => {
console.logGlobal(err);
});
}
}
export default alt.createActions(OwnershipActions);

View File

@ -39,7 +39,7 @@ let AccordionListItem = React.createClass({
</div>
<span style={{'clear': 'both'}}></span>
<div className="request-action-batch">
<div className="request-action-badge">
{this.props.badge}
</div>
</div>

View File

@ -61,13 +61,13 @@ let AccordionListItemWallet = React.createClass({
},
getGlyphicon(){
if (this.props.content.requestAction) {
if (this.props.content.requestAction && this.props.content.requestAction.length > 0) {
return (
<OverlayTrigger
delay={500}
placement="left"
overlay={<Tooltip>{getLangText('You have actions pending in one of your editions')}</Tooltip>}>
<Glyphicon glyph='bell'/>
<Glyphicon glyph='bell' color="green"/>
</OverlayTrigger>);
}
return null;

View File

@ -6,6 +6,7 @@ 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';
@ -27,6 +28,8 @@ let AclButton = React.createClass({
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
},
@ -89,6 +92,18 @@ let AclButton = React.createClass({
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'),
@ -140,17 +155,20 @@ let AclButton = React.createClass({
// 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() {
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 ' : 'hidden'}>
<button className={shouldDisplay ? 'btn btn-default btn-sm ' + buttonClassName : 'hidden'}>
{this.sanitizeAction()}
</button>
}

View File

@ -28,7 +28,7 @@ import EditionDetailProperty from './detail_property';
import EditionFurtherDetails from './further_details';
import RequestActionForm from './../ascribe_forms/form_request_action';
import ListRequestActions from './../ascribe_forms/list_form_request_actions';
import AclButtonList from './../ascribe_buttons/acl_button_list';
import UnConsignRequestButton from './../ascribe_buttons/unconsign_request_button';
import DeleteButton from '../ascribe_buttons/delete_button';
@ -101,10 +101,6 @@ let Edition = React.createClass({
this.transitionTo('pieces');
},
getId() {
return {'bitcoin_id': this.props.edition.bitcoin_id};
},
render() {
return (
<Row>
@ -159,7 +155,7 @@ let Edition = React.createClass({
show={(this.state.currentUser.username && true || false) ||
(this.props.edition.acl.acl_edit || this.props.edition.public_note)}>
<Note
id={this.getId}
id={() => {return {'bitcoin_id': this.props.edition.bitcoin_id}; }}
label={getLangText('Personal note (private)')}
defaultValue={this.props.edition.private_note ? this.props.edition.private_note : null}
placeholder={getLangText('Enter your comments ...')}
@ -168,7 +164,7 @@ let Edition = React.createClass({
url={ApiUrls.note_private_edition}
currentUser={this.state.currentUser}/>
<Note
id={this.getId}
id={() => {return {'bitcoin_id': this.props.edition.bitcoin_id}; }}
label={getLangText('Edition note (public)')}
defaultValue={this.props.edition.public_note ? this.props.edition.public_note : null}
placeholder={getLangText('Enter your comments ...')}
@ -240,12 +236,11 @@ let EditionSummary = React.createClass({
let actions = null;
if (this.props.edition.request_action && this.props.edition.request_action.length > 0){
actions = (
<RequestActionForm
<ListRequestActions
pieceOrEditions={[this.props.edition]}
currentUser={this.props.currentUser}
pieceOrEditions={ [this.props.edition] }
requestAction={this.props.edition.request_action}
requestUser={this.props.edition.owner}
handleSuccess={this.showNotification}/>);
handleSuccess={this.showNotification}
requestActions={this.props.edition.request_action}/>);
}
else {

View File

@ -26,7 +26,7 @@ import CreateEditionsForm from '../ascribe_forms/create_editions_form';
import CreateEditionsButton from '../ascribe_buttons/create_editions_button';
import DeleteButton from '../ascribe_buttons/delete_button';
import RequestActionForm from '../ascribe_forms/form_request_action';
import ListRequestActions from '../ascribe_forms/list_form_request_actions';
import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions';
@ -175,12 +175,12 @@ let PieceContainer = React.createClass({
this.state.piece.request_action &&
this.state.piece.request_action.length > 0) {
return (
<RequestActionForm
<ListRequestActions
pieceOrEditions={this.state.piece}
currentUser={this.state.currentUser}
pieceOrEditions={ this.state.piece }
requestAction={this.state.piece.request_action}
requestUser={this.state.piece.user_registered}
handleSuccess={this.loadPiece}/>);
handleSuccess={this.loadPiece}
requestActions={this.state.piece.request_action}/>
);
}
else {
return (

View File

@ -28,6 +28,9 @@ let LoanForm = React.createClass({
startdate: React.PropTypes.object,
enddate: React.PropTypes.object,
showPersonalMessage: React.PropTypes.bool,
showEndDate: React.PropTypes.bool,
showStartDate: React.PropTypes.bool,
showPassword: React.PropTypes.bool,
url: React.PropTypes.string,
id: React.PropTypes.object,
message: React.PropTypes.string,
@ -37,7 +40,10 @@ let LoanForm = React.createClass({
getDefaultProps() {
return {
loanHeading: '',
showPersonalMessage: true
showPersonalMessage: true,
showEndDate: true,
showStartDate: true,
showPassword: true
};
},
@ -47,7 +53,7 @@ let LoanForm = React.createClass({
componentDidMount() {
LoanContractStore.listen(this.onChange);
LoanContractActions.flushLoanContract();
LoanContractActions.flushLoanContract.defer();
},
componentWillUnmount() {
@ -155,7 +161,7 @@ let LoanForm = React.createClass({
required/>
</Property>
<Property
name='gallery_name'
name='gallery'
label={getLangText('Gallery/exhibition (optional)')}
editable={!this.props.gallery}
overrideForm={!!this.props.gallery}>
@ -167,15 +173,19 @@ let LoanForm = React.createClass({
<Property
name='startdate'
label={getLangText('Start date')}
hidden={this.props.startdate}>
editable={!this.props.startdate}
overrideForm={!!this.props.startdate}
hidden={!this.props.showStartDate}>
<InputDate
defaultValue={this.props.startdate}
placeholderText={getLangText('Loan start date')} />
</Property>
<Property
name='enddate'
editable={!this.props.enddate}
overrideForm={!!this.props.enddate}
label={getLangText('End date')}
hidden={this.props.enddate}>
hidden={!this.props.showEndDate}>
<InputDate
defaultValue={this.props.enddate}
placeholderText={getLangText('Loan end date')} />
@ -190,15 +200,16 @@ let LoanForm = React.createClass({
editable={true}
defaultValue={this.props.message}
placeholder={getLangText('Enter a message...')}
required="required"/>
required={this.props.showPersonalMessage ? 'required' : ''}/>
</Property>
<Property
name='password'
label={getLangText('Password')}>
label={getLangText('Password')}
hidden={!this.props.showPassword}>
<input
type="password"
placeholder={getLangText('Enter your password')}
required/>
required={this.props.showPassword ? 'required' : ''}/>
</Property>
{this.getContractCheckbox()}
{this.props.children}

View File

@ -0,0 +1,79 @@
'use strict';
import React from 'react';
import Moment from 'moment';
import LoanForm from './form_loan';
import OwnershipActions from '../../actions/ownership_actions';
import OwnershipStore from '../../stores/ownership_store';
import AppConstants from '../../constants/application_constants';
import { getLangText } from '../../utils/lang_utils';
let LoanRequestAnswerForm = React.createClass({
propTypes: {
url: React.PropTypes.string,
id: React.PropTypes.object,
message: React.PropTypes.string,
handleSuccess: React.PropTypes.func.required
},
getDefaultProps() {
return {
loanHeading: '',
showPersonalMessage: true,
showEndDate: false,
showStartDate: false,
showPassword: true
};
},
getInitialState() {
return OwnershipStore.getState();
},
componentDidMount() {
OwnershipStore.listen(this.onChange);
OwnershipActions.fetchLoanRequest(this.props.id.piece_id);
},
componentWillUnmount() {
OwnershipStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
},
render() {
let startDate = null;
let endDate = null;
if (this.state.loanRequest) {
startDate = new Moment(this.state.loanRequest.datetime_from, Moment.ISO_8601);
endDate = new Moment(this.state.loanRequest.datetime_to, Moment.ISO_8601);
return (
<LoanForm
loanHeading={null}
message={''}
id={this.props.id}
url={this.props.url}
email={this.state.loanRequest ? this.state.loanRequest.new_owner : null}
gallery={this.state.loanRequest ? this.state.loanRequest.gallery : null}
startdate={startDate}
enddate={endDate}
showPassword={true}
showPersonalMessage={false}
handleSuccess={this.props.handleSuccess}/>
);
}
return <span/>;
}
});
export default LoanRequestAnswerForm;

View File

@ -45,6 +45,9 @@ let RequestActionForm = React.createClass({
} else if (this.props.requestAction === 'loan' && this.isPiece()){
urls.accept = ApiUrls.ownership_loans_pieces_confirm;
urls.deny = ApiUrls.ownership_loans_pieces_deny;
} else if (this.props.requestAction === 'loan_request' && this.isPiece()){
urls.accept = ApiUrls.ownership_loans_pieces_request_confirm;
urls.deny = ApiUrls.ownership_loans_pieces_request_deny;
}
return urls;
@ -75,8 +78,11 @@ let RequestActionForm = React.createClass({
},
getContent() {
let message = this.props.requestUser + ' ' + getLangText('requests you') + ' ' + this.props.requestAction + ' ' + getLangText('this edition%s', '.');
let pieceOrEditionStr = this.isPiece() ? getLangText('this work%s', '.') : getLangText('this edition%s', '.');
let message = this.props.requestUser + ' ' + getLangText('requests you') + ' ' + this.props.requestAction + ' ' + pieceOrEditionStr;
if (this.props.requestAction === 'loan_request'){
message = this.props.requestUser + ' ' + getLangText('requests you to loan') + ' ' + pieceOrEditionStr;
}
return (
<span>
{message}
@ -90,6 +96,18 @@ let RequestActionForm = React.createClass({
<AclButton
availableAcls={{'acl_unconsign': true}}
action="acl_unconsign"
buttonAcceptClassName='inline pull-right btn-sm ascribe-margin-1px'
pieceOrEditions={this.props.pieceOrEditions}
currentUser={this.props.currentUser}
handleSuccess={this.props.handleSuccess} />
);
} else if(this.props.requestAction === 'loan_request') {
return (
<AclButton
availableAcls={{'acl_loan_request': true}}
action="acl_loan_request"
buttonAcceptName="LOAN"
buttonAcceptClassName='inline pull-right btn-sm ascribe-margin-1px'
pieceOrEditions={this.props.pieceOrEditions}
currentUser={this.props.currentUser}
handleSuccess={this.props.handleSuccess} />

View File

@ -9,7 +9,11 @@ let InputDate = React.createClass({
submitted: React.PropTypes.bool,
placeholderText: React.PropTypes.string,
onChange: React.PropTypes.func,
defaultValue: React.PropTypes.object
defaultValue: React.PropTypes.object,
// DatePicker implements the disabled attribute
// https://github.com/Hacker0x01/react-datepicker/blob/master/src/datepicker.jsx#L30
disabled: React.PropTypes.bool
},
getInitialState() {
@ -41,10 +45,11 @@ let InputDate = React.createClass({
});
},
render: function () {
render() {
return (
<div>
<DatePicker
disabled={this.props.disabled}
dateFormat="YYYY-MM-DD"
selected={this.state.value_moment}
onChange={this.handleChange}

View File

@ -0,0 +1,37 @@
'use strict';
import React from 'react';
import RequestActionForm from './form_request_action';
let ListRequestActions = React.createClass({
propTypes: {
pieceOrEditions: React.PropTypes.oneOfType([
React.PropTypes.object,
React.PropTypes.array
]).isRequired,
currentUser: React.PropTypes.object.isRequired,
handleSuccess: React.PropTypes.func.isRequired,
requestActions: React.PropTypes.array.isRequired
},
render () {
if (this.props.requestActions &&
this.props.requestActions.length > 0) {
return (
<div>
{this.props.requestActions.map((requestAction) =>
<RequestActionForm
currentUser={this.props.currentUser}
pieceOrEditions={ this.props.pieceOrEditions }
requestAction={requestAction.action}
requestUser={requestAction.by}
handleSuccess={this.props.handleSuccess}/>)}
</div>
);
}
return null;
}
});
export default ListRequestActions;

View File

@ -11,10 +11,10 @@ let TableItemAclFiltered = React.createClass({
render() {
var availableAcls = ['acl_consign', 'acl_loan', 'acl_transfer', 'acl_view', 'acl_share', 'acl_unshare', 'acl_delete'];
if (this.props.requestAction){
if (this.props.requestAction && this.props.requestAction.length > 0){
return (
<span>
{this.props.requestAction + ' request pending'}
{this.props.requestAction[0].action + ' request pending'}
</span>
);
}

View File

@ -28,7 +28,8 @@ let PieceList = React.createClass({
redirectTo: React.PropTypes.string,
customSubmitButton: React.PropTypes.element,
filterParams: React.PropTypes.array,
orderParams: React.PropTypes.array
orderParams: React.PropTypes.array,
orderBy: React.PropTypes.string
},
@ -59,10 +60,10 @@ let PieceList = React.createClass({
PieceListStore.listen(this.onChange);
EditionListStore.listen(this.onChange);
let orderBy = this.props.orderBy ? this.props.orderBy : this.state.orderBy;
if (this.state.pieceList.length === 0 || this.state.page !== page){
PieceListActions.fetchPieceList(page, this.state.pageSize, this.state.search,
this.state.orderBy, this.state.orderAsc, this.state.filterBy)
orderBy, this.state.orderAsc, this.state.filterBy)
.then(() => PieceListActions.fetchPieceRequestActions());
}
},

View File

@ -9,16 +9,17 @@ class PrizeRatingActions {
constructor() {
this.generateActions(
'updatePrizeRatings',
'updatePrizeRatingAverage',
'updatePrizeRating'
);
}
fetch() {
fetchAverage(pieceId) {
return Q.Promise((resolve, reject) => {
PrizeRatingFetcher
.fetch()
.fetchAverage(pieceId)
.then((res) => {
this.actions.updatePrizeRatings(res.ratings);
this.actions.updatePrizeRatingAverage(res.data);
resolve(res);
})
.catch((err) => {
@ -56,6 +57,20 @@ class PrizeRatingActions {
});
}
toggleShortlist(pieceId) {
return Q.Promise((resolve, reject) => {
PrizeRatingFetcher
.select(pieceId)
.then((res) => {
this.actions.updatePrizeRatings(res.data.ratings);
resolve(res);
})
.catch((err) => {
reject(err);
});
});
}
updateRating(rating) {
this.actions.updatePrizeRating(rating);
}

View File

@ -4,13 +4,17 @@ import React from 'react';
import Router from 'react-router';
import StarRating from 'react-star-rating';
import AccordionListItemPiece from '../../../../ascribe_accordion_list/accordion_list_item_piece';
import PieceListActions from '../../../../../actions/piece_list_actions';
import PieceListStore from '../../../../../stores/piece_list_store';
import PrizeRatingActions from '../../actions/prize_rating_actions';
import UserStore from '../../../../../stores/user_store';
import InputCheckbox from '../../../../ascribe_forms/input_checkbox';
import AccordionListItemPiece from '../../../../ascribe_accordion_list/accordion_list_item_piece';
import GlobalNotificationModel from '../../../../../models/global_notification_model';
import GlobalNotificationActions from '../../../../../actions/global_notification_actions';
@ -65,17 +69,28 @@ let AccordionListItemPrize = React.createClass({
getPrizeButtons() {
if (this.state.currentUser && this.state.currentUser.is_jury){
if (this.props.content.ratings && this.props.content.ratings.rating){
if ((this.props.content.ratings) &&
(this.props.content.ratings.rating || this.props.content.ratings.average)){
// jury and rating available
let rating = parseInt(this.props.content.ratings.rating, 10);
let rating = null,
caption = null;
if (this.props.content.ratings.rating){
rating = parseInt(this.props.content.ratings.rating, 10);
caption = getLangText('Your rating');
}
else if (this.props.content.ratings.average){
rating = this.props.content.ratings.average;
caption = getLangText('Average of ' + this.props.content.ratings.num_ratings + ' rating(s)');
}
return (
<div id="list-rating" className="pull-right">
<Link to='piece' params={{pieceId: this.props.content.id}}>
<StarRating
ref='rating'
name="prize-rating"
caption="Your rating"
step={1}
caption={caption}
step={0.5}
size='sm'
rating={rating}
ratingAmount={5} />
@ -83,17 +98,28 @@ let AccordionListItemPrize = React.createClass({
</div>);
}
else {
if (this.state.currentUser.is_judge){
return (
<div className="react-rating-caption pull-right">
{getLangText('Not rated')}
</div>
);
}
// jury and no rating yet
return (
<div className="react-rating-caption pull-right">
<Link to='piece' params={{pieceId: this.props.content.id}}>
Submit your rating
{getLangText('Submit your rating')}
</Link>
</div>
);
}
}
// participant
return this.getPrizeButtonsParticipant();
},
getPrizeButtonsParticipant() {
return (
<div>
<AclProxy
@ -108,22 +134,64 @@ let AccordionListItemPrize = React.createClass({
);
},
handleShortlistSuccess(message){
let notification = new GlobalNotificationModel(message, 'success', 2000);
GlobalNotificationActions.appendGlobalNotification(notification);
},
refreshPieceData() {
PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search,
this.state.orderBy, this.state.orderAsc, this.state.filterBy);
},
onSelectChange(){
PrizeRatingActions.toggleShortlist(this.props.content.id)
.then(
(res) => {
this.refreshPieceData();
return res;
})
.then(
(res) => {
this.handleShortlistSuccess(res.notification);
}
);
},
getPrizeBadge(){
if (this.state.currentUser && this.state.currentUser.is_judge) {
return (
<span className="pull-right ascribe-checkbox-wrapper ascribe-checkbox-badge">
<InputCheckbox
defaultChecked={this.props.content.selected}
onChange={this.onSelectChange}/>
</span>
);
}
return null;
},
render() {
let artistName = this.state.currentUser.is_jury ?
<span className="glyphicon glyphicon-eye-close" style={{fontSize: '0.75em'}} aria-hidden="true"/> :
this.props.content.artist_name;
// 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) ||
(this.state.currentUser.is_judge && !this.props.content.selected )) ?
<span className="glyphicon glyphicon-eye-close" aria-hidden="true"/> : this.props.content.artist_name;
return (
<AccordionListItemPiece
className={this.props.className}
piece={this.props.content}
artistName={artistName}
subsubheading={
<div className="pull-left">
<span>{this.props.content.date_created.split('-')[0]}</span>
</div>}
buttons={this.getPrizeButtons()}>
{this.props.children}
</AccordionListItemPiece>
<div>
<AccordionListItemPiece
className={this.props.className}
piece={this.props.content}
artistName={artistName}
subsubheading={
<div className="pull-left">
<span>{this.props.content.date_created.split('-')[0]}</span>
</div>}
buttons={this.getPrizeButtons()}
badge={this.getPrizeBadge()}>
{this.props.children}
</AccordionListItemPiece>
</div>
);
}
});

View File

@ -2,6 +2,7 @@
import React from 'react';
import Router from 'react-router';
import Moment from 'moment';
import StarRating from 'react-star-rating';
@ -17,6 +18,7 @@ import PrizeRatingStore from '../../stores/prize_rating_store';
import UserStore from '../../../../../stores/user_store';
import Piece from '../../../../../components/ascribe_detail/piece';
import Note from '../../../../../components/ascribe_detail/note';
import AppConstants from '../../../../../constants/application_constants';
@ -25,6 +27,11 @@ import Property from '../../../../../components/ascribe_forms/property';
import InputTextAreaToggable from '../../../../../components/ascribe_forms/input_textarea_toggable';
import CollapsibleParagraph from '../../../../../components/ascribe_collapsible/collapsible_paragraph';
import InputCheckbox from '../../../../ascribe_forms/input_checkbox';
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 GlobalNotificationActions from '../../../../../actions/global_notification_actions';
@ -53,6 +60,15 @@ let PieceContainer = React.createClass({
UserStore.listen(this.onChange);
},
// 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)
componentWillReceiveProps(nextProps) {
if(this.props.params.pieceId !== nextProps.params.pieceId) {
PieceActions.updatePiece({});
PieceActions.fetchOne(nextProps.params.pieceId);
}
},
componentWillUnmount() {
// Every time we're leaving the piece detail page,
// just reset the piece that is saved in the piece store
@ -63,14 +79,6 @@ let PieceContainer = React.createClass({
UserStore.unlisten(this.onChange);
},
// 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)
componentWillReceiveProps(nextProps) {
if(this.props.params.pieceId !== nextProps.params.pieceId) {
PieceActions.updatePiece({});
PieceActions.fetchOne(nextProps.params.pieceId);
}
},
onChange(state) {
this.setState(state);
@ -78,12 +86,18 @@ let PieceContainer = React.createClass({
loadPiece() {
PieceActions.fetchOne(this.props.params.pieceId);
this.setState(this.state);
},
render() {
if('title' in this.state.piece) {
let artistName = this.state.currentUser.is_jury ?
// 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) ||
(this.state.currentUser.is_judge && !this.state.piece.selected )) ?
<span className="glyphicon glyphicon-eye-close" aria-hidden="true"/> : this.state.piece.artist_name;
// 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 ) ?
<DetailProperty label={getLangText('REGISTREE')} value={ this.state.piece.user_registered } /> : null;
return (
<Piece
piece={this.state.piece}
@ -95,13 +109,20 @@ let PieceContainer = React.createClass({
currentUser={this.state.currentUser}/>
<hr/>
<h1 className="ascribe-detail-title">{this.state.piece.title}</h1>
<DetailProperty label="BY" value={artistName} />
<DetailProperty label="DATE" value={ this.state.piece.date_created.slice(0, 4) } />
<DetailProperty label={getLangText('BY')} value={artistName} />
<DetailProperty label={getLangText('DATE')} value={ this.state.piece.date_created.slice(0, 4) } />
{artistEmail}
<ListRequestActions
pieceOrEditions={this.state.piece}
currentUser={this.state.currentUser}
handleSuccess={this.loadPiece}
requestActions={this.state.piece.request_action}/>
<hr/>
</div>
}
subheader={
<PrizePieceRatings
loadPiece={this.loadPiece}
piece={this.state.piece}
currentUser={this.state.currentUser}/>
}>
@ -153,6 +174,7 @@ let NavigationHeader = React.createClass({
let PrizePieceRatings = React.createClass({
propTypes: {
loadPiece: React.PropTypes.func,
piece: React.PropTypes.object,
currentUser: React.PropTypes.object
},
@ -167,6 +189,7 @@ let PrizePieceRatings = React.createClass({
componentDidMount() {
PrizeRatingStore.listen(this.onChange);
PrizeRatingActions.fetchOne(this.props.piece.id);
PrizeRatingActions.fetchAverage(this.props.piece.id);
PieceListStore.listen(this.onChange);
},
@ -199,16 +222,142 @@ let PrizePieceRatings = React.createClass({
onRatingClick(event, args) {
event.preventDefault();
PrizeRatingActions.createRating(this.props.piece.id, args.rating).then(
PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search,
this.state.orderBy, this.state.orderAsc, this.state.filterBy)
this.refreshPieceData()
);
},
handleLoanRequestSuccess(message){
let notification = new GlobalNotificationModel(message, 'success', 4000);
GlobalNotificationActions.appendGlobalNotification(notification);
},
getLoanButton(){
let today = new Moment();
let endDate = new Moment();
endDate.add(6, 'months');
return (
<ModalWrapper
trigger={
<button className='btn btn-default btn-sm'>
{getLangText('SEND LOAN REQUEST')}
</button>
}
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() {
this.props.loadPiece();
PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search,
this.state.orderBy, this.state.orderAsc, this.state.filterBy);
},
onSelectChange() {
PrizeRatingActions.toggleShortlist(this.props.piece.id)
.then(
(res) => {
this.refreshPieceData();
return res;
})
.then(
(res) => {
this.handleShortlistSuccess(res.notification);
}
);
},
render(){
if (this.props.currentUser && this.props.currentUser.is_jury) {
if (this.props.piece && this.props.currentUser && this.props.currentUser.is_judge && this.state.average) {
// Judge sees shortlisting, average and per-jury notes
return (
<div>
<CollapsibleParagraph
title={getLangText('Shortlisting')}
show={true}
defaultExpanded={true}>
<div className="row no-margin">
<span className="ascribe-checkbox-wrapper" style={{marginLeft: '1.5em'}}>
<InputCheckbox
defaultChecked={this.props.piece.selected}
onChange={this.onSelectChange}>
<span>
{getLangText('Select for the prize')}
</span>
</InputCheckbox>
</span>
<span className="pull-right">
{this.props.piece.selected ? this.getLoanButton() : null}
</span>
</div>
<hr />
</CollapsibleParagraph>
<CollapsibleParagraph
title={getLangText('Average Rating')}
show={true}
defaultExpanded={true}>
<div id="list-rating" style={{marginLeft: '1.5em', marginBottom: '1em'}}>
<StarRating
ref='average-rating'
name="average-rating"
caption=""
size='md'
step={0.5}
rating={this.state.average}
ratingAmount={5}/>
</div>
<hr />
{this.state.ratings.map((item, i) => {
let note = item.note ?
<div className="rating-note">
note: {item.note}
</div> : null;
return (
<div className="rating-list">
<div id="list-rating" className="row no-margin">
<span className="pull-right">
<StarRating
ref={'rating' + i}
name={'rating' + i}
caption=""
size='sm'
step={0.5}
rating={item.rating}
ratingAmount={5}/>
</span>
<span> {item.user}</span>
{note}
</div>
</div>
);
})}
<hr />
</CollapsibleParagraph>
</div>);
}
else if (this.props.currentUser && this.props.currentUser.is_jury) {
// Jury can set rating and note
return (
<CollapsibleParagraph
title="Rating"
title={getLangText('Rating')}
show={true}
defaultExpanded={true}>
<div style={{marginLeft: '1.5em', marginBottom: '1em'}}>
@ -222,8 +371,14 @@ let PrizePieceRatings = React.createClass({
onRatingClick={this.onRatingClick}
ratingAmount={5} />
</div>
<PersonalNote
piece={this.props.piece}
<Note
id={() => {return {'piece_id': this.props.piece.id}; }}
label={getLangText('Jury note')}
defaultValue={this.props.piece && this.props.piece.note_from_user ? this.props.piece.note_from_user.note : null}
placeholder={getLangText('Enter your comments ...')}
editable={true}
successMessage={getLangText('Jury note saved')}
url={ApiUrls.notes}
currentUser={this.props.currentUser}/>
</CollapsibleParagraph>);
}
@ -231,43 +386,6 @@ let PrizePieceRatings = React.createClass({
}
});
let PersonalNote = React.createClass({
propTypes: {
piece: React.PropTypes.object,
currentUser: React.PropTypes.object
},
showNotification(){
let notification = new GlobalNotificationModel(getLangText('Jury note saved'), 'success');
GlobalNotificationActions.appendGlobalNotification(notification);
},
render() {
if (this.props.currentUser && this.props.currentUser.username) {
return (
<Form
url={ApiUrls.notes}
handleSuccess={this.showNotification}>
<Property
name='value'
label={getLangText('Jury note')}
editable={true}>
<InputTextAreaToggable
rows={1}
editable={true}
defaultValue={this.props.piece && this.props.piece.note_from_user ? this.props.piece.note_from_user.note : null}
placeholder={getLangText('Enter your comments...')}/>
</Property>
<Property hidden={true} name='piece_id'>
<input defaultValue={this.props.piece.id}/>
</Property>
<hr />
</Form>
);
}
return null;
}
});
let PrizePieceDetails = React.createClass({
propTypes: {

View File

@ -40,23 +40,35 @@ let PrizePieceList = React.createClass({
},
getButtonSubmit() {
if (this.state.prize && this.state.prize.active){
if (this.state.prize && this.state.prize.active && !this.state.currentUser.is_jury){
return (
<ButtonLink to="register_piece">
{getLangText('Submit to prize')}
</ButtonLink>
);
}
else if (this.state.prize && this.state.currentUser.is_judge){
return null;
}
return null;
},
render() {
let orderParams = ['artist_name', 'title'];
if (this.state.currentUser.is_jury) {
orderParams = ['rating', 'title'];
}
if (this.state.currentUser.is_judge) {
orderParams = ['rating', 'title', 'selected'];
}
return (
<div>
<PieceList
ref="list"
redirectTo="register_piece"
accordionListItemType={AccordionListItemPrize}
orderParams={this.state.currentUser.is_jury ? ['rating', 'title'] : ['artist_name', 'title']}
orderParams={orderParams}
orderBy={this.state.currentUser.is_jury ? 'rating' : null}
filterParams={null}
customSubmitButton={this.getButtonSubmit()}/>
</div>

View File

@ -17,6 +17,8 @@ function getPrizeApiUrls(subdomain) {
'jury_resend': AppPrizeConstants.prizeApiEndpoint + subdomain + '/jury/${email}/resend/',
'ratings': AppPrizeConstants.prizeApiEndpoint + subdomain + '/ratings/',
'rating': AppPrizeConstants.prizeApiEndpoint + subdomain + '/ratings/${piece_id}/',
'rating_average': AppPrizeConstants.prizeApiEndpoint + subdomain + '/ratings/${piece_id}/average/',
'select_piece' : AppPrizeConstants.prizeApiEndpoint + subdomain + '/ratings/${piece_id}/select/',
'notes': AppPrizeConstants.prizeApiEndpoint + subdomain + '/notes/',
'note': AppPrizeConstants.prizeApiEndpoint + subdomain + '/notes/${piece_id}/'

View File

@ -4,8 +4,8 @@ import requests from '../../../../utils/requests';
let PrizeRatingFetcher = {
fetch() {
return requests.get('rating');
fetchAverage(pieceId) {
return requests.get('rating_average', {'piece_id': pieceId});
},
fetchOne(pieceId) {
@ -13,7 +13,11 @@ let PrizeRatingFetcher = {
},
rate(pieceId, rating) {
return requests.post('ratings', {body: {'piece_id': pieceId, 'value': rating}});
return requests.post('ratings', {body: {'piece_id': pieceId, 'note': rating}});
},
select(pieceId) {
return requests.post('select_piece', {'piece_id': pieceId});
}
};

View File

@ -8,16 +8,22 @@ class PrizeRatingStore {
constructor() {
this.ratings = [];
this.currentRating = null;
this.average = null;
this.bindActions(PrizeRatingActions);
}
onUpdatePrizeRatings( ratings ) {
onUpdatePrizeRatings(ratings) {
this.ratings = ratings;
}
onUpdatePrizeRating( rating ) {
onUpdatePrizeRating(rating) {
this.currentRating = parseInt(rating, 10);
}
onUpdatePrizeRatingAverage(data) {
this.average = data.average;
this.ratings = data.ratings;
}
}
export default alt.createStore(PrizeRatingStore, 'PrizeRatingStore');

View File

@ -67,11 +67,6 @@ let CylandPieceContainer = React.createClass({
PieceActions.fetchOne(this.props.params.pieceId);
},
getId() {
return {'id': this.state.piece.id};
},
render() {
if('title' in this.state.piece) {
return (
@ -106,7 +101,7 @@ let CylandPieceContainer = React.createClass({
show={(this.state.currentUser.username && true || false) ||
(this.state.piece.public_note)}>
<Note
id={this.getId}
id={() => {return {'id': this.state.piece.id}; }}
label={getLangText('Personal note (private)')}
defaultValue={this.state.piece.private_note ? this.state.piece.private_note : null}
placeholder={getLangText('Enter your comments ...')}

View File

@ -7,7 +7,8 @@ function getWalletApiUrls(subdomain) {
if (subdomain === 'cyland'){
return {
'pieces_list': walletConstants.walletApiEndpoint + subdomain + '/pieces/',
'piece': walletConstants.walletApiEndpoint + subdomain + '/pieces/${piece_id}/'
'piece': walletConstants.walletApiEndpoint + subdomain + '/pieces/${piece_id}/',
'piece_extradata': walletConstants.walletApiEndpoint + subdomain + '/pieces/${piece_id}/extradata/'
};
}
else if (subdomain === 'ikonotv'){

View File

@ -32,6 +32,9 @@ let ApiUrls = {
'ownership_loans_pieces': AppConstants.apiEndpoint + 'ownership/loans/pieces/',
'ownership_loans_pieces_confirm': AppConstants.apiEndpoint + 'ownership/loans/pieces/confirm/',
'ownership_loans_pieces_deny': AppConstants.apiEndpoint + 'ownership/loans/pieces/deny/',
'ownership_loans_pieces_request': AppConstants.apiEndpoint + 'ownership/loans/pieces/request/',
'ownership_loans_pieces_request_confirm': AppConstants.apiEndpoint + 'ownership/loans/pieces/request_confirm/',
'ownership_loans_pieces_request_deny': AppConstants.apiEndpoint + 'ownership/loans/pieces/request_deny/',
'ownership_loans_editions': AppConstants.apiEndpoint + 'ownership/loans/editions/',
'ownership_loans_confirm': AppConstants.apiEndpoint + 'ownership/loans/editions/confirm/',
'ownership_loans_deny': AppConstants.apiEndpoint + 'ownership/loans/editions/deny/',

View File

@ -17,8 +17,11 @@ let OwnershipFetcher = {
*/
fetchLoanContractList(){
return requests.get(ApiUrls.ownership_loans_contract);
}
},
fetchLoanPieceRequestList(){
return requests.get(ApiUrls.ownership_loans_pieces_request);
}
};
export default OwnershipFetcher;

View File

@ -0,0 +1,35 @@
'use strict';
import alt from '../alt';
import OwnershipActions from '../actions/ownership_actions';
class OwnershipStore {
constructor() {
this.loanRequestList = [];
this.loanRequest = null;
this.bindActions(OwnershipActions);
}
onUpdateLoanPieceRequestList(loanRequests) {
this.loanRequestList = loanRequests;
}
onUpdateLoanPieceRequest({loanRequests, pieceId}) {
this.loanRequestList = loanRequests;
this.loanRequest = loanRequests.filter((item) => item.piece_id === pieceId.toString());
if (this.loanRequest.length > 0){
this.loanRequest = this.loanRequest[0];
}
else {
this.loanRequest = null;
}
}
onFlushLoanPieceRequest(){
this.loanRequestList = [];
this.loanRequest = null;
}
}
export default alt.createStore(OwnershipStore, 'OwnershipStore');

View File

@ -23,6 +23,8 @@ export function getAclFormMessage(aclName, entities, senderName) {
message += getLangText('I un-consign');
} else if(aclName === 'acl_loan') {
message += getLangText('I loan');
} else if(aclName === 'acl_loan_request') {
message += getLangText('I request to loan');
} else if(aclName === 'acl_share') {
message += getLangText('I share');
} else {
@ -34,7 +36,7 @@ export function getAclFormMessage(aclName, entities, senderName) {
if(aclName === 'acl_transfer' || aclName === 'acl_loan' || aclName === 'acl_consign') {
message += getLangText('to you');
} else if(aclName === 'acl_unconsign') {
} else if(aclName === 'acl_unconsign' || aclName === 'acl_loan_request') {
message += getLangText('from you');
} else if(aclName === 'acl_share') {
message += getLangText('with you');

View File

@ -73,7 +73,7 @@
"raven-js": "^1.1.19",
"react": "^0.13.2",
"react-bootstrap": "^0.24.3",
"react-datepicker": "~0.8.0",
"react-datepicker": "^0.12.0",
"react-progressbar": "^1.1.0",
"react-router": "^0.13.3",
"react-router-bootstrap": "~0.16.0",

View File

@ -152,11 +152,11 @@ span.ascribe-accordion-list-table-toggle {
}
}
.request-action-batch {
.request-action-badge {
position: absolute;
top: 0px;
right: 0px;
color: #666;
color: $ascribe-color-green;
font-size: 1.2em;
padding: 0.3em;
}

View File

@ -145,8 +145,15 @@
display: inline-block;
width: 100%;
border-top: 1px solid rgba(0,0,0,.05);
padding: .5em 1.5em .5em 1.5em;
}
.ascribe-checkbox-wrapper{
.checkbox > span {color: black;}
}
.ascribe-settings-property-collapsible-toggle, .ascribe-checkbox-wrapper {
cursor:pointer;
@ -196,4 +203,10 @@
font-size: 20px;
color: rgba(2, 182, 163, 1);
}
}
.ascribe-checkbox-badge{
> span > span {
margin-top: 0;
}
}

View File

@ -403,17 +403,6 @@ hr {
}
}
.rating-container .rating-stars {
width: 25px;
color: #02b6a3;
}
#list-rating > a > span > span > .rating-container .rating-stars{
color: #000;
}
.react-rating-caption {
font-size: 1em;
}
.disable-select {
-webkit-user-select: none;

View File

@ -1,4 +1,5 @@
@import 'landing';
@import 'rating';
.ascribe-prize-app {
border-radius: 0;

View File

@ -0,0 +1,34 @@
.rating-container .rating-stars {
width: 25px;
color: #02b6a3;
}
#list-rating {
> a > span > span > .rating-container .rating-stars {
color: #000;
}
> span > span > span > .rating-container .rating-stars {
color: #000;
}
> span > span > .rating-container .rating-stars {
color: #000;
}
}
.react-rating-caption {
font-size: 1em;
}
.rating-list {
margin-left: 1.5em;
font-size: 0.9em;
margin-bottom: 0.3em;
color: #333;
}
.rating-note {
color: #666;
font-style: italic;
padding: 0.7em;
}