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

Merged in AD-545-add-rating-support-for-the-pieces (pull request #37)

Ad 545 add rating support for the pieces
This commit is contained in:
diminator 2015-08-13 13:30:43 +02:00
commit 722898b0e3
17 changed files with 870 additions and 379 deletions

View File

@ -3,151 +3,24 @@
import React from 'react';
import Router from 'react-router';
import Glyphicon from 'react-bootstrap/lib/Glyphicon';
import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger';
import Tooltip from 'react-bootstrap/lib/Tooltip';
import AccordionListItemEditionWidget from './accordion_list_item_edition_widget';
import CreateEditionsForm from '../ascribe_forms/create_editions_form';
import PieceListActions from '../../actions/piece_list_actions';
import PieceListStore from '../../stores/piece_list_store';
import WhitelabelStore from '../../stores/whitelabel_store';
import EditionListActions from '../../actions/edition_list_actions';
import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions';
import AclProxy from '../acl_proxy';
import SubmitToPrizeButton from '../whitelabel/prize/components/ascribe_buttons/submit_to_prize_button';
import { getLangText } from '../../utils/lang_utils';
import { mergeOptions } from '../../utils/general_utils';
let Link = Router.Link;
let AccordionListItem = React.createClass({
propTypes: {
badge: React.PropTypes.object,
className: React.PropTypes.string,
content: React.PropTypes.object,
children: React.PropTypes.object
thumbnail: React.PropTypes.object,
heading: React.PropTypes.object,
subheading: React.PropTypes.object,
subsubheading: React.PropTypes.object,
buttons: React.PropTypes.object,
children: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.element),
React.PropTypes.element
])
},
mixins: [Router.Navigation],
getInitialState() {
return mergeOptions(
{
showCreateEditionsDialog: false
},
PieceListStore.getState(),
WhitelabelStore.getState()
);
},
componentDidMount() {
PieceListStore.listen(this.onChange);
WhitelabelStore.listen(this.onChange);
},
componentWillUnmount() {
PieceListStore.unlisten(this.onChange);
WhitelabelStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
},
getGlyphicon(){
if (this.props.content.requestAction) {
return (
<OverlayTrigger
delay={500}
placement="left"
overlay={<Tooltip>{getLangText('You have actions pending in one of your editions')}</Tooltip>}>
<Glyphicon glyph='bell'/>
</OverlayTrigger>);
}
return null;
},
toggleCreateEditionsDialog() {
this.setState({
showCreateEditionsDialog: !this.state.showCreateEditionsDialog
});
},
handleEditionCreationSuccess() {
PieceListActions.updatePropertyForPiece({pieceId: this.props.content.id, key: 'num_editions', value: 0});
this.toggleCreateEditionsDialog();
},
handleSubmitPrizeSuccess(response) {
PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search,
this.state.orderBy, this.state.orderAsc, this.state.filterBy);
let notification = new GlobalNotificationModel(response.notification, 'success', 10000);
GlobalNotificationActions.appendGlobalNotification(notification);
},
onPollingSuccess(pieceId) {
PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search,
this.state.orderBy, this.state.orderAsc, this.state.filterBy);
EditionListActions.toggleEditionList(pieceId);
let notification = new GlobalNotificationModel('Editions successfully created', 'success', 10000);
GlobalNotificationActions.appendGlobalNotification(notification);
},
getCreateEditionsDialog() {
if (this.props.content.num_editions < 1 && this.state.showCreateEditionsDialog) {
return (
<div
className="ascribe-accordion-list-item-table col-xs-12 col-sm-10 col-md-8 col-lg-8 col-sm-offset-1 col-md-offset-2 col-lg-offset-2">
<CreateEditionsForm
pieceId={this.props.content.id}
handleSuccess={this.handleEditionCreationSuccess}/>
</div>
);
}
},
getLicences() {
// convert this to acl_view_licences later
if (this.state.whitelabel && this.state.whitelabel.name === 'Creative Commons France') {
return (
<span>
<span>, </span>
<a href={this.props.content.license_type.url} target="_blank">
{getLangText('%s license', this.props.content.license_type.code)}
</a>
</span>
);
}
},
render() {
let linkData;
if (this.props.content.num_editions < 1 || !this.props.content.first_edition) {
linkData = {
to: 'piece',
params: {
pieceId: this.props.content.id
}
};
} else {
linkData = {
to: 'edition',
params: {
editionId: this.props.content.first_edition.bitcoin_id
}
};
}
return (
<div className="row">
@ -155,52 +28,22 @@ let AccordionListItem = React.createClass({
<div className="wrapper">
<div className="col-xs-4 col-sm-3 col-md-2 col-lg-2 clear-paddings">
<div className="thumbnail-wrapper">
<Link {...linkData}>
<img src={this.props.content.thumbnail.url_safe}/>
</Link>
{this.props.thumbnail}
</div>
</div>
<div className="col-xs-8 col-sm-9 col-md-9 col-lg-9 col-md-offset-1 col-lg-offset-1 accordion-list-item-header">
<Link {...linkData}>
<h1>{this.props.content.title}</h1>
</Link>
<h3>{getLangText('by %s', this.props.content.artist_name)}</h3>
<div>
<span className="pull-left">{this.props.content.date_created.split('-')[0]}</span>
<AclProxy
aclObject={this.props.content.acl}
aclName="acl_view_editions">
<AccordionListItemEditionWidget
className="pull-right"
piece={this.props.content}
toggleCreateEditionsDialog={this.toggleCreateEditionsDialog}
onPollingSuccess={this.onPollingSuccess}/>
</AclProxy>
<AclProxy
aclObject={this.props.content.acl}
aclName="acl_submit_to_prize">
<SubmitToPrizeButton
className="pull-right"
piece={this.props.content}
handleSuccess={this.handleSubmitPrizeSuccess}/>
</AclProxy>
{this.getLicences()}
</div>
{this.props.heading}
{this.props.subheading}
{this.props.subsubheading}
{this.props.buttons}
</div>
<span style={{'clear': 'both'}}></span>
<div className="request-action-batch">
{this.getGlyphicon()}
{this.props.badge}
</div>
</div>
</div>
{this.getCreateEditionsDialog()}
{/* this.props.children is AccordionListItemTableEditions */}
{this.props.children}
</div>
);

View File

@ -0,0 +1,77 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import AccordionListItem from './accordion_list_item';
import { getLangText } from '../../utils/lang_utils';
let Link = Router.Link;
let AccordionListItemPiece = React.createClass({
propTypes: {
className: React.PropTypes.string,
piece: React.PropTypes.object,
children: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.element),
React.PropTypes.element
]),
subsubheading: React.PropTypes.object,
buttons: React.PropTypes.object,
badge: React.PropTypes.object
},
mixins: [Router.Navigation],
getLinkData(){
let linkData;
if (this.props.piece.num_editions < 1 || !this.props.piece.first_edition) {
linkData = {
to: 'piece',
params: {
pieceId: this.props.piece.id
}
};
} else {
linkData = {
to: 'edition',
params: {
editionId: this.props.piece.first_edition.bitcoin_id
}
};
}
return linkData;
},
render() {
return (
<AccordionListItem
className={this.props.className}
thumbnail={
<Link {...this.getLinkData()}>
<img src={this.props.piece.thumbnail.url_safe}/>
</Link>}
heading={
<Link {...this.getLinkData()}>
<h1>{this.props.piece.title}</h1>
</Link>}
subheading={
<h3>
{getLangText('by ')}
{this.props.artistName ? this.props.artistName : this.props.piece.artist_name}
</h3>
}
subsubheading={this.props.subsubheading}
buttons={this.props.buttons}
badge={this.props.badge}
>
{this.props.children}
</AccordionListItem>
);
}
});
export default AccordionListItemPiece;

