Merge branch 'AD-456-ikonotv-branded-page-for-registra' into AD-957-custom-upload-button-for-contract

This commit is contained in:
Tim Daubenschütz 2015-09-15 13:56:31 +02:00
commit 497a330e1a
21 changed files with 366 additions and 174 deletions

View File

@ -1,47 +0,0 @@
'use strict';
import alt from '../alt';
import OwnershipFetcher from '../fetchers/ownership_fetcher';
class ContractActions {
constructor() {
this.generateActions(
'updateContract',
'flushContract'
);
}
fetchContract(email) {
if(email.match(/.+\@.+\..+/)) {
OwnershipFetcher.fetchContract(email)
.then((contracts) => {
if (contracts && contracts.length > 0) {
this.actions.updateContract({
contractKey: contracts[0].s3Key,
contractUrl: contracts[0].s3Url,
contractEmail: email
});
}
else {
this.actions.updateContract({
contractKey: null,
contractUrl: null,
contractEmail: null
});
}
})
.catch((err) => {
console.logGlobal(err);
this.actions.updateContract({
contractKey: null,
contractUrl: null,
contractEmail: null
});
});
}
}
}
export default alt.createActions(ContractActions);

View File

@ -0,0 +1,94 @@
'use strict';
import alt from '../alt';
import Q from 'q';
import OwnershipFetcher from '../fetchers/ownership_fetcher';
import ContractListActions from './contract_list_actions';
class ContractAgreementListActions {
constructor() {
this.generateActions(
'updateContractAgreementList',
'flushContractAgreementList'
);
}
fetchContractAgreementList(issuer, accepted, pending) {
return Q.Promise((resolve, reject) => {
this.actions.updateContractAgreementList(null);
OwnershipFetcher.fetchContractAgreementList(issuer, accepted, pending)
.then((contractAgreementList) => {
if (contractAgreementList.count > 0) {
this.actions.updateContractAgreementList(contractAgreementList.results);
resolve(contractAgreementList.results);
}
else{
resolve(null);
}
})
.catch((err) => {
console.logGlobal(err);
reject(err);
});
}
);
}
fetchAvailableContractAgreementList(issuer){
return Q.Promise((resolve, reject) => {
this.actions.fetchContractAgreementList(issuer, true, null)
.then((contractAgreementListAccepted) => {
if (!contractAgreementListAccepted) {
// fetch pending agreements if no accepted ones
return this.actions.fetchContractAgreementList(issuer, null, true);
}
else {
resolve(contractAgreementListAccepted);
}
}).then((contractAgreementListPending) => {
resolve(contractAgreementListPending);
}).catch((err) => {
console.logGlobal(err);
reject(err);
});
});
}
createContractAgreementFromPublicContract(issuer){
ContractListActions.fetchContractList(null, null, issuer)
.then((publicContract) => {
// create an agreement with the public contract if there is one
if (publicContract && publicContract.length > 0) {
return this.actions.createContractAgreement(null, publicContract[0]);
}
else {
/*
contractAgreementList in the store is already set to null;
*/
}
}).then((publicContracAgreement) => {
if (publicContracAgreement) {
this.actions.updateContractAgreementList([publicContracAgreement]);
}
}).catch((err) => {
console.logGlobal(err);
});
}
createContractAgreement(issuer, contract){
return Q.Promise((resolve, reject) => {
OwnershipFetcher.createContractAgreement(issuer, contract).then(
(contractAgreement) => {
resolve(contractAgreement);
}
).catch((err) => {
console.logGlobal(err);
reject(err);
});
});
}
}
export default alt.createActions(ContractAgreementListActions);

View File

