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

Merge branch 'AD-456-ikonotv-branded-page-for-registra' of bitbucket.org:ascribe/onion into AD-456-ikonotv-branded-page-for-registra

This commit is contained in:
Tim Daubenschütz 2015-09-15 13:53:29 +02:00
commit 891b2eee9e
18 changed files with 337 additions and 172 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,14 +12,18 @@ class ContractListActions {
); );
} }
fetchContractList(isActive) { fetchContractList(isActive, isPublic, issuer) {
OwnershipFetcher.fetchContractList(isActive) return Q.Promise((resolve, reject) => {
OwnershipFetcher.fetchContractList(isActive, isPublic, issuer)
.then((contracts) => { .then((contracts) => {
this.actions.updateContractList(contracts.results); this.actions.updateContractList(contracts.results);
resolve(contracts.results);
}) })
.catch((err) => { .catch((err) => {
console.logGlobal(err); console.logGlobal(err);
this.actions.updateContractList([]); this.actions.updateContractList([]);
reject(err);
});
}); });
} }

View File

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

View File

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

View File

@ -50,7 +50,7 @@ let CreateContractForm = React.createClass({
}, },
handleCreateSuccess(response) { 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); let notification = new GlobalNotificationModel(getLangText('Contract %s successfully created', response.name), 'success', 5000);
GlobalNotificationActions.appendGlobalNotification(notification); GlobalNotificationActions.appendGlobalNotification(notification);
this.refs.form.reset(); this.refs.form.reset();

View File

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

View File

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

View File

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

View File

@ -3,53 +3,80 @@
import React from 'react'; import React from 'react';
import Nav from 'react-bootstrap/lib/Nav'; import Nav from 'react-bootstrap/lib/Nav';
import DropdownButton from 'react-bootstrap/lib/DropdownButton';
import MenuItemLink from 'react-router-bootstrap/lib/MenuItemLink'; import NavRoutesLinksLink from './nav_routes_links_link';
import NavItemLink from 'react-router-bootstrap/lib/NavItemLink';
import AclProxy from './acl_proxy';
import { sanitizeList } from '../utils/general_utils'; import { sanitizeList } from '../utils/general_utils';
let NavRoutesLinks = React.createClass({ let NavRoutesLinks = React.createClass({
propTypes: { 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) { if(!node) {
return; 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
// check if this a candidate for a link generation //
if(child.props.headerTitle && typeof child.props.headerTitle === 'string') { // Otherwise we'll just pass childrenFn as false
// also check if it is a candidate for generating a dropdown menu
if(child.props.children && child.props.children.length > 0) { if(child.props.children && child.props.children.length > 0) {
childrenFn = this.extractLinksFromRoutes(child, userAcl, i++);
}
// 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 ( return (
<DropdownButton title={child.props.headerTitle} key={j}> <AclProxy
{this.extractLinksFromRoutes(child, i++)} key={j}
</DropdownButton> aclName={aclName}
); aclObject={this.props.userAcl}>
} else if(i === 1) { <NavRoutesLinksLink
// if the node's child is actually a node of level one (a child of a node), we're headerTitle={headerTitle}
// returning a DropdownButton matching MenuItemLink routeName={name}
return ( depth={i}
<MenuItemLink to={child.props.name} key={j}>{child.props.headerTitle}</MenuItemLink> children={childrenFn}/>
); </AclProxy>
} else if(i === 0) {
return (
<NavItemLink to={child.props.name} key={j}>{child.props.headerTitle}</NavItemLink>
); );
} else { } else {
return null; return (
<NavRoutesLinksLink
key={j}
headerTitle={headerTitle}
routeName={name}
depth={i}
children={childrenFn}/>
);
} }
} else { } else {
return null; return null;
} }
}); });
// remove all nulls from the list of generated links // remove all nulls from the list of generated links
@ -57,9 +84,11 @@ let NavRoutesLinks = React.createClass({
}, },
render() { render() {
let {routes, userAcl} = this.props;
return ( return (
<Nav {...this.props}> <Nav {...this.props}>
{this.extractLinksFromRoutes(this.props.routes, 0)} {this.extractLinksFromRoutes(routes, userAcl, 0)}
</Nav> </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} showStartDate={false}
showEndDate={false} showEndDate={false}
showPersonalMessage={false} showPersonalMessage={false}
handleSuccess={this.handleLoanSuccess}> 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>
</Col> </Col>
</Row> </Row>
</div> </div>

View File

@ -50,21 +50,7 @@ let IkonotvSubmitButton = React.createClass({
enddate={enddate} enddate={enddate}
gallery="IkonoTV archive" gallery="IkonoTV archive"
showPersonalMessage={false} showPersonalMessage={false}
handleSuccess={this.props.handleSuccess}> 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>
</ModalWrapper> </ModalWrapper>
); );

View File

@ -14,7 +14,8 @@ function getWalletApiUrls(subdomain) {
else if (subdomain === 'ikonotv'){ else if (subdomain === 'ikonotv'){
return { return {
'pieces_list': walletConstants.walletApiEndpoint + subdomain + '/pieces/', '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 {}; return {};

View File

@ -72,7 +72,7 @@ let ROUTES = {
<Route name="logout" path="logout" handler={LogoutContainer} /> <Route name="logout" path="logout" handler={LogoutContainer} />
<Route name="signup" path="signup" handler={SignupContainer} /> <Route name="signup" path="signup" handler={SignupContainer} />
<Route name="password_reset" path="password_reset" handler={PasswordResetContainer} /> <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="register_piece" path="register_piece" handler={RegisterPiece} headerTitle="+ NEW WORK"/>
<Route name="pieces" path="collection" handler={IkonotvPieceList} headerTitle="COLLECTION"/> <Route name="pieces" path="collection" handler={IkonotvPieceList} headerTitle="COLLECTION"/>
<Route name="piece" path="pieces/:pieceId" handler={IkonotvPieceContainer} /> <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. * Fetch the contracts of the logged-in user from the API.
*/ */
fetchContractList(isActive){ fetchContractList(isActive, isPublic, issuer){
return requests.get(ApiUrls.ownership_contract_list, isActive); 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(){ 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');