View File

@ -0,0 +1,156 @@
'use strict';
import React from 'react';
import Glyphicon from 'react-bootstrap/lib/Glyphicon';
import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger';
import Tooltip from 'react-bootstrap/lib/Tooltip';
import AccordionListItemPiece from './accordion_list_item_piece';
import AccordionListItemEditionWidget from './accordion_list_item_edition_widget';
import CreateEditionsForm from '../ascribe_forms/create_editions_form';
import PieceListActions from '../../actions/piece_list_actions';
import PieceListStore from '../../stores/piece_list_store';
import WhitelabelStore from '../../stores/whitelabel_store';
import EditionListActions from '../../actions/edition_list_actions';
import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions';
import AclProxy from '../acl_proxy';
import { getLangText } from '../../utils/lang_utils';
import { mergeOptions } from '../../utils/general_utils';
let AccordionListItemWallet = React.createClass({
propTypes: {
className: React.PropTypes.string,
content: React.PropTypes.object,
children: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.element),
React.PropTypes.element
])
},
getInitialState() {
return mergeOptions(
{
showCreateEditionsDialog: false
},
PieceListStore.getState(),
WhitelabelStore.getState()
);
},
componentDidMount() {
PieceListStore.listen(this.onChange);
WhitelabelStore.listen(this.onChange);
},
componentWillUnmount() {
PieceListStore.unlisten(this.onChange);
WhitelabelStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
},
getGlyphicon(){
if (this.props.content.requestAction) {
return (
<OverlayTrigger
delay={500}
placement="left"
overlay={<Tooltip>{getLangText('You have actions pending in one of your editions')}</Tooltip>}>
<Glyphicon glyph='bell'/>
</OverlayTrigger>);
}
return null;
},
toggleCreateEditionsDialog() {
this.setState({
showCreateEditionsDialog: !this.state.showCreateEditionsDialog
});
},
handleEditionCreationSuccess() {
PieceListActions.updatePropertyForPiece({pieceId: this.props.content.id, key: 'num_editions', value: 0});
this.toggleCreateEditionsDialog();
},
onPollingSuccess(pieceId) {
PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search,
this.state.orderBy, this.state.orderAsc, this.state.filterBy);
EditionListActions.toggleEditionList(pieceId);
let notification = new GlobalNotificationModel('Editions successfully created', 'success', 10000);
GlobalNotificationActions.appendGlobalNotification(notification);
},
getCreateEditionsDialog() {
if (this.props.content.num_editions < 1 && this.state.showCreateEditionsDialog) {
return (
<div
className="ascribe-accordion-list-item-table col-xs-12 col-sm-10 col-md-8 col-lg-8 col-sm-offset-1 col-md-offset-2 col-lg-offset-2">
<CreateEditionsForm
pieceId={this.props.content.id}
handleSuccess={this.handleEditionCreationSuccess}/>
</div>
);
}
},
getLicences() {
// convert this to acl_view_licences later
if (this.state.whitelabel && this.state.whitelabel.name === 'Creative Commons France') {
return (
<span>
<span>, </span>
<a href={this.props.content.license_type.url} target="_blank">
{getLangText('%s license', this.props.content.license_type.code)}
</a>
</span>
);
}
},
render() {
return (
<AccordionListItemPiece
className={this.props.className}
piece={this.props.content}
subsubheading={
<div className="pull-left">
<span>{this.props.content.date_created.split('-')[0]}</span>
{this.getLicences()}
</div>}
buttons={
<div>
<AclProxy
aclObject={this.props.content.acl}
aclName="acl_view_editions">
<AccordionListItemEditionWidget
className="pull-right"
piece={this.props.content}
toggleCreateEditionsDialog={this.toggleCreateEditionsDialog}
onPollingSuccess={this.onPollingSuccess}/>
</AclProxy>
</div>}
badge={this.getGlyphicon()}>
{this.getCreateEditionsDialog()}
{/* this.props.children is AccordionListItemTableEditions */}
{this.props.children}
</AccordionListItemPiece>
);
}
});
export default AccordionListItemWallet;