@ -12,15 +12,19 @@ class ContractListActions {
);
}
fetchContractList(isActive) {
OwnershipFetcher.fetchContractList(isActive)
.then((contracts) => {
this.actions.updateContractList(contracts.results);
})
.catch((err) => {
console.logGlobal(err);
this.actions.updateContractList([]);
});
fetchContractList(isActive, isPublic, issuer) {
return Q.Promise((resolve, reject) => {
OwnershipFetcher.fetchContractList(isActive, isPublic, issuer)
.then((contracts) => {
this.actions.updateContractList(contracts.results);
resolve(contracts.results);
})
.catch((err) => {
console.logGlobal(err);
this.actions.updateContractList([]);
reject(err);
});
});
}

View File

@ -20,21 +20,28 @@ let AclProxy = React.createClass({
show: React.PropTypes.bool
},
render() {
if(this.props.show) {
getChildren() {
if (React.Children.count(this.props.children) > 1){
/*
This might ruin styles for header items in the navbar etc
*/
return (
<span>
{this.props.children}
</span>
);
}
/* can only do this when there is only 1 child, but will preserve styles */
return this.props.children;
},
render() {
if(this.props.show) {
return this.getChildren();
} else {
if(this.props.aclObject) {
if(this.props.aclObject[this.props.aclName]) {
return (
<span>
{this.props.children}
</span>
);
return this.getChildren();
} else {
/* if(typeof this.props.aclObject[this.props.aclName] === 'undefined') {
console.warn('The aclName you\'re filtering for was not present (or undefined) in the aclObject.');

View File

@ -14,7 +14,7 @@ import CoaActions from '../../actions/coa_actions';
import CoaStore from '../../stores/coa_store';
import PieceListActions from '../../actions/piece_list_actions';
import PieceListStore from '../../stores/piece_list_store';
import EditionListActions from '../../actions/edition_list_actions';;
import EditionListActions from '../../actions/edition_list_actions';
import HistoryIterator from './history_iterator';

View File

@ -35,7 +35,7 @@ let ContractAgreementForm = React.createClass({
componentDidMount() {
ContractListStore.listen(this.onChange);
ContractListActions.fetchContractList({is_active: 'True'});
ContractListActions.fetchContractList({is_active: true});
},
componentWillUnmount() {

View File

@ -45,7 +45,7 @@ let CreateContractForm = React.createClass({
},
handleCreateSuccess(response) {
ContractListActions.fetchContractList({is_active: 'True'});
ContractListActions.fetchContractList({is_active: true});
let notification = new GlobalNotificationModel(getLangText('Contract %s successfully created', response.name), 'success', 5000);
GlobalNotificationActions.appendGlobalNotification(notification);
this.refs.form.reset();

View File

@ -12,11 +12,12 @@ import InputTextAreaToggable from './input_textarea_toggable';
import InputDate from './input_date';
import InputCheckbox from './input_checkbox';
import ContractStore from '../../stores/contract_store';
import ContractActions from '../../actions/contract_actions';
import ContractAgreementListStore from '../../stores/contract_agreement_list_store';
import ContractAgreementListActions from '../../actions/contract_agreement_list_actions';
import AppConstants from '../../constants/application_constants';
import { mergeOptions } from '../../utils/general_utils';
import { getLangText } from '../../utils/lang_utils';
@ -48,40 +49,74 @@ let LoanForm = React.createClass({
},
getInitialState() {
return ContractStore.getState();
return ContractAgreementListStore.getState();
},
componentDidMount() {
ContractStore.listen(this.onChange);
ContractActions.flushContract.defer();
ContractAgreementListStore.listen(this.onChange);
this.getContractAgreementsOrCreatePublic(this.props.email);
},
componentWillReceiveProps(nextProps) {
// however, it can also be that at the time the component is mounting,
// the email is not defined (because it's asynchronously fetched from the server).
// Then we need to update it as soon as it is included into LoanForm's props.
if(nextProps && nextProps.email) {
this.getContractAgreementsOrCreatePublic(nextProps.email);
}
},
componentWillUnmount() {
ContractStore.unlisten(this.onChange);
ContractAgreementListStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
},
getContractAgreementsOrCreatePublic(email){
ContractAgreementListActions.flushContractAgreementList();
if (email) {
ContractAgreementListActions.fetchAvailableContractAgreementList(email).then(
(contractAgreementList) => {
if (!contractAgreementList) {
ContractAgreementListActions.createContractAgreementFromPublicContract(email);
}
}
);
}
},
getFormData(){
return this.props.id;
return mergeOptions(
this.props.id,
this.getContractAgreementId()
);
},
handleOnChange(event) {
// event.target.value is the submitted email of the loanee
if(event && event.target && event.target.value && event.target.value.match(/.*@.*/)) {
ContractActions.fetchContract(event.target.value);
if(event && event.target && event.target.value && event.target.value.match(/.*@.*\..*/)) {
this.getContractAgreementsOrCreatePublic(event.target.value);
} else {
ContractActions.flushContract();
ContractAgreementListActions.flushContractAgreementList();
}
},
getContractAgreementId() {
if (this.state.contractAgreementList && this.state.contractAgreementList.length > 0) {
return {'contract_agreement_id': this.state.contractAgreementList[0].id};
}
return null;
},
getContractCheckbox() {
if(this.state.contractKey && this.state.contractUrl) {
if(this.state.contractAgreementList && this.state.contractAgreementList.length > 0) {
// we need to define a key on the InputCheckboxes as otherwise
// react is not rerendering them on a store switch and is keeping
// the default value of the component (which is in that case true)
let contract = this.state.contractAgreementList[0].contract;
return (
<Property
name="terms"
@ -92,8 +127,8 @@ let LoanForm = React.createClass({
defaultChecked={false}>
<span>
{getLangText('I agree to the')}&nbsp;
<a href={this.state.contractUrl} target="_blank">
{getLangText('terms of')} {this.state.contractEmail}
<a href={contract.blob.url_safe} target="_blank">
{getLangText('terms of ')} {contract.issuer}
</a>
</span>
</InputCheckbox>
@ -157,8 +192,8 @@ let LoanForm = React.createClass({
<Property
name='loanee'
label={getLangText('Loanee Email')}
onChange={this.handleOnChange}
editable={!this.props.email}
onBlur={this.handleOnChange}
overrideForm={!!this.props.email}>
<input
value={this.props.email}

View File

@ -28,7 +28,7 @@ let ContractSettings = React.createClass({
componentDidMount() {
ContractListStore.listen(this.onChange);
ContractListActions.fetchContractList({is_active: 'True'});
ContractListActions.fetchContractList({is_active: true});
},
componentWillUnmount() {
@ -44,7 +44,7 @@ let ContractSettings = React.createClass({
contract.is_public = true;
ContractListActions.changeContract(contract)
.then(() => {
ContractListActions.fetchContractList({is_active: 'True'});
ContractListActions.fetchContractList({is_active: true});
let notification = getLangText('Contract %s is now public', contract.name);
notification = new GlobalNotificationModel(notification, 'success', 4000);
GlobalNotificationActions.appendGlobalNotification(notification);
@ -60,7 +60,7 @@ let ContractSettings = React.createClass({
return () => {
ContractListActions.removeContract(contract.id)
.then((response) => {
ContractListActions.fetchContractList({is_active: 'True'});
ContractListActions.fetchContractList({is_active: true});
let notification = new GlobalNotificationModel(response.notification, 'success', 4000);
GlobalNotificationActions.appendGlobalNotification(notification);
})

View File

@ -141,7 +141,7 @@ let Header = React.createClass({
<MenuItemLink eventKey="3" to="logout">{getLangText('Log out')}</MenuItemLink>
</DropdownButton>
);
navRoutesLinks = <NavRoutesLinks routes={this.props.routes} navbar right/>;
navRoutesLinks = <NavRoutesLinks routes={this.props.routes} userAcl={this.state.currentUser.acl} navbar right/>;
}
else {
account = <NavItemLink to="login">{getLangText('LOGIN')}</NavItemLink>;

View File

@ -3,53 +3,80 @@
import React from 'react';
import Nav from 'react-bootstrap/lib/Nav';
import DropdownButton from 'react-bootstrap/lib/DropdownButton';
import MenuItemLink from 'react-router-bootstrap/lib/MenuItemLink';
import NavItemLink from 'react-router-bootstrap/lib/NavItemLink';
import NavRoutesLinksLink from './nav_routes_links_link';
import AclProxy from './acl_proxy';
import { sanitizeList } from '../utils/general_utils';
let NavRoutesLinks = React.createClass({
propTypes: {
routes: React.PropTypes.element
routes: React.PropTypes.element,
userAcl: React.PropTypes.object
},
extractLinksFromRoutes(node, i) {
/**
* This method generales a bunch of react-bootstrap specific links
* from the routes we defined in one of the specific routes.js file
*
* We can define a headerTitle as well as a aclName and according to that the
* link will be created for a specific user
* @param {ReactElement} node Starts at the very top of a routes files root
* @param {object} userAcl ACL object we use throughout the whole app
* @param {number} i Depth of the route in comparison to the root
* @return {Array} Array of ReactElements that can be displayed to the user
*/
extractLinksFromRoutes(node, userAcl, i) {
if(!node) {
return;
}
node = node.props;
let links = node.props.children.map((child, j) => {
let childrenFn = null;
let { aclName, headerTitle, name, children } = child.props;
let links = node.children.map((child, j) => {
// If the node has children that could be rendered, then we want
// to execute this function again with the child as the root
//
// Otherwise we'll just pass childrenFn as false
if(child.props.children && child.props.children.length > 0) {
childrenFn = this.extractLinksFromRoutes(child, userAcl, i++);
}
// check if this a candidate for a link generation
if(child.props.headerTitle && typeof child.props.headerTitle === 'string') {
// also check if it is a candidate for generating a dropdown menu
if(child.props.children && child.props.children.length > 0) {
// We validate if the user has set the title correctly,
// otherwise we're not going to render his route
if(headerTitle && typeof headerTitle === 'string') {
// if there is an aclName present on the route definition,
// we evaluate it against the user's acl
if(aclName && typeof aclName !== 'undefined') {
return (
<DropdownButton title={child.props.headerTitle} key={j}>
{this.extractLinksFromRoutes(child, i++)}
</DropdownButton>
);
} else if(i === 1) {
// if the node's child is actually a node of level one (a child of a node), we're
// returning a DropdownButton matching MenuItemLink
return (
<MenuItemLink to={child.props.name} key={j}>{child.props.headerTitle}</MenuItemLink>
);
} else if(i === 0) {
return (
<NavItemLink to={child.props.name} key={j}>{child.props.headerTitle}</NavItemLink>
<AclProxy
key={j}
aclName={aclName}
aclObject={this.props.userAcl}>
<NavRoutesLinksLink
headerTitle={headerTitle}
routeName={name}
depth={i}
children={childrenFn}/>
</AclProxy>
);
} else {
return null;
return (
<NavRoutesLinksLink
key={j}
headerTitle={headerTitle}
routeName={name}
depth={i}
children={childrenFn}/>
);
}
} else {
return null;
}
});
// remove all nulls from the list of generated links
@ -57,9 +84,11 @@ let NavRoutesLinks = React.createClass({
},
render() {
let {routes, userAcl} = this.props;
return (
<Nav {...this.props}>
{this.extractLinksFromRoutes(this.props.routes, 0)}
{this.extractLinksFromRoutes(routes, userAcl, 0)}
</Nav>
);
}

View File

@ -0,0 +1,51 @@
'use strict';
import React from 'react';
import DropdownButton from 'react-bootstrap/lib/DropdownButton';
import MenuItemLink from 'react-router-bootstrap/lib/MenuItemLink';
import NavItemLink from 'react-router-bootstrap/lib/NavItemLink';
let NavRoutesLinksLink = React.createClass({
propTypes: {
headerTitle: React.PropTypes.string,
routeName: React.PropTypes.string,
children: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.element),
React.PropTypes.element
]),
depth: React.PropTypes.number
},
render() {
let { children, headerTitle, depth, routeName } = this.props;
// if the route has children, we're returning a DropdownButton that will get filled
// with MenuItemLinks
if(children) {
return (
<DropdownButton title={headerTitle}>
{children}
</DropdownButton>
);
} else {
if(depth === 1) {
// if the node's child is actually a node of level one (a child of a node), we're
// returning a DropdownButton matching MenuItemLink
return (
<MenuItemLink to={routeName}>{headerTitle}</MenuItemLink>
);
} else if(depth === 0) {
return (
<NavItemLink to={routeName}>{headerTitle}</NavItemLink>
);
} else {
return null;
}
}
}
});
export default NavRoutesLinksLink;

View File

@ -222,21 +222,7 @@ let CylandRegisterPiece = React.createClass({
showStartDate={false}
showEndDate={false}
showPersonalMessage={false}
handleSuccess={this.handleLoanSuccess}>
<Property
name="terms"
className="ascribe-settings-property-collapsible-toggle"
style={{paddingBottom: 0}}>
<InputCheckbox>
<span>
{' ' + getLangText('I agree to the Terms of Service of Cyland Archive') + ' '}
(<a href="https://s3-us-west-2.amazonaws.com/ascribe0/whitelabel/cyland/terms_and_contract.pdf" target="_blank" style={{fontSize: '0.9em', color: 'rgba(0,0,0,0.7)'}}>
{getLangText('read')}
</a>)
</span>
</InputCheckbox>
</Property>
</LoanForm>
handleSuccess={this.handleLoanSuccess} />
</Col>
</Row>
</div>

View File

@ -50,21 +50,7 @@ let IkonotvSubmitButton = React.createClass({
enddate={enddate}
gallery="IkonoTV archive"
showPersonalMessage={false}
handleSuccess={this.props.handleSuccess}>
<Property
name="terms"
className="ascribe-settings-property-collapsible-toggle"
style={{paddingBottom: 0}}>
<InputCheckbox>
<span>
{' ' + getLangText('I agree to the Terms of Service of IkonoTV Archive') + ' '}
(<a href="https://s3-us-west-2.amazonaws.com/ascribe0/whitelabel/ikonotv/ikono-tos.pdf" target="_blank" style={{fontSize: '0.9em', color: 'rgba(0,0,0,0.7)'}}>
{getLangText('read')}
</a>)
</span>
</InputCheckbox>
</Property>
</LoanForm>
handleSuccess={this.props.handleSuccess} />
</ModalWrapper>
);

View File

@ -8,7 +8,6 @@ import UserStore from '../../../../../stores/user_store';
import IkonotvAccordionListItem from './ascribe_accordion_list/ikonotv_accordion_list_item';
let IkonotvPieceList = React.createClass({
getInitialState() {
return UserStore.getState();

View File

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

View File

@ -72,7 +72,7 @@ let ROUTES = {
<Route name="logout" path="logout" handler={LogoutContainer} />
<Route name="signup" path="signup" handler={SignupContainer} />
<Route name="password_reset" path="password_reset" handler={PasswordResetContainer} />
<Route name="request_loan" path="request_loan" handler={IkonotvRequestLoan} headerTitle="SEND NEW CONTRACT" />
<Route name="request_loan" path="request_loan" handler={IkonotvRequestLoan} headerTitle="SEND NEW CONTRACT" aclName="acl_send_contract" />
<Route name="register_piece" path="register_piece" handler={RegisterPiece} headerTitle="+ NEW WORK"/>
<Route name="pieces" path="collection" handler={IkonotvPieceList} headerTitle="COLLECTION"/>
<Route name="piece" path="pieces/:pieceId" handler={IkonotvPieceContainer} />

View File

@ -15,8 +15,33 @@ let OwnershipFetcher = {
/**
* Fetch the contracts of the logged-in user from the API.
*/
fetchContractList(isActive){
return requests.get(ApiUrls.ownership_contract_list, isActive);
fetchContractList(isActive, isPublic, issuer){
let queryParams = {
isActive,
isPublic,
issuer
};
return requests.get(ApiUrls.ownership_contract_list, queryParams);
},
/**
* Create a contractagreement between the logged-in user and the email from the API with contract.
*/
createContractAgreement(signee, contractObj){
return requests.post(ApiUrls.ownership_contract_agreements, { body: {signee: signee, contract: contractObj.id }});
},
/**
* Fetch the contractagreement between the logged-in user and the email from the API.
*/
fetchContractAgreementList(issuer, accepted, pending) {
let queryParams = {
issuer,
accepted,
pending
};
return requests.get(ApiUrls.ownership_contract_agreements, queryParams);
},
fetchLoanPieceRequestList(){

View File

@ -0,0 +1,22 @@
'use strict';
import alt from '../alt';
import ContractAgreementListActions from '../actions/contract_agreement_list_actions';
class ContractAgreementListStore {
constructor() {
this.contractAgreementList = null;
this.bindActions(ContractAgreementListActions);
}
onUpdateContractAgreementList(contractAgreementList) {
this.contractAgreementList = contractAgreementList;
}
onFlushContractAgreementList() {
this.contractAgreementList = null;
}
}
export default alt.createStore(ContractAgreementListStore, 'ContractAgreementListStore');

View File

@ -1,28 +0,0 @@
'use strict';
import alt from '../alt';
import ContractActions from '../actions/contract_actions';
class ContractStore {
constructor() {
this.contractKey = null;
this.contractUrl = null;
this.contractEmail = null;
this.bindActions(ContractActions);
}
onUpdateContract({contractKey, contractUrl, contractEmail}) {
this.contractKey = contractKey;
this.contractUrl = contractUrl;
this.contractEmail = contractEmail;
}
onFlushContract() {
this.contractKey = null;
this.contractUrl = null;
this.contractEmail = null;
}
}
export default alt.createStore(ContractStore, 'ContractStore');

View File

@ -122,6 +122,34 @@ hr {
height: 60px;
}
//http://stackoverflow.com/questions/22228239/bootstrap-navbar-static-top-menu-breaks-on-two-lines
@media (max-width: 990px) {
.navbar-header {
float: none;
}
.navbar-toggle {
display: block;
}
.navbar-collapse {
border-top: 1px solid transparent;
box-shadow: inset 0 1px 0 rgba(255,255,255,0.1);
}
.navbar-collapse.collapse {
display: none!important;
}
.navbar-nav {
float: none!important;
margin: 7.5px -15px;
}
.navbar-nav>li {
float: none;
}
.navbar-nav>li>a {
padding-top: 10px;
padding-bottom: 10px;
}
}
.truncate {
overflow: hidden;
text-overflow: ellipsis;