diff --git a/js/actions/contract_actions.js b/js/actions/contract_actions.js deleted file mode 100644 index d1bf1432..00000000 --- a/js/actions/contract_actions.js +++ /dev/null @@ -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); diff --git a/js/actions/contract_agreement_list_actions.js b/js/actions/contract_agreement_list_actions.js new file mode 100644 index 00000000..86988f47 --- /dev/null +++ b/js/actions/contract_agreement_list_actions.js @@ -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); diff --git a/js/actions/contract_list_actions.js b/js/actions/contract_list_actions.js index aaee33c6..a856fb2b 100644 --- a/js/actions/contract_list_actions.js +++ b/js/actions/contract_list_actions.js @@ -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); + }); + }); } diff --git a/js/components/acl_proxy.js b/js/components/acl_proxy.js index be0d8466..4fc90a9b 100644 --- a/js/components/acl_proxy.js +++ b/js/components/acl_proxy.js @@ -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 ( {this.props.children} ); + } + /* 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 ( - - {this.props.children} - - ); + 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.'); diff --git a/js/components/ascribe_forms/form_contract_agreement.js b/js/components/ascribe_forms/form_contract_agreement.js index 0c20383c..887b99db 100644 --- a/js/components/ascribe_forms/form_contract_agreement.js +++ b/js/components/ascribe_forms/form_contract_agreement.js @@ -35,7 +35,7 @@ let ContractAgreementForm = React.createClass({ componentDidMount() { ContractListStore.listen(this.onChange); - ContractListActions.fetchContractList({is_active: 'True'}); + ContractListActions.fetchContractList({is_active: true}); }, componentWillUnmount() { diff --git a/js/components/ascribe_forms/form_create_contract.js b/js/components/ascribe_forms/form_create_contract.js index 9cf97db7..b275df03 100644 --- a/js/components/ascribe_forms/form_create_contract.js +++ b/js/components/ascribe_forms/form_create_contract.js @@ -50,7 +50,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(); diff --git a/js/components/ascribe_forms/form_loan.js b/js/components/ascribe_forms/form_loan.js index 44a24ad2..dacbae77 100644 --- a/js/components/ascribe_forms/form_loan.js +++ b/js/components/ascribe_forms/form_loan.js @@ -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 ( {getLangText('I agree to the')}  - - {getLangText('terms of')} {this.state.contractEmail} + + {getLangText('terms of ')} {contract.issuer} @@ -157,8 +192,8 @@ let LoanForm = React.createClass({ { - 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); @@ -58,7 +58,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); }) diff --git a/js/components/header.js b/js/components/header.js index 2dca74b2..0309b748 100644 --- a/js/components/header.js +++ b/js/components/header.js @@ -141,7 +141,7 @@ let Header = React.createClass({ {getLangText('Log out')} ); - navRoutesLinks = ; + navRoutesLinks = ; } else { account = {getLangText('LOGIN')}; diff --git a/js/components/nav_routes_links.js b/js/components/nav_routes_links.js index 9a266ba8..d3342cb8 100644 --- a/js/components/nav_routes_links.js +++ b/js/components/nav_routes_links.js @@ -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 ( - - {this.extractLinksFromRoutes(child, i++)} - - ); - } 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 ( - {child.props.headerTitle} - ); - } else if(i === 0) { - return ( - {child.props.headerTitle} + + + ); } else { - return null; + return ( + + ); } } 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 ( ); } diff --git a/js/components/nav_routes_links_link.js b/js/components/nav_routes_links_link.js new file mode 100644 index 00000000..15aff405 --- /dev/null +++ b/js/components/nav_routes_links_link.js @@ -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 ( + + {children} + + ); + } 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 ( + {headerTitle} + ); + } else if(depth === 0) { + return ( + {headerTitle} + ); + } else { + return null; + } + } + } +}); + +export default NavRoutesLinksLink; \ No newline at end of file diff --git a/js/components/whitelabel/wallet/components/cyland/cyland_register_piece.js b/js/components/whitelabel/wallet/components/cyland/cyland_register_piece.js index 3768adab..6e8a77c3 100644 --- a/js/components/whitelabel/wallet/components/cyland/cyland_register_piece.js +++ b/js/components/whitelabel/wallet/components/cyland/cyland_register_piece.js @@ -222,21 +222,7 @@ let CylandRegisterPiece = React.createClass({ showStartDate={false} showEndDate={false} showPersonalMessage={false} - handleSuccess={this.handleLoanSuccess}> - - - - {' ' + getLangText('I agree to the Terms of Service of Cyland Archive') + ' '} - ( - {getLangText('read')} - ) - - - - + handleSuccess={this.handleLoanSuccess} /> diff --git a/js/components/whitelabel/wallet/components/ikonotv/ascribe_buttons/ikonotv_submit_button.js b/js/components/whitelabel/wallet/components/ikonotv/ascribe_buttons/ikonotv_submit_button.js index 41b47a82..4b7f3dbc 100644 --- a/js/components/whitelabel/wallet/components/ikonotv/ascribe_buttons/ikonotv_submit_button.js +++ b/js/components/whitelabel/wallet/components/ikonotv/ascribe_buttons/ikonotv_submit_button.js @@ -50,21 +50,7 @@ let IkonotvSubmitButton = React.createClass({ enddate={enddate} gallery="IkonoTV archive" showPersonalMessage={false} - handleSuccess={this.props.handleSuccess}> - - - - {' ' + getLangText('I agree to the Terms of Service of IkonoTV Archive') + ' '} - ( - {getLangText('read')} - ) - - - - + handleSuccess={this.props.handleSuccess} /> ); diff --git a/js/components/whitelabel/wallet/constants/wallet_api_urls.js b/js/components/whitelabel/wallet/constants/wallet_api_urls.js index c1b101a1..e0d8a862 100644 --- a/js/components/whitelabel/wallet/constants/wallet_api_urls.js +++ b/js/components/whitelabel/wallet/constants/wallet_api_urls.js @@ -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 {}; diff --git a/js/components/whitelabel/wallet/wallet_routes.js b/js/components/whitelabel/wallet/wallet_routes.js index bc9e0e19..25fc1d80 100644 --- a/js/components/whitelabel/wallet/wallet_routes.js +++ b/js/components/whitelabel/wallet/wallet_routes.js @@ -72,7 +72,7 @@ let ROUTES = { - + diff --git a/js/fetchers/ownership_fetcher.js b/js/fetchers/ownership_fetcher.js index c0d32d71..fde66c7d 100644 --- a/js/fetchers/ownership_fetcher.js +++ b/js/fetchers/ownership_fetcher.js @@ -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(){ diff --git a/js/stores/contract_agreement_list_store.js b/js/stores/contract_agreement_list_store.js new file mode 100644 index 00000000..ca1d8e6b --- /dev/null +++ b/js/stores/contract_agreement_list_store.js @@ -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'); diff --git a/js/stores/contract_store.js b/js/stores/contract_store.js deleted file mode 100644 index 623bec6a..00000000 --- a/js/stores/contract_store.js +++ /dev/null @@ -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');