View File

@ -23,7 +23,7 @@ const CollapsibleParagraph = React.createClass({
getInitialState() {
return {
expanded: false
expanded: this.props.defaultExpanded
};
},

View File

@ -17,9 +17,9 @@ let DetailProperty = React.createClass({
getDefaultProps() {
return {
separator: ':',
labelClassName: 'col-xs-3 col-sm-3 col-md-2 col-lg-2',
valueClassName: 'col-xs-9 col-sm-9 col-md-10 col-lg-10'
separator: '',
labelClassName: 'col-xs-3 col-sm-3 col-md-2 col-lg-2 col-xs-height col-bottom ascribe-detail-property-label',
valueClassName: 'col-xs-9 col-sm-9 col-md-10 col-lg-10 col-xs-height col-bottom ascribe-detail-property-value'
};
},
@ -52,11 +52,11 @@ let DetailProperty = React.createClass({
return (
<div className="row ascribe-detail-property">
<div className="row-same-height">
<div className={this.props.labelClassName + ' col-xs-height col-bottom ascribe-detail-property-label'}>
{ this.props.label + this.props.separator}
<div className={this.props.labelClassName}>
{ this.props.label } { this.props.separator}
</div>
<div
className={this.props.valueClassName + ' col-xs-height col-bottom ascribe-detail-property-value'}
className={this.props.valueClassName}
style={styles}>
{value}
</div>

View File

@ -19,7 +19,33 @@ const EMBED_IFRAME_HEIGHT = {
let MediaContainer = React.createClass({
propTypes: {
content: React.PropTypes.object
content: React.PropTypes.object,
refreshObject: React.PropTypes.func
},
getInitialState() {
return {timerId: null};
},
componentDidMount() {
if (!this.props.content.digital_work) {
return;
}
let isEncoding = this.props.content.digital_work.isEncoding;
if (this.props.content.digital_work.mime === 'video' && typeof isEncoding === 'number' && isEncoding !== 100 && !this.state.timerId) {
let timerId = window.setInterval(this.props.refreshObject, 10000);
this.setState({timerId: timerId});
}
},
componentWillUpdate() {
if (this.props.content.digital_work.isEncoding === 100) {
window.clearInterval(this.state.timerId);
}
},
componentWillUnmount() {
window.clearInterval(this.state.timerId);
},
render() {

View File

@ -1,38 +1,14 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import Row from 'react-bootstrap/lib/Row';
import Col from 'react-bootstrap/lib/Col';
import DetailProperty from './detail_property';
import UserActions from '../../actions/user_actions';
import UserStore from '../../stores/user_store';
import PieceListActions from '../../actions/piece_list_actions';
import PieceListStore from '../../stores/piece_list_store';
import EditionListActions from '../../actions/edition_list_actions';
import PieceActions from '../../actions/piece_actions';
import MediaContainer from './media_container';
import EditionDetailProperty from './detail_property';
import AclButtonList from './../ascribe_buttons/acl_button_list';
import CreateEditionsForm from '../ascribe_forms/create_editions_form';
import CreateEditionsButton from '../ascribe_buttons/create_editions_button';
import DeleteButton from '../ascribe_buttons/delete_button';
import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions';
import { getLangText } from '../../utils/lang_utils';
import { mergeOptions } from '../../utils/general_utils';
/**
* This is the component that implements display-specific functionality
@ -40,97 +16,16 @@ import { mergeOptions } from '../../utils/general_utils';
let Piece = React.createClass({
propTypes: {
piece: React.PropTypes.object,
header: React.PropTypes.object,
subheader: React.PropTypes.object,
buttons: React.PropTypes.object,
loadPiece: React.PropTypes.func,
children: React.PropTypes.object
},
mixins: [Router.Navigation],
getInitialState() {
return mergeOptions(
UserStore.getState(),
PieceListStore.getState(),
{
showCreateEditionsDialog: false
}
);
},
componentDidMount() {
UserStore.listen(this.onChange);
PieceListStore.listen(this.onChange);
UserActions.fetchCurrentUser();
},
componentWillUnmount() {
UserStore.unlisten(this.onChange);
PieceListStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
},
toggleCreateEditionsDialog() {
this.setState({
showCreateEditionsDialog: !this.state.showCreateEditionsDialog
});
},
handleEditionCreationSuccess() {
PieceActions.updateProperty({key: 'num_editions', value: 0});
PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search,
this.state.orderBy, this.state.orderAsc, this.state.filterBy);
this.toggleCreateEditionsDialog();
},
handleDeleteSuccess(response) {
PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search,
this.state.orderBy, this.state.orderAsc, this.state.filterBy);
// since we're deleting a piece, we just need to close
// all editions dialogs and not reload them
EditionListActions.closeAllEditionLists();
EditionListActions.clearAllEditionSelections();
let notification = new GlobalNotificationModel(response.notification, 'success');
GlobalNotificationActions.appendGlobalNotification(notification);
this.transitionTo('pieces');
},
getCreateEditionsDialog() {
if(this.props.piece.num_editions < 1 && this.state.showCreateEditionsDialog) {
return (
<div style={{marginTop: '1em'}}>
<CreateEditionsForm
pieceId={this.props.piece.id}
handleSuccess={this.handleEditionCreationSuccess} />
<hr/>
</div>
);
} else {
return (<hr/>);
}
},
handlePollingSuccess(pieceId, numEditions) {
// we need to refresh the num_editions property of the actual piece we're looking at
PieceActions.updateProperty({
key: 'num_editions',
value: numEditions
});
// as well as its representation in the collection
// btw.: It's not sufficient to just set num_editions to numEditions, since a single accordion
// list item also uses the firstEdition property which we can only get from the server in that case.
// Therefore we need to at least refetch the changed piece from the server or on our case simply all
PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search,
this.state.orderBy, this.state.orderAsc, this.state.filterBy);
let notification = new GlobalNotificationModel('Editions successfully created', 'success', 10000);
GlobalNotificationActions.appendGlobalNotification(notification);
updateObject() {
return PieceActions.fetchOne(this.props.piece.id);
},
render() {
@ -138,38 +33,14 @@ let Piece = React.createClass({
<Row>
<Col md={6}>
<MediaContainer
refreshObject={this.updateObject}
content={this.props.piece}/>
</Col>
<Col md={6} className="ascribe-edition-details">
<div className="ascribe-detail-header">
<h1 className="ascribe-detail-title">{this.props.piece.title}</h1>
<hr/>
<EditionDetailProperty label="BY" value={this.props.piece.artist_name} />
<EditionDetailProperty label="DATE" value={ this.props.piece.date_created.slice(0, 4) } />
{this.props.piece.num_editions > 0 ? <EditionDetailProperty label="EDITIONS" value={ this.props.piece.num_editions } /> : null}
<hr/>
</div>
<div className="ascribe-detail-header">
<DetailProperty label={getLangText('REGISTREE')} value={ this.props.piece.user_registered } />
</div>
{this.props.header}
{this.props.subheader}
{this.props.buttons}
<AclButtonList
className="text-center ascribe-button-list"
availableAcls={this.props.piece.acl}
editions={this.props.piece}
handleSuccess={this.props.loadPiece}>
<CreateEditionsButton
label={getLangText('CREATE EDITIONS')}
className="btn-sm"
piece={this.props.piece}
toggleCreateEditionsDialog={this.toggleCreateEditionsDialog}
onPollingSuccess={this.handlePollingSuccess}/>
<DeleteButton
handleSuccess={this.handleDeleteSuccess}
piece={this.props.piece}/>
</AclButtonList>
{this.getCreateEditionsDialog()}
{this.props.children}
</Col>

View File

@ -1,37 +1,59 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import PieceActions from '../../actions/piece_actions';
import PieceStore from '../../stores/piece_store';
import PieceListActions from '../../actions/piece_list_actions';
import PieceListStore from '../../stores/piece_list_store';
import UserActions from '../../actions/user_actions';
import UserStore from '../../stores/user_store';
import EditionListActions from '../../actions/edition_list_actions';
import Piece from './piece';
import CollapsibleParagraph from './../ascribe_collapsible/collapsible_paragraph';
import FurtherDetails from './further_details';
import DetailProperty from './detail_property';
import AclButtonList from './../ascribe_buttons/acl_button_list';
import CreateEditionsForm from '../ascribe_forms/create_editions_form';
import CreateEditionsButton from '../ascribe_buttons/create_editions_button';
import DeleteButton from '../ascribe_buttons/delete_button';
import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions';
import AppConstants from '../../constants/application_constants';
import { mergeOptions } from '../../utils/general_utils';
import { getLangText } from '../../utils/lang_utils';
/**
* This is the component that implements resource/data specific functionality
*/
let PieceContainer = React.createClass({
getInitialState() {
return PieceStore.getState();
},
onChange(state) {
this.setState(state);
if (!state.piece.digital_work) {
return;
}
let isEncoding = state.piece.digital_work.isEncoding;
if (state.piece.digital_work.mime === 'video' && typeof isEncoding === 'number' && isEncoding !== 100 && !this.state.timerId) {
let timerId = window.setInterval(() => PieceActions.fetchOne(this.props.params.pieceId), 10000);
this.setState({timerId: timerId});
}
mixins: [Router.Navigation],
getInitialState() {
return mergeOptions(
UserStore.getState(),
PieceListStore.getState(),
PieceStore.getState(),
{
showCreateEditionsDialog: false
}
);
},
componentDidMount() {
UserStore.listen(this.onChange);
PieceListStore.listen(this.onChange);
UserActions.fetchCurrentUser();
PieceStore.listen(this.onChange);
PieceActions.fetchOne(this.props.params.pieceId);
},
@ -42,21 +64,121 @@ let PieceContainer = React.createClass({
// as it will otherwise display wrong/old data once the user loads
// the piece detail a second time
PieceActions.updatePiece({});
window.clearInterval(this.state.timerId);
PieceStore.unlisten(this.onChange);
UserStore.unlisten(this.onChange);
PieceListStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
},
loadPiece() {
PieceActions.fetchOne(this.props.params.pieceId);
},
toggleCreateEditionsDialog() {
this.setState({
showCreateEditionsDialog: !this.state.showCreateEditionsDialog
});
},
handleEditionCreationSuccess() {
PieceActions.updateProperty({key: 'num_editions', value: 0});
PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search,
this.state.orderBy, this.state.orderAsc, this.state.filterBy);
this.toggleCreateEditionsDialog();
},
handleDeleteSuccess(response) {
PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search,
this.state.orderBy, this.state.orderAsc, this.state.filterBy);
// since we're deleting a piece, we just need to close
// all editions dialogs and not reload them
EditionListActions.closeAllEditionLists();
EditionListActions.clearAllEditionSelections();
let notification = new GlobalNotificationModel(response.notification, 'success');
GlobalNotificationActions.appendGlobalNotification(notification);
this.transitionTo('pieces');
},
getCreateEditionsDialog() {
if(this.state.piece.num_editions < 1 && this.state.showCreateEditionsDialog) {
return (
<div style={{marginTop: '1em'}}>
<CreateEditionsForm
pieceId={this.state.piece.id}
handleSuccess={this.handleEditionCreationSuccess} />
<hr/>
</div>
);
} else {
return (<hr/>);
}
},
handlePollingSuccess(pieceId, numEditions) {
// we need to refresh the num_editions property of the actual piece we're looking at
PieceActions.updateProperty({
key: 'num_editions',
value: numEditions
});
// as well as its representation in the collection
// btw.: It's not sufficient to just set num_editions to numEditions, since a single accordion
// list item also uses the firstEdition property which we can only get from the server in that case.
// Therefore we need to at least refetch the changed piece from the server or on our case simply all
PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search,
this.state.orderBy, this.state.orderAsc, this.state.filterBy);
let notification = new GlobalNotificationModel('Editions successfully created', 'success', 10000);
GlobalNotificationActions.appendGlobalNotification(notification);
},
render() {
if('title' in this.state.piece) {
return (
<Piece
piece={this.state.piece}
loadPiece={this.loadPiece}>
loadPiece={this.loadPiece}
header={
<div className="ascribe-detail-header">
<h1 className="ascribe-detail-title">{this.state.piece.title}</h1>
<hr/>
<DetailProperty label="BY" value={this.state.piece.artist_name} />
<DetailProperty label="DATE" value={ this.state.piece.date_created.slice(0, 4) } />
{this.state.piece.num_editions > 0 ? <DetailProperty label="EDITIONS" value={ this.state.piece.num_editions } /> : null}
<hr/>
</div>
}
subheader={
<div className="ascribe-detail-header">
<DetailProperty label={getLangText('REGISTREE')} value={ this.state.piece.user_registered } />
</div>
}
buttons={
<AclButtonList
className="text-center ascribe-button-list"
availableAcls={this.state.piece.acl}
editions={this.state.piece}
handleSuccess={this.state.loadPiece}>
<CreateEditionsButton
label={getLangText('CREATE EDITIONS')}
className="btn-sm"
piece={this.state.piece}
toggleCreateEditionsDialog={this.toggleCreateEditionsDialog}
onPollingSuccess={this.handlePollingSuccess}/>
<DeleteButton
handleSuccess={this.handleDeleteSuccess}
piece={this.state.piece}/>
</AclButtonList>
}>
{this.getCreateEditionsDialog()}
<CollapsibleParagraph
title="Further Details"
show={this.state.piece.acl.acl_edit

View File

@ -10,7 +10,7 @@ import EditionListStore from '../stores/edition_list_store';
import EditionListActions from '../actions/edition_list_actions';
import AccordionList from './ascribe_accordion_list/accordion_list';
import AccordionListItem from './ascribe_accordion_list/accordion_list_item';
import AccordionListItemWallet from './ascribe_accordion_list/accordion_list_item_wallet';
import AccordionListItemTableEditions from './ascribe_accordion_list/accordion_list_item_table_editions';
import Pagination from './ascribe_pagination/pagination';
@ -24,12 +24,19 @@ import { mergeOptions } from '../utils/general_utils';
let PieceList = React.createClass({
propTypes: {
accordionListItemType: React.PropTypes.func,
redirectTo: React.PropTypes.string,
customSubmitButton: React.PropTypes.element
},
mixins: [Router.Navigation, Router.State],
getDefaultProps() {
return {
accordionListItemType: AccordionListItemWallet
};
},
getInitialState() {
return mergeOptions(
PieceListStore.getState(),
@ -128,6 +135,7 @@ let PieceList = React.createClass({
render() {
let loadingElement = (<img src={AppConstants.baseUrl + 'static/img/ascribe_animated_medium.gif'} />);
let AccordionListItemType = this.props.accordionListItemType;
return (
<div>
<PieceListToolbar
@ -151,14 +159,14 @@ let PieceList = React.createClass({
loadingElement={loadingElement}>
{this.state.pieceList.map((piece, i) => {
return (
<AccordionListItem
<AccordionListItemType
className="col-xs-12 col-sm-10 col-md-8 col-lg-8 col-sm-offset-1 col-md-offset-2 col-lg-offset-2 ascribe-accordion-list-item"
content={piece}
key={i}>
<AccordionListItemTableEditions
className="ascribe-accordion-list-item-table col-xs-12 col-sm-10 col-md-8 col-lg-8 col-sm-offset-1 col-md-offset-2 col-lg-offset-2"
parentId={piece.id} />
</AccordionListItem>
</AccordionListItemType>
);
})}
</AccordionList>

View File

@ -37,7 +37,6 @@ class PrizeRatingActions {
resolve(res);
})
.catch((err) => {
console.logGlobal(err);
reject(err);
});
});
@ -52,7 +51,6 @@ class PrizeRatingActions {
resolve(res);
})
.catch((err) => {
console.logGlobal(err);
reject(err);
});
});

View File

@ -0,0 +1,131 @@
'use strict';
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 UserStore from '../../../../../stores/user_store';
import GlobalNotificationModel from '../../../../../models/global_notification_model';
import GlobalNotificationActions from '../../../../../actions/global_notification_actions';
import AclProxy from '../../../../acl_proxy';
import SubmitToPrizeButton from './../ascribe_buttons/submit_to_prize_button';
import { getLangText } from '../../../../../utils/lang_utils';
import { mergeOptions } from '../../../../../utils/general_utils';
let Link = Router.Link;
let AccordionListItemPrize = React.createClass({
propTypes: {
className: React.PropTypes.string,
content: React.PropTypes.object,
children: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.element),
React.PropTypes.element
])
},
getInitialState() {
return mergeOptions(
PieceListStore.getState(),
UserStore.getState()
);
},
componentDidMount() {
PieceListStore.listen(this.onChange);
UserStore.listen(this.onChange);
},
componentWillUnmount() {
PieceListStore.unlisten(this.onChange);
UserStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
},
handleSubmitPrizeSuccess(response) {
PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search,
this.state.orderBy, this.state.orderAsc, this.state.filterBy);
let notification = new GlobalNotificationModel(response.notification, 'success', 10000);
GlobalNotificationActions.appendGlobalNotification(notification);
},
getPrizeButtons() {
if (this.state.currentUser && this.state.currentUser.is_jury){
if (this.props.content.ratings && this.props.content.ratings.rating){
// jury and rating available
let rating = parseInt(this.props.content.ratings.rating, 10);
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}
size='sm'
rating={rating}
ratingAmount={5} />
</Link>
</div>);
}
else {
// 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
</Link>
</div>
);
}
}
// participant
return (
<div>
<AclProxy
aclObject={this.props.content.acl}
aclName="acl_submit_to_prize">
<SubmitToPrizeButton
className="pull-right"
piece={this.props.content}
handleSuccess={this.handleSubmitPrizeSuccess}/>
</AclProxy>
</div>
);
},
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;
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>
);
}
});
export default AccordionListItemPrize;

View File

@ -1,15 +1,21 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import StarRating from 'react-star-rating';
import PieceActions from '../../../../../actions/piece_actions';
import PieceStore from '../../../../../stores/piece_store';
import PieceListStore from '../../../../../stores/piece_list_store';
import PieceListActions from '../../../../../actions/piece_list_actions';
import PrizeRatingActions from '../../actions/prize_rating_actions';
import PrizeRatingStore from '../../stores/prize_rating_store';
import UserStore from '../../../../../stores/user_store';
import Piece from '../../../../../components/ascribe_detail/piece';
import AppConstants from '../../../../../constants/application_constants';
@ -19,21 +25,32 @@ 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 GlobalNotificationModel from '../../../../../models/global_notification_model';
import GlobalNotificationActions from '../../../../../actions/global_notification_actions';
import DetailProperty from '../../../../ascribe_detail/detail_property';
import ApiUrls from '../../../../../constants/api_urls';
import { mergeOptions } from '../../../../../utils/general_utils';
import { getLangText } from '../../../../../utils/lang_utils';
let Link = Router.Link;
/**
* This is the component that implements resource/data specific functionality
*/
let PieceContainer = React.createClass({
getInitialState() {
return PieceStore.getState();
},
onChange(state) {
this.setState(state);
return mergeOptions(
PieceStore.getState(),
UserStore.getState()
);
},
componentDidMount() {
PieceStore.listen(this.onChange);
PieceActions.fetchOne(this.props.params.pieceId);
UserStore.listen(this.onChange);
},
componentWillUnmount() {
@ -42,10 +59,20 @@ let PieceContainer = React.createClass({
// as it will otherwise display wrong/old data once the user loads
// the piece detail a second time
PieceActions.updatePiece({});
PieceStore.unlisten(this.onChange);
UserStore.unlisten(this.onChange);
},
componentWillReceiveProps(nextProps) {
if(this.props.params.pieceId !== nextProps.params.pieceId) {
PieceActions.updatePiece({});
PieceActions.fetchOne(nextProps.params.pieceId);
}
},
onChange(state) {
this.setState(state);
},
loadPiece() {
PieceActions.fetchOne(this.props.params.pieceId);
@ -53,10 +80,29 @@ let PieceContainer = React.createClass({
render() {
if('title' in this.state.piece) {
let artistName = this.state.currentUser.is_jury ?
<span className="glyphicon glyphicon-eye-close" aria-hidden="true"/> : this.state.piece.artist_name;
return (
<Piece
piece={this.state.piece}
loadPiece={this.loadPiece}>
loadPiece={this.loadPiece}
header={
<div className="ascribe-detail-header">
<NavigationHeader
piece={this.state.piece}
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) } />
<hr/>
</div>
}
subheader={
<PrizePieceRatings
piece={this.state.piece}
currentUser={this.state.currentUser}/>
}>
<PrizePieceDetails piece={this.state.piece}/>
</Piece>
);
@ -70,23 +116,55 @@ let PieceContainer = React.createClass({
}
});
let PrizePieceDetails = React.createClass({
let NavigationHeader = React.createClass({
propTypes: {
piece: React.PropTypes.object
piece: React.PropTypes.object,
currentUser: React.PropTypes.object
},
render() {
if (this.props.currentUser && this.props.piece.navigation) {
let nav = this.props.piece.navigation;
return (
<div style={{marginBottom: '1em'}}>
<div className="row no-margin">
<Link className="disable-select" to='piece' params={{pieceId: nav.prev_index ? nav.prev_index : this.props.piece.id}}>
<span className="glyphicon glyphicon-chevron-left pull-left link-ascribe" aria-hidden="true">
Previous
</span>
</Link>
<Link className="disable-select" to='piece' params={{pieceId: nav.next_index ? nav.next_index : this.props.piece.id}}>
<span className="pull-right link-ascribe">
Next
<span className="glyphicon glyphicon-chevron-right" aria-hidden="true"></span>
</span>
</Link>
</div>
</div>
);
}
return null;
}
});
let PrizePieceRatings = React.createClass({
propTypes: {
piece: React.PropTypes.object,
currentUser: React.PropTypes.object
},
getInitialState() {
return PrizeRatingStore.getState();
},
onChange(state) {
this.setState(state);
return mergeOptions(
PieceListStore.getState(),
PrizeRatingStore.getState()
);
},
componentDidMount() {
PrizeRatingStore.listen(this.onChange);
PrizeRatingActions.fetchOne(this.props.piece.id);
PieceListStore.listen(this.onChange);
},
componentWillUnmount() {
@ -96,11 +174,96 @@ let PrizePieceDetails = React.createClass({
// the piece detail a second time
PrizeRatingActions.updateRating({});
PrizeRatingStore.unlisten(this.onChange);
PieceListStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
if (this.refs.rating) {
this.refs.rating.state.ratingCache = {
pos: this.refs.rating.state.pos,
rating: this.state.currentRating,
caption: this.refs.rating.props.caption,
name: this.refs.rating.props.name
};
}
},
onRatingClick(event, args) {
event.preventDefault();
PrizeRatingActions.createRating(this.props.piece.id, args.rating);
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)
);
},
render(){
if (this.props.currentUser && this.props.currentUser.is_jury) {
return (
<CollapsibleParagraph
title="Rating"
show={true}
defaultExpanded={true}>
<div style={{marginLeft: '1.5em', marginBottom: '1em'}}>
<StarRating
ref='rating'
name="prize-rating"
caption=""
step={1}
size='md'
rating={this.state.currentRating}
onRatingClick={this.onRatingClick}
ratingAmount={5} />
</div>
<PersonalNote
piece={this.props.piece}
currentUser={this.props.currentUser}/>
</CollapsibleParagraph>);
}
return null;
}
});
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.username && true || false) {
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.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 nul
}
});
let PrizePieceDetails = React.createClass({
propTypes: {
piece: React.PropTypes.object
},
render() {

View File

@ -3,28 +3,39 @@
import React from 'react';
import Router from 'react-router';
import PrizeActions from '../actions/prize_actions';
import PrizeStore from '../stores/prize_store';
import ButtonLink from 'react-router-bootstrap/lib/ButtonLink';
import ButtonGroup from 'react-bootstrap/lib/ButtonGroup';
import UserStore from '../../../../stores/user_store';
import UserActions from '../../../../actions/user_actions';
import { mergeOptions } from '../../../../utils/general_utils';
let Landing = React.createClass({
mixins: [Router.Navigation],
getInitialState() {
return UserStore.getState();
return mergeOptions(
PrizeStore.getState(),
UserStore.getState()
);
},
componentDidMount() {
UserStore.listen(this.onChange);
UserActions.fetchCurrentUser();
PrizeStore.listen(this.onChange);
PrizeActions.fetchPrize();
},
componentWillUnmount() {
UserStore.unlisten(this.onChange);
PrizeStore.unlisten(this.onChange);
},
onChange(state) {
@ -37,27 +48,61 @@ let Landing = React.createClass({
}
},
getButtons() {
if (this.state.prize && this.state.prize.active){
return (
<ButtonGroup className="enter" bsSize="large" vertical>
<ButtonLink to="signup">
Sign up to submit
</ButtonLink>
<p>
or, already an ascribe user?
</p>
<ButtonLink to="login">
Log in to submit
</ButtonLink>
</ButtonGroup>
);
}
return (
<ButtonGroup className="enter" bsSize="large" vertical>
<a className="btn btn-default" href="https://www.ascribe.io/app/signup">
Sign up to ascribe
</a>
<p>
or, already an ascribe user?
</p>
<ButtonLink to="login">
Log in
</ButtonLink>
</ButtonGroup>
);
},
getTitle() {
if (this.state.prize && this.state.prize.active){
return (
<p>
This is the submission page for Sluice_screens ↄc Prize 2015.
</p>
);
}
return (
<p>
Submissions for Sluice_screens ↄc Prize 2015 are now closed.
</p>
);
},
render() {
return (
<div className="container">
<div className="row">
<div className="col-xs-12 wp-landing-wrapper">
<h1>Sluice_screens ↄc Prize 2015</h1>
<p>
This is the submission page for Sluice_screens ↄc Prize 2015.
</p>
<ButtonGroup className="enter" bsSize="large" vertical>
<ButtonLink to="signup">
Sign up to submit
</ButtonLink>
<p>
or, already an ascribe user?
</p>
<ButtonLink to="login">
Log in to submit
</ButtonLink>
</ButtonGroup>
{this.getTitle()}
{this.getButtons()}
</div>
</div>
</div>

View File

@ -3,20 +3,47 @@
import React from 'react';
import PieceList from '../../../piece_list';
import ButtonLink from 'react-router-bootstrap/lib/ButtonLink';
import PrizeActions from '../actions/prize_actions';
import PrizeStore from '../stores/prize_store';
import ButtonLink from 'react-router-bootstrap/lib/ButtonLink';
import AccordionListItemPrize from './ascribe_accordion_list/accordion_list_item_prize';
let PrizePieceList = React.createClass({
getInitialState() {
return PrizeStore.getState();
},
componentDidMount() {
PrizeStore.listen(this.onChange);
PrizeActions.fetchPrize();
},
componentWillUnmount() {
PrizeStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
},
getButtonSubmit() {
if (this.state.prize && this.state.prize.active){
return (
<ButtonLink to="register_piece">
Submit to prize
</ButtonLink>
);
}
return null;
},
render() {
return (
<div>
<PieceList
redirectTo="register_piece"
customSubmitButton={
<ButtonLink to="register_piece">
Submit to prize
</ButtonLink>
}/>
accordionListItemType={AccordionListItemPrize}
customSubmitButton={this.getButtonSubmit()}/>
</div>
);
}

View File

@ -16,7 +16,10 @@ function getApiUrls(subdomain) {
'jury_activate': AppPrizeConstants.prizeApiEndpoint + subdomain + '/jury/${email}/activate/',
'jury_resend': AppPrizeConstants.prizeApiEndpoint + subdomain + '/jury/${email}/resend/',
'ratings': AppPrizeConstants.prizeApiEndpoint + subdomain + '/ratings/',
'rating': AppPrizeConstants.prizeApiEndpoint + subdomain + '/ratings/${piece_id}/'
'rating': AppPrizeConstants.prizeApiEndpoint + subdomain + '/ratings/${piece_id}/',
'notes': AppPrizeConstants.prizeApiEndpoint + subdomain + '/notes/',
'note': AppPrizeConstants.prizeApiEndpoint + subdomain + '/notes/${piece_id}/'
};
}

View File

@ -13,7 +13,7 @@ let PrizeRatingFetcher = {
},
rate(pieceId, rating) {
return requests.post('ratings', {body: {'piece_id': pieceId, 'rating': rating}});
return requests.post('ratings', {body: {'piece_id': pieceId, 'value': rating}});
}
};

View File

@ -386,5 +386,26 @@ 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;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.link-ascribe {
color: #666;
&:hover {
color: #000;
}
}