diff --git a/package.json b/package.json index e51572a01d..dbd458eb53 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "serve-favicon": "^2.4.3", "superagent": "^3.8.2", "superagent-bluebird-promise": "^4.1.0", + "uuid": "^3.3.2", "validator": "^9.1.2" }, "devDependencies": { diff --git a/scripts/build-client-js.sh b/scripts/build-client-js.sh index afcbf26811..fce55f6506 100755 --- a/scripts/build-client-js.sh +++ b/scripts/build-client-js.sh @@ -1,5 +1,8 @@ #!/usr/bin/env bash +pushd static/js +mkdir -p editor entity import-entity +popd pushd src/client/controllers cross-env BABEL_ENV="browser" browserify -t [babelify] \ ../entity-editor/controller.js \ @@ -7,23 +10,29 @@ cross-env BABEL_ENV="browser" browserify -t [babelify] \ editor/achievement.js \ editor/editor.js \ entity/entity.js \ + import-entity/import-entity.js \ deletion.js \ index.js \ registrationDetails.js \ revision.js \ search.js \ statistics.js \ + import-entity/recent-imports.js \ + import-entity/discard-import-entity.js \ -p [ factor-bundle \ -o ../../../static/js/entity-editor.js \ -o ../../../static/js/editor/edit.js \ -o ../../../static/js/editor/achievement.js \ -o ../../../static/js/editor/editor.js \ -o ../../../static/js/entity/entity.js \ + -o ../../../static/js/import-entity/import-entity.js \ -o ../../../static/js/deletion.js \ -o ../../../static/js/index.js \ -o ../../../static/js/registrationDetails.js \ -o ../../../static/js/revision.js \ -o ../../../static/js/search.js \ -o ../../../static/js/statistics.js \ + -o ../../../static/js/import-entity/recent-imports.js \ + -o ../../../static/js/import-entity/discard-import-entity.js \ ] > ../../../static/js/bundle.js popd diff --git a/scripts/watch-client-js.sh b/scripts/watch-client-js.sh index e985025705..562a1a24fc 100755 --- a/scripts/watch-client-js.sh +++ b/scripts/watch-client-js.sh @@ -1,5 +1,8 @@ #!/usr/bin/env bash +pushd static/js +mkdir -p editor entity import-entity +popd pushd src/client/controllers cross-env BABEL_ENV="browser" watchify -t [babelify] \ ../entity-editor/controller.js \ @@ -7,23 +10,29 @@ cross-env BABEL_ENV="browser" watchify -t [babelify] \ editor/achievement.js \ editor/editor.js \ entity/entity.js \ + import-entity/import-entity.js \ deletion.js \ index.js \ registrationDetails.js \ revision.js \ search.js \ statistics.js \ + import-entity/recent-imports.js \ + import-entity/discard-import-entity.js \ -p [ factor-bundle \ -o ../../../static/js/entity-editor.js \ -o ../../../static/js/editor/edit.js \ -o ../../../static/js/editor/achievement.js \ -o ../../../static/js/editor/editor.js \ -o ../../../static/js/entity/entity.js \ + -o ../../../static/js/import-entity/import-entity.js \ -o ../../../static/js/deletion.js \ -o ../../../static/js/index.js \ -o ../../../static/js/registrationDetails.js \ -o ../../../static/js/revision.js \ -o ../../../static/js/search.js \ -o ../../../static/js/statistics.js \ + -o ../../../static/js/import-entity/recent-imports.js \ + -o ../../../static/js/import-entity/discard-import-entity.js \ ] -o ../../../static/js/bundle.js -dv popd diff --git a/src/client/components/pages/entities/creator.js b/src/client/components/pages/entities/creator.js index e59d3aa6f4..2f51797480 100644 --- a/src/client/components/pages/entities/creator.js +++ b/src/client/components/pages/entities/creator.js @@ -31,7 +31,7 @@ const {extractAttribute, getTypeAttribute, getEntityUrl} = entityHelper; const {Col, Row} = bootstrap; -function CreatorAttributes({creator}) { +export function CreatorAttributes({creator}) { const type = getTypeAttribute(creator.creatorType).data; const gender = extractAttribute(creator.gender, 'name'); const beginArea = extractAttribute(creator.beginArea, 'name'); diff --git a/src/client/components/pages/entities/edition.js b/src/client/components/pages/entities/edition.js index 37588e03ea..e61e9e1002 100644 --- a/src/client/components/pages/entities/edition.js +++ b/src/client/components/pages/entities/edition.js @@ -34,7 +34,7 @@ const { } = entityHelper; const {Col, Row} = bootstrap; -function EditionAttributes({edition}) { +export function EditionAttributes({edition}) { const status = extractAttribute(edition.editionStatus, 'label'); const format = extractAttribute(edition.editionFormat, 'label'); const pageCount = extractAttribute(edition.pages); diff --git a/src/client/components/pages/entities/image.js b/src/client/components/pages/entities/image.js index 8024a1bc6f..7db045efe9 100644 --- a/src/client/components/pages/entities/image.js +++ b/src/client/components/pages/entities/image.js @@ -46,8 +46,12 @@ function EntityImage({backupIcon, imageUrl}) { } EntityImage.displayName = 'EntityImage'; EntityImage.propTypes = { - backupIcon: PropTypes.string.isRequired, - imageUrl: PropTypes.string.isRequired + backupIcon: PropTypes.string, + imageUrl: PropTypes.string +}; +EntityImage.defaultProps = { + backupIcon: '', + imageUrl: '' }; export default EntityImage; diff --git a/src/client/components/pages/entities/publication.js b/src/client/components/pages/entities/publication.js index 455a177e64..e46a5f9d35 100644 --- a/src/client/components/pages/entities/publication.js +++ b/src/client/components/pages/entities/publication.js @@ -31,7 +31,7 @@ import React from 'react'; const {getTypeAttribute, getEntityUrl} = entityHelper; const {Col, Row} = bootstrap; -function PublicationAttributes({publication}) { +export function PublicationAttributes({publication}) { const type = getTypeAttribute(publication.publicationType).data; return ( diff --git a/src/client/components/pages/entities/publisher.js b/src/client/components/pages/entities/publisher.js index 799a23f8d9..4ea209a1da 100644 --- a/src/client/components/pages/entities/publisher.js +++ b/src/client/components/pages/entities/publisher.js @@ -33,7 +33,7 @@ import React from 'react'; const {extractAttribute, getTypeAttribute, getEntityUrl} = entityHelper; const {Col, Row} = bootstrap; -function PublisherAttributes({publisher}) { +export function PublisherAttributes({publisher}) { const type = getTypeAttribute(publisher.publisherType).data; const area = extractAttribute(publisher.area, 'name'); const beginDate = extractAttribute(publisher.beginDate); diff --git a/src/client/components/pages/entities/relationships.js b/src/client/components/pages/entities/relationships.js index 7d76ef4667..1d1432fc0c 100644 --- a/src/client/components/pages/entities/relationships.js +++ b/src/client/components/pages/entities/relationships.js @@ -41,7 +41,10 @@ function EntityRelationships({relationships}) { } EntityRelationships.displayName = 'EntityRelationships'; EntityRelationships.propTypes = { - relationships: PropTypes.array.isRequired + relationships: PropTypes.array +}; +EntityRelationships.defaultProps = { + relationships: [] }; export default EntityRelationships; diff --git a/src/client/components/pages/entities/work.js b/src/client/components/pages/entities/work.js index 08a8e9842b..d84cac81e1 100644 --- a/src/client/components/pages/entities/work.js +++ b/src/client/components/pages/entities/work.js @@ -33,7 +33,7 @@ const {getLanguageAttribute, getTypeAttribute, getEntityUrl} = entityHelper; const {Col, Row} = bootstrap; -function WorkAttributes({work}) { +export function WorkAttributes({work}) { const type = getTypeAttribute(work.workType).data; const languages = getLanguageAttribute(work).data; diff --git a/src/client/components/pages/import-entities/creator.js b/src/client/components/pages/import-entities/creator.js new file mode 100644 index 0000000000..fbc110c072 --- /dev/null +++ b/src/client/components/pages/import-entities/creator.js @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2018 Shivam Tripathi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import * as bootstrap from 'react-bootstrap'; +import * as importHelper from '../../../helpers/import-entity'; + +import {CreatorAttributes} from '../entities/creator'; +import EntityImage from '../entities/image'; +import EntityLinks from '../entities/links'; +import ImportFooter from './footer'; +import ImportTitle from './title'; +import PropTypes from 'prop-types'; +import React from 'react'; +import _ from 'lodash'; + + +const {getImportUrl} = importHelper; +const {Alert, Col, Row} = bootstrap; + + +function ImportCreatorDisplayPage({importEntity, identifierTypes}) { + const urlPrefix = getImportUrl(importEntity); + return ( +
+ + + + + + + + + + +
+ + + {`This ${_.startCase(importEntity.type.toLowerCase())} `} + {'has been automatically added. Kindly approve/discard it '} + {'to help us improve our data.'} + + + +
+ ); +} +ImportCreatorDisplayPage.displayName = 'ImportCreatorDisplayPage'; +ImportCreatorDisplayPage.propTypes = { + identifierTypes: PropTypes.array, + importEntity: PropTypes.object.isRequired +}; +ImportCreatorDisplayPage.defaultProps = { + identifierTypes: [] +}; + +export default ImportCreatorDisplayPage; diff --git a/src/client/components/pages/import-entities/discard-import-entity.js b/src/client/components/pages/import-entities/discard-import-entity.js new file mode 100644 index 0000000000..f7e8700b90 --- /dev/null +++ b/src/client/components/pages/import-entities/discard-import-entity.js @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2018 Shivam Tripathi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import * as bootstrap from 'react-bootstrap'; +import { + getImportDiscardUrl, getImportUrl +} from '../../../helpers/import-entity'; +import LoadingSpinner from '../../loading-spinner'; +import PropTypes from 'prop-types'; +import React from 'react'; +import request from 'superagent-bluebird-promise'; + + +const {Alert, Button, ButtonGroup, Col, Panel, Row} = bootstrap; + +class DiscardImportEntity extends React.Component { + constructor(props) { + super(props); + + this.state = { + error: null, + waiting: false + }; + + this.handleClick = this.handleClick.bind(this); + } + + handleClick() { + event.preventDefault(); + + this.setState({ + error: null, + waiting: true + }); + + const discardUrl = getImportDiscardUrl(this.props.importEntity); + const importUrl = getImportUrl(this.props.importEntity); + + request.post(discardUrl) + .then(() => { + window.location.href = importUrl; + }) + .catch((res) => { + const {error} = res.body; + + this.setState({ + error, + waiting: false + }); + }); + } + + render() { + const {importEntity} = this.props; + + let errorComponent = null; + if (this.state.error) { + errorComponent = + {this.state.error}; + } + + const loadingComponent = this.state.waiting ? : null; + + const headerComponent =

Confirm Discard

; + + const entityName = + importEntity.defaultAlias ? + importEntity.defaultAlias.name : '(unnamed)'; + + return ( +
+

Discard Imported Entity

+ + {loadingComponent} + + {errorComponent} + + We really appreciate your efforts in helping us + improve our database. The {importEntity.type} + {entityName} + has been automatically added to + our records and will be permanently deleted in + case multiple editors find it to be corrupt. + If you’re sure that the {importEntity.type} + {entityName} + should be discarded, please press the confirm + button below. Other wise click cancel to get back + to the imported entity page. + + + + + + + +
+ ); + } +} + +DiscardImportEntity.displayName = 'DiscardImportEntity'; +DiscardImportEntity.propTypes = { + importEntity: PropTypes.object.isRequired +}; + +export default DiscardImportEntity; diff --git a/src/client/components/pages/import-entities/edition.js b/src/client/components/pages/import-entities/edition.js new file mode 100644 index 0000000000..e2f6dd696d --- /dev/null +++ b/src/client/components/pages/import-entities/edition.js @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2018 Shivam Tripathi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import * as bootstrap from 'react-bootstrap'; +import * as importHelper from '../../../helpers/import-entity'; + +import {EditionAttributes} from '../entities/edition'; +import EntityImage from '../entities/image'; +import EntityLinks from '../entities/links'; +import ImportFooter from './footer'; +import ImportTitle from './title'; +import PropTypes from 'prop-types'; +import React from 'react'; +import _ from 'lodash'; + + +const {getImportUrl} = importHelper; + +const {Alert, Col, Row} = bootstrap; + + +function ImportEditionDisplayPage({importEntity, identifierTypes}) { + const urlPrefix = getImportUrl(importEntity); + return ( +
+ + + + + + + + + + +
+ + + {`This ${_.startCase(importEntity.type.toLowerCase())} `} + {'has been automatically added. Kindly approve/discard it '} + {'to help us improve our data.'} + + + +
+ ); +} +ImportEditionDisplayPage.displayName = 'ImportEditionDisplayPage'; +ImportEditionDisplayPage.propTypes = { + identifierTypes: PropTypes.array, + importEntity: PropTypes.object.isRequired +}; +ImportEditionDisplayPage.defaultProps = { + identifierTypes: [] +}; + +export default ImportEditionDisplayPage; diff --git a/src/client/components/pages/import-entities/footer.js b/src/client/components/pages/import-entities/footer.js new file mode 100644 index 0000000000..20e9baa446 --- /dev/null +++ b/src/client/components/pages/import-entities/footer.js @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2018 Shivam Tripathi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import * as bootstrap from 'react-bootstrap'; +import * as utilsHelper from '../../../helpers/utils'; + +import Icon from 'react-fontawesome'; +import PropTypes from 'prop-types'; +import React from 'react'; + + +const {formatDate} = utilsHelper; +const { + Button, ButtonGroup, Col, Row, Tooltip +} = bootstrap; + +function ImportFooter({importUrl, importedAt, source, hasVoted}) { + const tooltip = ( + + You can only vote once to discard an import. + + ); + + return ( +
+ + + + + + + + + +
+
+
Imported at
+
{formatDate(new Date(importedAt))}
+
+
+
Source
+
{source}
+
+
+
+ ); +} +ImportFooter.displayName = 'ImportFooter'; +ImportFooter.propTypes = { + hasVoted: PropTypes.bool.isRequired, + importUrl: PropTypes.string.isRequired, + importedAt: PropTypes.string.isRequired, + source: PropTypes.string.isRequired +}; + +export default ImportFooter; diff --git a/src/client/components/pages/import-entities/index.js b/src/client/components/pages/import-entities/index.js new file mode 100644 index 0000000000..448b67b3ba --- /dev/null +++ b/src/client/components/pages/import-entities/index.js @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2018 Shivam Tripathi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import Creator from './creator'; +import DiscardImportEntityPage from './discard-import-entity'; +import Edition from './edition'; +import Publication from './publication'; +import Publisher from './publisher'; +import Work from './work'; + + +const importEntityPages = { + Creator, + DiscardImportEntityPage, + Edition, + Publication, + Publisher, + Work +}; + +export default importEntityPages; diff --git a/src/client/components/pages/import-entities/publication.js b/src/client/components/pages/import-entities/publication.js new file mode 100644 index 0000000000..ca3b5c5f76 --- /dev/null +++ b/src/client/components/pages/import-entities/publication.js @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2018 Shivam Tripathi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import * as bootstrap from 'react-bootstrap'; +import * as importHelper from '../../../helpers/import-entity'; + +import EntityImage from '../entities/image'; +import EntityLinks from '../entities/links'; +import ImportFooter from './footer'; +import ImportTitle from './title'; +import PropTypes from 'prop-types'; +import {PublicationAttributes} from '../entities/publication'; +import React from 'react'; +import _ from 'lodash'; + + +const {getImportUrl} = importHelper; +const {Alert, Col, Row} = bootstrap; + + +function ImportPublicationDisplayPage({importEntity, identifierTypes}) { + const urlPrefix = getImportUrl(importEntity); + return ( +
+ + + + + + + + + + +
+ + + {`This ${_.startCase(importEntity.type.toLowerCase())} `} + {'has been automatically added. Kindly approve/discard it '} + {'to help us improve our data.'} + + + +
+ ); +} +ImportPublicationDisplayPage.displayName = 'ImportPublicationDisplayPage'; +ImportPublicationDisplayPage.propTypes = { + identifierTypes: PropTypes.array, + importEntity: PropTypes.object.isRequired +}; +ImportPublicationDisplayPage.defaultProps = { + identifierTypes: [] +}; + +export default ImportPublicationDisplayPage; diff --git a/src/client/components/pages/import-entities/publisher.js b/src/client/components/pages/import-entities/publisher.js new file mode 100644 index 0000000000..4b7369e2b5 --- /dev/null +++ b/src/client/components/pages/import-entities/publisher.js @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2018 Shivam Tripathi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import * as bootstrap from 'react-bootstrap'; +import * as importHelper from '../../../helpers/import-entity'; + +import EntityImage from '../entities/image'; +import EntityLinks from '../entities/links'; +import ImportFooter from './footer'; +import ImportTitle from './title'; +import PropTypes from 'prop-types'; +import {PublisherAttributes} from '../entities/publisher'; +import React from 'react'; +import _ from 'lodash'; + + +const {getImportUrl} = importHelper; + +const {Alert, Col, Row} = bootstrap; + + +function ImportPublisherDisplayPage({importEntity, identifierTypes}) { + const urlPrefix = getImportUrl(importEntity); + return ( +
+ + + + + + + + + + +
+ + + {`This ${_.startCase(importEntity.type.toLowerCase())} `} + {'has been automatically added. Kindly approve/discard it '} + {'to help us improve our data.'} + + + +
+ ); +} +ImportPublisherDisplayPage.displayName = 'ImportPublisherDisplayPage'; +ImportPublisherDisplayPage.propTypes = { + identifierTypes: PropTypes.array, + importEntity: PropTypes.object.isRequired +}; +ImportPublisherDisplayPage.defaultProps = { + identifierTypes: [] +}; + +export default ImportPublisherDisplayPage; diff --git a/src/client/components/pages/import-entities/recent-imports.js b/src/client/components/pages/import-entities/recent-imports.js new file mode 100644 index 0000000000..8951994c8f --- /dev/null +++ b/src/client/components/pages/import-entities/recent-imports.js @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2018 Shivam Tripathi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import * as bootstrap from 'react-bootstrap'; +import PaginationProps from '../../../helpers/pagination-props'; +import PropTypes from 'prop-types'; +import React from 'react'; +import RecentImportsTable from '../parts/recent-import-results'; +import request from 'superagent-bluebird-promise'; + + +const {PageHeader, Pagination} = bootstrap; + +class RecentImports extends React.Component { + constructor(props) { + super(props); + + this.paginationPropsGenerator = PaginationProps({ + displayedPagesRange: 10, + itemsPerPage: props.limit + }); + + this.state = { + currentPage: props.currentPage, + offset: 0, + paginationProps: { + hasBeginningPage: false, + hasEndPage: false, + hasNextPage: false, + hasPreviousPage: false, + totalPages: 0 + }, + recentImports: [] + }; + + this.handleClick = this.handleClick.bind(this); + } + + componentDidMount() { + this.handleClick(this.state.currentPage); + } + + componentDidUpdate() { + window.history.replaceState( + null, null, `?page=${this.state.currentPage}` + ); + } + + async handleClick(pageNumber) { + const {currentPage, limit, offset, totalResults, recentImports} = + await request.get(`/imports/recent/raw?page=${pageNumber}`) + .then((res) => JSON.parse(res.text)); + + const paginationProps = this.paginationPropsGenerator( + totalResults, currentPage + ); + + this.setState({ + currentPage, limit, offset, paginationProps, recentImports, + totalResults + }); + } + + render() { + const {currentPage, limit, totalResults, paginationProps} = this.state; + return ( +
+ Recent Imports +

The following data has been imported recently.

+ +

{`Displaying ${limit} of ${totalResults} results`}

+ +
+ ); + } +} + +RecentImports.displayName = 'RecentImports'; +RecentImports.propTypes = { + currentPage: PropTypes.number, + limit: PropTypes.number +}; +RecentImports.defaultProps = { + currentPage: 1, + limit: 10 +}; + +export default RecentImports; + diff --git a/src/client/components/pages/import-entities/title.js b/src/client/components/pages/import-entities/title.js new file mode 100644 index 0000000000..f6a967feb1 --- /dev/null +++ b/src/client/components/pages/import-entities/title.js @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2018 Shivam Tripathi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import * as entityHelper from '../../../helpers/entity'; +import * as importHelper from '../../../helpers/import-entity'; + +import PropTypes from 'prop-types'; +import React from 'react'; + + +const {getEntityDisambiguation} = entityHelper; +const {getImportLabel} = importHelper; + + +function ImportTitle({importEntity}) { + const label = getImportLabel(importEntity); + const disambiguation = getEntityDisambiguation(importEntity); + return ( +
+

{label}{disambiguation}

+
+
+ ); +} +ImportTitle.displayName = 'ImportTitle'; +ImportTitle.propTypes = { + importEntity: PropTypes.object.isRequired +}; + +export default ImportTitle; diff --git a/src/client/components/pages/import-entities/work.js b/src/client/components/pages/import-entities/work.js new file mode 100644 index 0000000000..cea74c62c5 --- /dev/null +++ b/src/client/components/pages/import-entities/work.js @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2018 Shivam Tripathi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import * as bootstrap from 'react-bootstrap'; +import * as importHelper from '../../../helpers/import-entity'; + +import EntityImage from '../entities/image'; +import EntityLinks from '../entities/links'; +import ImportFooter from './footer'; +import ImportTitle from './title'; +import PropTypes from 'prop-types'; +import React from 'react'; +import {WorkAttributes} from '../entities/work'; +import _ from 'lodash'; + + +const {getImportUrl} = importHelper; +const {Alert, Col, Row} = bootstrap; + + +function ImportWorkDisplayPage({importEntity, identifierTypes}) { + const urlPrefix = getImportUrl(importEntity); + return ( +
+ + + + + + + + + + +
+ + + {`This ${_.startCase(importEntity.type.toLowerCase())} `} + {'has been automatically added. Kindly approve/discard it '} + {'to help us improve our data.'} + + + +
+ ); +} +ImportWorkDisplayPage.displayName = 'ImportWorkDisplayPage'; +ImportWorkDisplayPage.propTypes = { + identifierTypes: PropTypes.array, + importEntity: PropTypes.object.isRequired +}; +ImportWorkDisplayPage.defaultProps = { + identifierTypes: [] +}; + +export default ImportWorkDisplayPage; diff --git a/src/client/components/pages/parts/recent-import-results.js b/src/client/components/pages/parts/recent-import-results.js new file mode 100644 index 0000000000..184a1c003e --- /dev/null +++ b/src/client/components/pages/parts/recent-import-results.js @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2018 Shivam Tripathi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import * as bootstrap from 'react-bootstrap'; +import * as importHelper from '../../../helpers/import-entity'; +import * as utilsHelper from '../../../helpers/utils'; +import PropTypes from 'prop-types'; +import React from 'react'; + + +const {formatDate} = utilsHelper; +const {getImportUrl} = importHelper; +const {Table} = bootstrap; + +/** + * Renders the document and displays the recentImports table. + * @returns {ReactElement} a HTML document which displays the recentImports + */ + +function RecentImportsTable(props) { + const {offset, recentImports} = props; + return ( +
+

Click to review them!

+ + + + + + + + + + + + { + recentImports.map((importEntity, i) => { + const {importId, importedAt} = importEntity; + return ( + + + + + + + + ); + }) + } + +
#NameTypeDate AddedSource
{i + 1 + offset} + + {importEntity.defaultAlias.name} + + {importEntity.type} + {formatDate(new Date(importedAt))} + + {importEntity.source} +
+
+ ); +} + +RecentImportsTable.propTypes = { + offset: PropTypes.number.isRequired, + recentImports: PropTypes.array.isRequired +}; + +export default RecentImportsTable; diff --git a/src/client/components/pages/parts/search-results.js b/src/client/components/pages/parts/search-results.js index 0bca384c67..acecb71e20 100644 --- a/src/client/components/pages/parts/search-results.js +++ b/src/client/components/pages/parts/search-results.js @@ -1,6 +1,7 @@ /* * Copyright (C) 2015 Ohm Patel * 2016 Sean Burke + * 2017 Shivam Tripathi * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -22,7 +23,7 @@ import PropTypes from 'prop-types'; import React from 'react'; -const {Table} = bootstrap; +const {Label, Table} = bootstrap; function SearchResults(props) { const noResults = !props.results || props.results.length === 0; @@ -38,19 +39,32 @@ function SearchResults(props) { // No redirect link for Area entity results const name = result.defaultAlias ? result.defaultAlias.name : '(unnamed)'; - const link = result.type === 'Area' ? - `//musicbrainz.org/area/${result.bbid}` : - `/${result.type.toLowerCase()}/${result.bbid}`; + + let link = null; + if (result.type === 'Area') { + link = `//musicbrainz.org/area/${result.bbid}`; + } + else if (result.bbid) { + link = `/${result.type.toLowerCase()}/${result.bbid}`; + } + else if (result.importId) { + link = `/imports/${result.type.toLowerCase()}/${result.importId}`; + } + result.id = result.bbid || result.importId; + const tag = result.importId ? : null; return ( - + - + {name} - {result.type} + {result.type}{' '}{tag} ); diff --git a/src/client/components/pages/search.js b/src/client/components/pages/search.js index 1031aac9a5..aad7b0ba81 100644 --- a/src/client/components/pages/search.js +++ b/src/client/components/pages/search.js @@ -1,6 +1,7 @@ /* * Copyright (C) 2015 Ohm Patel * 2016 Sean Burke + * 2018 Shivam Tripathi * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -68,6 +69,12 @@ class SearchPage extends React.Component {
+
+

+ Entries marked in red have been automatically imported. + Kindly review them to make them a part of our BookBrainz + database. +

); } diff --git a/src/client/containers/layout.js b/src/client/containers/layout.js index 23e77823cd..d0c82b0938 100644 --- a/src/client/containers/layout.js +++ b/src/client/containers/layout.js @@ -140,6 +140,12 @@ class Layout extends React.Component { {' Statistics '} + {!(homepage || hideSearch) &&
+ + +); + +ReactDOM.hydrate(markup, document.getElementById('target')); diff --git a/src/client/controllers/import-entity/import-entity.js b/src/client/controllers/import-entity/import-entity.js new file mode 100644 index 0000000000..58f3513568 --- /dev/null +++ b/src/client/controllers/import-entity/import-entity.js @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2018 Shivam Tripathi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import { + extractImportEntityProps, + extractLayoutProps +} from '../../helpers/props'; +import ImportEntityPages from '../../components/pages/import-entities'; +import Layout from '../../containers/layout'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import _ from 'lodash'; + + +const propsTarget = document.getElementById('props'); +const props = propsTarget ? JSON.parse(propsTarget.innerHTML) : {}; + +const entityType = _.get(props, 'importEntity.type'); +const Child = ImportEntityPages[entityType]; + +if (!Child) { + throw new Error('Controller::ImportEntity - Invalid entity type'); +} + +const markup = ( + + + +); + + +ReactDOM.hydrate(markup, document.getElementById('target')); diff --git a/src/client/controllers/import-entity/recent-imports.js b/src/client/controllers/import-entity/recent-imports.js new file mode 100644 index 0000000000..daa37bbc36 --- /dev/null +++ b/src/client/controllers/import-entity/recent-imports.js @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2018 Shivam Tripathi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import Layout from '../../containers/layout'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import RecentImports from + '../../components/pages/import-entities/recent-imports'; +import {extractLayoutProps} from '../../helpers/props'; + + +const propsTarget = document.getElementById('props'); +const props = propsTarget ? JSON.parse(propsTarget.innerHTML) : {}; +const markup = ( + + + +); + +ReactDOM.hydrate(markup, document.getElementById('target')); diff --git a/src/client/entity-editor/submission-section/actions.js b/src/client/entity-editor/submission-section/actions.js index a4ffb3545c..d6af9fac72 100644 --- a/src/client/entity-editor/submission-section/actions.js +++ b/src/client/entity-editor/submission-section/actions.js @@ -80,7 +80,7 @@ function postSubmission(url: string, data: Map): Promise { * pass the entity type and generate both URLs from that. */ - const [, submissionEntity] = url.split('/'); + const [, submissionEntity, importSubmissionEntity] = url.split('/'); return request.post(url).send(data) .promise() .then((response: Response) => { @@ -89,7 +89,13 @@ function postSubmission(url: string, data: Map): Promise { } const redirectUrl = `/${submissionEntity}/${response.body.bbid}`; - if (response.body.alert) { + const importRedirectUrl = + `/${importSubmissionEntity}/${response.body.bbid}`; + + if (submissionEntity === 'imports') { + window.location.href = importRedirectUrl; + } + else if (response.body.alert) { const alertParam = `?alert=${response.body.alert}`; window.location.href = `${redirectUrl}${alertParam}`; } diff --git a/src/client/helpers/import-entity.js b/src/client/helpers/import-entity.js new file mode 100644 index 0000000000..00aba3222a --- /dev/null +++ b/src/client/helpers/import-entity.js @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2018 Shivam Tripathi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import _ from 'lodash'; + + +export function getImportLabel(importEntity) { + return `${_.get(importEntity, 'defaultAlias.name', '(unnamed)')} `; +} + +export function getImportUrl(importEntity) { + const type = importEntity.type.toLowerCase(); + const id = importEntity.importId; + return `/imports/${type}/${id}`; +} + +export function getImportDiscardUrl(importEntity) { + const type = importEntity.type.toLowerCase(); + const id = importEntity.importId; + return `/imports/${type}/${id}/discard/handler`; +} diff --git a/src/client/helpers/pagination-props.js b/src/client/helpers/pagination-props.js new file mode 100644 index 0000000000..d2cb42eec5 --- /dev/null +++ b/src/client/helpers/pagination-props.js @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2018 Shivam Tripathi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +// @flow + +import _ from 'lodash'; + + +type getPaginationPropsGeneratorType = { + displayedPagesRange: number, + itemsPerPage: number +}; + +/** Pagination::validateDefault - Validates, if invalid passed default + * @param {number} currentPage - The current active page + * @param {number} totalResults - Total number of results + * @param {number} itemsPerPage - number of items per page + * @returns {Object} - Returns an object encapsulating validated + * currentPage, totalResults, totalPages + */ +function validatePaginationArgs( + currentPage: number, + totalResults: number, + itemsPerPage: number +) { + const totalPages: number = Math.ceil(totalResults / itemsPerPage); + const args = {currentPage, totalPages, totalResults}; + if (!args.currentPage || + !_.isNumber(args.currentPage) || + args.currentPage < 1 + ) { + args.currentPage = 1; + } + + if (!args.totalPages || + !_.isNumber(args.totalPages) || + args.totalPages < 1 + ) { + args.totalPages = 1; + args.totalResults = itemsPerPage; + } + + if (args.currentPage > args.totalPages) { + args.currentPage = args.totalPages; + } + return args; +} + +/** getPaginationPropsGenerator - Takes in number of pages and items per page, + * defaults to 10, 25 and returns propsGenerator function + * @param {object} args - encapsulates args + * @param {number} args.displayedPagesRange - Range of pages surrounding the + * current page to be displayed + * @param {number} args.itemsPerPage - Number of items per page + * @returns {function} - Returns a function which give pagination props given + * currentPage and totalResults + */ +export default function getPaginationPropsGenerator( + args: getPaginationPropsGeneratorType +) { + const {displayedPagesRange = 10, itemsPerPage = 25} = args; + + /** + * @param {number} totalRes - Total number of results + * @param {number} curPage - Current page + * @returns {object} Returns result encapsulating pagination details + */ + return function getDetails(totalRes: number, curPage: number) { + // Extract results, clean them up + const {currentPage, totalPages, totalResults} = + validatePaginationArgs(curPage, totalRes, itemsPerPage); + + // The first and last page links to be displayed, exactly halfway left + // and right from the currentpage + const halfwayDistance: number = + Math.floor(displayedPagesRange / 2); + let firstPage: number = Math.max(1, currentPage - halfwayDistance); + let lastPage: number = + Math.min(totalPages, currentPage + halfwayDistance); + + // Incase number of pages lying in [firstPage, lastPage] are not + // covering the range due to being at extremes, we adjust the range + const defaultRange: number = lastPage - firstPage + 1; + const defaultDiffInRange = displayedPagesRange - defaultRange; + if (defaultDiffInRange) { + if (lastPage < totalPages) { + lastPage = Math.min(lastPage + defaultDiffInRange, totalPages); + } + if (firstPage > 1) { + firstPage = Math.max(firstPage - defaultDiffInRange, 1); + } + } + + // First result on current page + const firstResultOnCurrentPage: number = _.clamp( + displayedPagesRange * (currentPage - 1), 1, totalResults + ); + const lastResultOnCurrentPage: number = _.clamp( + displayedPagesRange * currentPage, 1, totalResults + ); + + return { + beginningPage: 1, + currentPage, + endPage: totalPages, + firstPage, + firstResultOnCurrentPage, + hasBeginningPage: totalPages > 1, + hasEndPage: totalPages > 1, + hasNextPage: currentPage < totalPages, + hasPreviousPage: (currentPage - 1) > 0, + itemsPerPage, + lastPage, + lastResultOnCurrentPage, + nextPage: currentPage + 1, + pagesRange: _.clamp( + lastPage - firstPage + 1, + 1, totalPages + ), + previousPage: currentPage - 1, + resultsRange: _.clamp( + lastResultOnCurrentPage - firstResultOnCurrentPage + 1, + 1, totalResults + ), + totalPages, + totalResults + }; + }; +} diff --git a/src/client/helpers/props.js b/src/client/helpers/props.js index e45116d224..1d1e6e3509 100644 --- a/src/client/helpers/props.js +++ b/src/client/helpers/props.js @@ -53,3 +53,11 @@ export function extractEntityProps(props) { identifierTypes: props.identifierTypes }; } + +export function extractImportEntityProps(props) { + return { + alert: props.alert, + identifierTypes: props.identifierTypes, + importEntity: props.importEntity + }; +} diff --git a/src/client/stylesheets/style.less b/src/client/stylesheets/style.less index c4a8a229d3..d8b6b8c7fe 100644 --- a/src/client/stylesheets/style.less +++ b/src/client/stylesheets/style.less @@ -2,6 +2,7 @@ * Copyright (C) 2014-2015 Ben Ockmore * 2015 Leo Verto * 2016 Sean Burke + * 2018 Shivam Tripathi * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -250,3 +251,11 @@ html { .Select-menu-outer { z-index: 5; } + +.color-red { + color: red; +} + +.font-weight-bold { + font-weight: bold; +} \ No newline at end of file diff --git a/src/server/helpers/importEntityRouteUtils.js b/src/server/helpers/importEntityRouteUtils.js new file mode 100644 index 0000000000..db42e99e5e --- /dev/null +++ b/src/server/helpers/importEntityRouteUtils.js @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2018 Shivam Tripathi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +// @flow + +import * as utils from './utils'; +import _ from 'lodash'; +import express from 'express'; +import {generateProps} from './props'; + + +/** + * Returns a props object with reasonable defaults for import entity editing. + * @param {request} req - request object + * @param {response} res - response object + * @param {object} initialState - The initial state of the edit page + * to get the initial state + * @param {object} additionalProps - additional props + * @returns {object} - props + */ +export function generateImportEntityProps( + req: express.request, res: express.response, initialState: Object, + additionalProps: Object +): Object { + const {importEntity} = res.locals; + const {type: importEntityName} = importEntity; + const importEntityType = importEntityName.toLowerCase(); + + const getFilteredIdentifierTypes = + _.partialRight(utils.filterIdentifierTypesByEntity, importEntity); + const filteredIdentifierTypes = getFilteredIdentifierTypes( + res.locals.identifierTypes + ); + + const submissionUrl = + `/imports/${importEntityType}/${importEntity.importId}/edit/approve`; + + const props = Object.assign({ + entityType: importEntityType, + heading: `Edit Import ${importEntityName}`, + identifierTypes: filteredIdentifierTypes, + importEntityType, + initialState, + languageOptions: res.locals.languages, + requiresJS: true, + subheading: + `Edit and approve an import ${importEntityName} on BookBrainz`, + submissionUrl + }, additionalProps); + + return generateProps(req, res, props); +} diff --git a/src/server/helpers/middleware.js b/src/server/helpers/middleware.js index 05545a76bc..35941d7981 100644 --- a/src/server/helpers/middleware.js +++ b/src/server/helpers/middleware.js @@ -1,6 +1,7 @@ /* * Copyright (C) 2015 Ben Ockmore * 2015-2016 Sean Burke + * 2018 Shivam Tripathi * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -20,6 +21,8 @@ import * as error from '../helpers/error'; import * as utils from '../helpers/utils'; import Promise from 'bluebird'; +import _ from 'lodash'; +import moment from 'moment'; import renderRelationship from '../helpers/render'; @@ -157,3 +160,62 @@ export function makeEntityLoader(modelName, additionalRels, errMessage) { return next('route'); }; } + +export function makeImportLoader(modelName, additionalRels, errMessage) { + const relations = [ + 'aliasSet.aliases.language', + 'defaultAlias', + 'disambiguation', + 'identifierSet.identifiers.type' + ].concat(additionalRels); + + return async (req, res, next, _importId) => { + const importId = parseInt(_importId, 10); + + if (utils.isValidImportId(importId)) { + const {orm} = req.app.locals; + const model = utils.getImportModelByType(orm, modelName); + try { + const importEntityRecord = await model.forge({importId}) + .fetch({ + withRelated: relations + }); + res.locals.importEntity = importEntityRecord.toJSON(); + + const [votes, details] = await orm.bookshelf.transaction( + (transacting) => + Promise.all([ + orm.func.imports.discardVotesCast( + transacting, importId + ), + orm.func.imports.getImportDetails( + transacting, importId + ) + ]) + ); + + if (_.get(req, 'session.passport.user.id')) { + const editorId = req.session.passport.user.id; + res.locals.importEntity.hasVoted = + votes.length > 0 && Boolean(votes.filter( + vote => vote.editorId === editorId + )); + } + else { + res.locals.importEntity.hasVoted = false; + } + + res.locals.importEntity.source = details.source; + res.locals.importEntity.importedAt = + moment(details.importedAt).format('YYYY-MM-DD'); + } + catch (err) { + throw new error.NotFoundError(errMessage, req); + } + + return next(); + } + + return next('route'); + }; +} diff --git a/src/server/helpers/search.js b/src/server/helpers/search.js index fb03dfc53b..3445919091 100644 --- a/src/server/helpers/search.js +++ b/src/server/helpers/search.js @@ -1,5 +1,6 @@ /* * Copyright (C) 2016 Sean Burke + * 2018 Shivam Tripathi * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -54,10 +55,22 @@ function _fetchEntityModelsForESResults(orm, results) { return areaJSON; }); } - const model = utils.getEntityModelByType(orm, entityStub.type); - return model.forge({bbid: entityStub.bbid}) - .fetch({withRelated: ['defaultAlias']}) - .then((entity) => entity.toJSON()); + + if (entityStub.bbid) { + const model = utils.getEntityModelByType(orm, entityStub.type); + return model.forge({bbid: entityStub.bbid}) + .fetch({withRelated: ['defaultAlias']}) + .then((entity) => entity.toJSON()); + } + else if (entityStub.importId) { + const modelType = `${entityStub.type}Import`; + const model = utils.getImportModelByType(orm, modelType); + return model.forge({importId: entityStub.importId}) + .fetch({withRelated: ['defaultAlias']}) + .then((importObj) => importObj.toJSON()); + } + + return Promise.resolve(null); }); } @@ -196,7 +209,10 @@ export function refreshIndex() { /* eslint camelcase: 0, no-magic-numbers: 1 */ export async function generateIndex(orm) { - const {Area, Creator, Edition, Publication, Publisher, Work} = orm; + const {Area, Creator, Edition, Publication, Publisher, Work, + CreatorImport, EditionImport, PublicationImport, PublisherImport, + WorkImport} = orm; + const indexMappings = { mappings: { _default_: { @@ -302,7 +318,7 @@ export async function generateIndex(orm) { ]; // Update the indexed entries for each entity type - const behaviorPromise = entityBehaviors.map( + const entityBehaviorPromise = entityBehaviors.map( (behavior) => behavior.model.forge() .query((qb) => { qb.where('master', true); @@ -312,13 +328,54 @@ export async function generateIndex(orm) { withRelated: baseRelations.concat(behavior.relations) }) ); - const entityLists = await Promise.all(behaviorPromise); + const entityLists = await Promise.all(entityBehaviorPromise); + + const importBehaviors = [ + { + model: CreatorImport, + relations: [ + 'gender', + 'creatorType', + 'beginArea', + 'endArea' + ] + }, + { + model: EditionImport, + relations: [ + 'publication', + 'editionFormat', + 'editionStatus' + ] + }, + {model: PublicationImport, relations: ['publicationType']}, + {model: PublisherImport, relations: ['publisherType', 'area']}, + {model: WorkImport, relations: ['workType']} + ]; + + const importBehaviorPromise = importBehaviors.map( + (behavior) => behavior.model.forge() + .query((qb) => { + qb.whereNotNull('data_id'); + }) + .fetchAll({ + withRelated: baseRelations.concat(behavior.relations) + }) + ); + const importLists = await Promise.all(importBehaviorPromise); const listIndexes = []; + // Process entity lists for (const entityList of entityLists) { const listArray = entityList.toJSON(); listIndexes.push(_processEntityListForBulk(listArray)); } + // Process import lists + for (const importList of importLists) { + const listArray = importList.toJSON(); + listIndexes.push(_processEntityListForBulk(listArray)); + } + await Promise.all(listIndexes); const areaCollection = await Area.forge() diff --git a/src/server/helpers/utils.js b/src/server/helpers/utils.js index 6916458f5d..5ec659e0d2 100644 --- a/src/server/helpers/utils.js +++ b/src/server/helpers/utils.js @@ -1,6 +1,7 @@ /* * Copyright (C) 2015 Ben Ockmore * 2015-2017 Sean Burke + * 2018 Shivam Tripathi * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -20,6 +21,8 @@ // @flow import Promise from 'bluebird'; +import _ from 'lodash'; + /** * Returns an API path for interacting with the given Bookshelf entity model @@ -48,6 +51,25 @@ export function getEntityModels(orm: Object): Object { }; } +/** + * Returns all import models defined in bookbrainz-data-js + * + * @param {object} orm - the BookBrainz ORM, initialized during app setup + * @returns {object} - Object mapping model name to the import model + */ +export function getImportModels(orm: Object): Object { + const {CreatorImport, EditionImport, PublicationImport, PublisherImport, + WorkImport} = orm; + + return { + CreatorImport, + EditionImport, + PublicationImport, + PublisherImport, + WorkImport + }; +} + export function filterIdentifierTypesByEntityType( identifierTypes: Array, entityType: string @@ -100,6 +122,26 @@ export function getEntityModelByType(orm: Object, type: string): Object { return entityModels[type]; } +/** + * Retrieves the Bookshelf import model with the given the model name + * + * @param {Object} orm - The BookBrainz ORM, initialized during app setup + * @param {string} type - Name or type of model + * @throws {Error} Throws a custom error if the param 'type' does not + * map to a model + * @returns {object} - Bookshelf model object with the type specified in the + * single param + */ +export function getImportModelByType(orm: Object, type: string): Object { + const importModels = getImportModels(orm); + + if (!importModels[type]) { + throw new Error('Unrecognized import type'); + } + + return importModels[type]; +} + /** * Regular expression for valid BookBrainz UUIDs (bbid) * @@ -119,6 +161,10 @@ export function isValidBBID(bbid: string): boolean { return _bbidRegex.test(bbid); } +export function isValidImportId(id: number): boolean { + return _.isFinite(id) && id > 0; +} + /** * Helper-function / template-tag that allows the values of an object that * is passed in at a later time to be interpolated into a diff --git a/src/server/routes.js b/src/server/routes.js index 20e4fec9b3..05b90bd7ec 100644 --- a/src/server/routes.js +++ b/src/server/routes.js @@ -21,6 +21,7 @@ import authRouter from './routes/auth'; import creatorRouter from './routes/entity/creator'; import editionRouter from './routes/entity/edition'; import editorRouter from './routes/editor'; +import importRouter from './routes/import-entity'; import indexRouter from './routes/index'; import publicationRouter from './routes/entity/publication'; import publisherRouter from './routes/entity/publisher'; @@ -37,6 +38,7 @@ function initRootRoutes(app) { app.use('/search', searchRouter); app.use('/register', registerRouter); app.use('/statistics', statisticsRouter); + app.use('/imports', importRouter); } function initPublicationRoutes(app) { diff --git a/src/server/routes/import-entity/creator.js b/src/server/routes/import-entity/creator.js new file mode 100644 index 0000000000..b7abe14c04 --- /dev/null +++ b/src/server/routes/import-entity/creator.js @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2018 Shivam Tripathi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import * as auth from '../../helpers/auth'; +import * as importRoutes from './import-routes'; +import * as middleware from '../../helpers/middleware'; +import * as utils from '../../helpers/utils'; +import express from 'express'; + + +const router = express.Router(); + +/* If the route specifies an importId, load the importEntity for it. */ +router.param( + 'importId', + middleware.makeImportLoader( + 'CreatorImport', + ['creatorType', 'gender', 'beginArea', 'endArea'], + 'Creator Import not found' + ) +); + +function _setCreatorTitle(res) { + res.locals.title = utils.createEntityPageTitle( + res.locals.entity, + 'Creator (Import)', + utils.template`Creator (Import) “${'name'}”` + ); +} + +router.get('/:importId', (req, res) => { + _setCreatorTitle(res); + importRoutes.displayImport(req, res); +}); + +router.get('/:importId/discard', auth.isAuthenticated, (req, res) => { + _setCreatorTitle(res); + importRoutes.displayDiscardImportEntity(req, res); +}); + +router.post( + '/:importId/discard/handler', + auth.isAuthenticatedForHandler, + importRoutes.handleDiscardImportEntity +); + +router.get( + '/:importId/approve', + auth.isAuthenticatedForHandler, + importRoutes.approveImportEntity +); + +router.get( + '/:importId/edit', + middleware.loadIdentifierTypes, middleware.loadGenders, + middleware.loadLanguages, middleware.loadCreatorTypes, + importRoutes.editImportEntity +); + +router.post( + '/:importId/edit/approve', + auth.isAuthenticatedForHandler, middleware.loadIdentifierTypes, + middleware.loadGenders, middleware.loadLanguages, + middleware.loadCreatorTypes, + importRoutes.approveImportPostEditing +); + +export default router; diff --git a/src/server/routes/import-entity/edition.js b/src/server/routes/import-entity/edition.js new file mode 100644 index 0000000000..9349dfea43 --- /dev/null +++ b/src/server/routes/import-entity/edition.js @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2018 Shivam Tripathi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import * as auth from '../../helpers/auth'; +import * as importRoutes from './import-routes'; +import * as middleware from '../../helpers/middleware'; +import * as utils from '../../helpers/utils'; +import express from 'express'; + + +const router = express.Router(); + +/* If the route specifies an importId, load the importEntity for it. */ +router.param( + 'importId', + middleware.makeImportLoader( + 'EditionImport', + [ + 'publication.defaultAlias', + 'languageSet.languages', + 'editionFormat', + 'editionStatus', + 'releaseEventSet.releaseEvents', + 'publisherSet.publishers.defaultAlias' + ], + 'Edition Import not found' + ) +); + +function _setEditionTitle(res) { + res.locals.title = utils.createEntityPageTitle( + res.locals.entity, + 'Edition (Import)', + utils.template`Edition (Import) “${'name'}”` + ); +} + +router.get('/:importId', (req, res) => { + _setEditionTitle(res); + importRoutes.displayImport(req, res); +}); + +router.get('/:importId/discard', auth.isAuthenticated, (req, res) => { + _setEditionTitle(res); + importRoutes.displayDiscardImportEntity(req, res); +}); + +router.post( + '/:importId/discard/handler', + auth.isAuthenticatedForHandler, + importRoutes.handleDiscardImportEntity +); + +router.get( + '/:importId/approve', + auth.isAuthenticatedForHandler, + importRoutes.approveImportEntity +); + +router.get( + '/:importId/edit', + auth.isAuthenticated, middleware.loadIdentifierTypes, + middleware.loadEditionStatuses, middleware.loadEditionFormats, + middleware.loadLanguages, + importRoutes.editImportEntity +); + +router.post( + '/:importId/edit/approve', + auth.isAuthenticated, middleware.loadIdentifierTypes, + middleware.loadEditionStatuses, middleware.loadEditionFormats, + middleware.loadLanguages, + importRoutes.approveImportPostEditing +); + +export default router; diff --git a/src/server/routes/import-entity/import-routes.js b/src/server/routes/import-entity/import-routes.js new file mode 100644 index 0000000000..ed202de85d --- /dev/null +++ b/src/server/routes/import-entity/import-routes.js @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2016 Ben Ockmore + * 2018 Shivam Tripathi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import * as achievement from '../../helpers/achievement'; +import * as error from '../../helpers/error'; +import * as propHelpers from '../../../client/helpers/props'; +import * as search from '../../helpers/search'; +import {escapeProps, generateProps} from '../../helpers/props'; +import Layout from '../../../client/containers/layout'; +import React from 'react'; +import ReactDOMServer from 'react-dom/server'; +import _ from 'lodash'; +import {entityEditorMarkup} from '../../helpers/entityRouteUtils'; +import {entityToFormState} from './transform-import'; +import {generateImportEntityProps} from '../../helpers/importEntityRouteUtils'; +import {getEntityUrl} from '../../../client/helpers/entity'; +import {getImportUrl} from '../../../client/helpers/import-entity'; +import {getValidator} from '../../../client/entity-editor/helpers'; +import importEntityPages from + '../../../client/components/pages/import-entities'; +import {transformForm} from './transform-form'; +import uuid from 'uuid'; + + +export function displayImport(req, res) { + const {importEntity} = res.locals; + + // Get unique identifier types for display + const identifierTypes = importEntity.identifierSet && + _.uniqBy( + _.map(importEntity.identifierSet.identifiers, 'type'), + (type) => type.id + ); + + const {type} = importEntity; + const ImportEntityComponent = importEntityPages[type]; + if (ImportEntityComponent) { + const props = generateProps(req, res, {identifierTypes}); + const markup = ReactDOMServer.renderToString( + + + + ); + + res.render('target', { + markup, + props: escapeProps(props), + script: '/js/import-entity/import-entity.js' + }); + } + else { + throw new Error( + `Component was not found for the following import: ${type}` + ); + } +} + +export function displayDiscardImportEntity(req, res) { + const {importEntity} = res.locals; + const importUrl = getImportUrl(importEntity); + + if (importEntity.hasVoted) { + res.redirect(importUrl); + } + + const props = generateProps(req, res); + const {DiscardImportEntityPage} = importEntityPages; + const markup = ReactDOMServer.renderToString( + + + + ); + + res.render('target', { + markup, + props: escapeProps(props), + script: '/js/import-entity/discard-import-entity.js' + }); +} + +export function handleDiscardImportEntity(req, res) { + const {orm} = req.app.locals; + const editorId = req.session.passport.user.id; + const {importEntity} = res.locals; + orm.bookshelf.transaction(async (transacting) => { + try { + await orm.func.imports.castDiscardVote( + transacting, importEntity.importId, editorId + ); + // Todo: Add code to remove importEntity from the search index + res.status(200).send(); + } + catch (err) { + res.status(400).send({error: err}); + } + }); +} + +export async function approveImportEntity(req, res) { + const {orm} = res.app.locals; + const editorId = req.session.passport.user.id; + const {importEntity} = res.locals; + const entity = await orm.bookshelf.transaction((transacting) => + orm.func.imports.approveImport( + {editorId, importEntity, orm, transacting} + )); + const entityUrl = getEntityUrl(entity); + + /* Add code to remove import and add the newly created entity to the elastic + search index and remove delete import */ + + // Update editor achievement + entity.alert = (await achievement.processEdit( + orm, editorId, entity.revisionId + )).alert; + + // Cleanup search indexing + search.indexEntity(entity); + // Todo: Add functionality to remove imports from ES index upon deletion + + res.redirect(entityUrl); +} + +export function editImportEntity(req, res) { + const {importEntity} = res.locals; + const initialState = entityToFormState(importEntity); + const importEntityProps = generateImportEntityProps( + req, res, initialState, {} + ); + const {markup, props} = entityEditorMarkup(importEntityProps); + return res.render('target', { + markup, + props: escapeProps(props), + script: '/js/entity-editor.js', + title: 'Edit Work Import' + }); +} + +export async function approveImportPostEditing(req, res) { + const {orm} = req.app.locals; + const {importEntity} = res.locals; + const editorId = req.session.passport.user.id; + const {importId, type} = importEntity; + const formData = req.body; + + const validateForm = getValidator(type.toLowerCase()); + + if (!validateForm(formData)) { + const err = new error.FormSubmissionError(); + error.sendErrorAsJSON(res, err); + } + + const entityData = transformForm[type](formData); + + const entity = await orm.bookshelf.transaction(async (transacting) => { + await orm.func.imports.deleteImport( + transacting, importId + ); + return orm.func.createEntity( + {editorId, entityData, orm, transacting} + ); + }); + + // Update editor achievement + entity.alert = (await achievement.processEdit( + orm, editorId, entity.revisionId + )).alert; + + // Cleanup search indexing + await search.indexEntity(entity); + // To-do: Add code to remove importEntity from the search index + + res.send(entity); +} diff --git a/src/server/routes/import-entity/index.js b/src/server/routes/import-entity/index.js new file mode 100644 index 0000000000..1a9261d996 --- /dev/null +++ b/src/server/routes/import-entity/index.js @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2018 Shivam Tripathi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import ImportCreatorRouter from './creator'; +import ImportEditionRouter from './edition'; +import ImportPublicationRouter from './publication'; +import ImportPublisherRouter from './publisher'; +import ImportRecentRouter from './recent'; +import ImportWorkRouter from './work'; +import express from 'express'; + + +const importRouter = express.Router(); + +importRouter.use('/creator', ImportCreatorRouter); +importRouter.use('/edition', ImportEditionRouter); +importRouter.use('/publisher', ImportPublisherRouter); +importRouter.use('/publication', ImportPublicationRouter); +importRouter.use('/work', ImportWorkRouter); +importRouter.use('/recent', ImportRecentRouter); + +export default importRouter; diff --git a/src/server/routes/import-entity/publication.js b/src/server/routes/import-entity/publication.js new file mode 100644 index 0000000000..1a35a80d60 --- /dev/null +++ b/src/server/routes/import-entity/publication.js @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2018 Shivam Tripathi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import * as auth from '../../helpers/auth'; +import * as importRoutes from './import-routes'; +import * as middleware from '../../helpers/middleware'; +import * as utils from '../../helpers/utils'; +import express from 'express'; + + +const router = express.Router(); + +/* If the route specifies an importId, load the importEntity for it. */ +router.param( + 'importId', + middleware.makeImportLoader( + 'PublicationImport', + [ + 'publicationType', + 'editions.defaultAlias', + 'editions.disambiguation', + 'editions.releaseEventSet.releaseEvents' + ], + 'Publication Import not found' + ) +); + +function _setPublicationTitle(res) { + res.locals.title = utils.createEntityPageTitle( + res.locals.entity, + 'Publication (Import)', + utils.template`Publication (Import) “${'name'}”` + ); +} + +router.get('/:importId', (req, res) => { + _setPublicationTitle(res); + importRoutes.displayImport(req, res); +}); + +router.get('/:importId/discard', auth.isAuthenticated, (req, res) => { + _setPublicationTitle(res); + importRoutes.displayDiscardImportEntity(req, res); +}); + +router.post( + '/:importId/discard/handler', + auth.isAuthenticatedForHandler, + importRoutes.handleDiscardImportEntity +); + +router.get( + '/:importId/approve', + auth.isAuthenticatedForHandler, + importRoutes.approveImportEntity +); + +router.get( + '/:importId/edit', + auth.isAuthenticated, middleware.loadIdentifierTypes, + middleware.loadPublicationTypes, middleware.loadLanguages, + importRoutes.editImportEntity +); + +router.post( + '/:importId/edit/approve', + auth.isAuthenticated, middleware.loadIdentifierTypes, + middleware.loadPublicationTypes, middleware.loadLanguages, + importRoutes.approveImportPostEditing +); + +export default router; diff --git a/src/server/routes/import-entity/publisher.js b/src/server/routes/import-entity/publisher.js new file mode 100644 index 0000000000..434719394a --- /dev/null +++ b/src/server/routes/import-entity/publisher.js @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2018 Shivam Tripathi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import * as auth from '../../helpers/auth'; +import * as importRoutes from './import-routes'; +import * as middleware from '../../helpers/middleware'; +import * as utils from '../../helpers/utils'; +import express from 'express'; + + +const router = express.Router(); + +/* If the route specifies an importId, load the importEntity for it. */ +router.param( + 'importId', + middleware.makeImportLoader( + 'PublisherImport', + ['publisherType', 'area'], + 'Publisher Import not found' + ) +); + +function _setPublisherTitle(res) { + res.locals.title = utils.createEntityPageTitle( + res.locals.entity, + 'Creator (Import)', + utils.template`Creator (Import) “${'name'}”` + ); +} + +router.get('/:importId', (req, res) => { + _setPublisherTitle(res); + importRoutes.displayImport(req, res); +}); + +router.get('/:importId/discard', auth.isAuthenticated, (req, res) => { + _setPublisherTitle(res); + importRoutes.displayDiscardImportEntity(req, res); +}); + +router.post( + '/:importId/discard/handler', + auth.isAuthenticatedForHandler, + importRoutes.handleDiscardImportEntity +); + +router.get( + '/:importId/approve', + auth.isAuthenticatedForHandler, + importRoutes.approveImportEntity +); + +router.get( + '/:importId/edit', + auth.isAuthenticated, middleware.loadIdentifierTypes, + middleware.loadPublisherTypes, middleware.loadLanguages, + importRoutes.editImportEntity +); + +router.post( + '/:importId/edit/approve', + auth.isAuthenticated, middleware.loadIdentifierTypes, + middleware.loadPublisherTypes, middleware.loadLanguages, + importRoutes.approveImportPostEditing +); + +export default router; diff --git a/src/server/routes/import-entity/recent.js b/src/server/routes/import-entity/recent.js new file mode 100644 index 0000000000..e6517503e1 --- /dev/null +++ b/src/server/routes/import-entity/recent.js @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2018 Shivam Tripathi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import * as propHelpers from '../../../client/helpers/props'; +import {escapeProps, generateProps} from '../../helpers/props'; +import Layout from '../../../client/containers/layout'; +import React from 'react'; +import ReactDOMServer from 'react-dom/server'; +import RecentImports from + '../../../client/components/pages/import-entities/recent-imports'; +import express from 'express'; + + +// The limit for number of results fetched from database +const LIMIT = 10; + +const router = express.Router(); + +/* Function to fetch data from the database and create the object to be sent as + response */ +function fetchRecentImportsData(orm, page) { + let pageNumber = page; + + return orm.bookshelf.transaction(async (transacting) => { + // First fetch total imports + const totalResults = + await orm.func.imports.getTotalImports(transacting); + + if (totalResults < ((pageNumber - 1) * LIMIT)) { + pageNumber = Math.ceil(totalResults / LIMIT); + } + const offset = (pageNumber - 1) * LIMIT; + + // Now fetch recent imports according to the generated offset + const recentImports = await orm.func.imports.getRecentImports( + orm, transacting, LIMIT, offset + ); + + return { + currentPage: pageNumber, + limit: LIMIT, + offset, + recentImports, + totalResults + }; + }); +} + +// This handles the router to send initial container to hold recentImports data +function recentImportsRoute(req, res) { + const queryPage = parseInt(req.query.page, 10) || 1; + const props = generateProps(req, res, { + currentPage: queryPage, limit: LIMIT + }); + + const markup = ReactDOMServer.renderToString( + + + + ); + + res.render('target', { + markup, + props: escapeProps(props), + script: '/js/import-entity/recent-imports.js', + title: 'Recent Imports' + }); +} + +// This handles the data fetching route for recent imports, sends JSON as res +async function rawRecentImportsRoute(req, res) { + const {orm} = req.app.locals; + const queryPage = parseInt(req.query.page, 10) || 1; + const recentImportsData = await fetchRecentImportsData(orm, queryPage); + + res.send(recentImportsData); +} + +router.get('/', recentImportsRoute); +router.get('/raw', rawRecentImportsRoute); + +export default router; diff --git a/src/server/routes/import-entity/transform-form.js b/src/server/routes/import-entity/transform-form.js new file mode 100644 index 0000000000..dcc8c63728 --- /dev/null +++ b/src/server/routes/import-entity/transform-form.js @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2017 Ben Ockmore + * 2018 Shivam Tripathi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import {constructAliases, constructIdentifiers} from '../entity/entity'; +import _ from 'lodash'; + + +export function formToCreatorState(data) { + const aliases = constructAliases(data.aliasEditor, data.nameSection); + const identifiers = constructIdentifiers(data.identifierEditor); + + return { + aliases, + beginAreaId: data.creatorSection.beginArea && + data.creatorSection.beginArea.id, + beginDate: data.creatorSection.beginDate, + disambiguation: data.nameSection.disambiguation, + endAreaId: data.creatorSection.endArea && + data.creatorSection.endArea.id, + endDate: data.creatorSection.ended ? data.creatorSection.endDate : '', + ended: data.creatorSection.ended, + genderId: data.creatorSection.gender, + identifiers, + note: data.submissionSection.note, + type: 'Creator', + typeId: data.creatorSection.type + }; +} + +export function formToEditionState(data) { + const aliases = constructAliases(data.aliasEditor, data.nameSection); + const identifiers = constructIdentifiers(data.identifierEditor); + const languages = _.map(data.editionSection.languages, 'value'); + + let releaseEvents = []; + if (data.editionSection.releaseDate) { + releaseEvents = [{date: data.editionSection.releaseDate}]; + } + + return { + aliases, + depth: data.editionSection.depth && + parseInt(data.editionSection.depth, 10), + disambiguation: data.nameSection.disambiguation, + formatId: data.editionSection.format && + parseInt(data.editionSection.format, 10), + height: data.editionSection.height && + parseInt(data.editionSection.height, 10), + identifiers, + languages, + note: data.submissionSection.note, + pages: data.editionSection.pages && + parseInt(data.editionSection.pages, 10), + publicationBbid: data.editionSection.publication && + data.editionSection.publication.id, + publishers: data.editionSection.publisher && + [data.editionSection.publisher.id], + releaseEvents, + statusId: data.editionSection.status && + parseInt(data.editionSection.status, 10), + type: 'Edition', + weight: data.editionSection.weight && + parseInt(data.editionSection.weight, 10), + width: data.editionSection.width && + parseInt(data.editionSection.width, 10) + }; +} + +export function formToPublicationState(data) { + const aliases = constructAliases(data.aliasEditor, data.nameSection); + const identifiers = constructIdentifiers(data.identifierEditor); + + return { + aliases, + disambiguation: data.nameSection.disambiguation, + identifiers, + note: data.submissionSection.note, + type: 'Publication', + typeId: data.publicationSection.type + }; +} + +export function formToPublisherState(data) { + const aliases = constructAliases(data.aliasEditor, data.nameSection); + const identifiers = constructIdentifiers(data.identifierEditor); + + return { + aliases, + areaId: data.publisherSection.area && data.publisherSection.area.id, + beginDate: data.publisherSection.beginDate, + disambiguation: data.nameSection.disambiguation, + endDate: data.publisherSection.ended ? + data.publisherSection.endDate : '', + ended: data.publisherSection.ended, + identifiers, + note: data.submissionSection.note, + type: 'Publisher', + typeId: data.publisherSection.type + }; +} + +export function formToWorkState(data) { + const aliases = constructAliases(data.aliasEditor, data.nameSection); + const identifiers = constructIdentifiers(data.identifierEditor); + const languages = _.map(data.workSection.languages, 'value'); + + return { + aliases, + disambiguation: data.nameSection.disambiguation, + identifiers, + languages, + note: data.submissionSection.note, + type: 'Work', + typeId: data.workSection.type + }; +} + +export const transformForm = { + Creator: formToCreatorState, + Edition: formToEditionState, + Publication: formToPublicationState, + Publisher: formToPublisherState, + Work: formToWorkState +}; diff --git a/src/server/routes/import-entity/transform-import.js b/src/server/routes/import-entity/transform-import.js new file mode 100644 index 0000000000..87c3104a33 --- /dev/null +++ b/src/server/routes/import-entity/transform-import.js @@ -0,0 +1,192 @@ +/* + * Copyright (C) 2018 Shivam Tripathi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +/* This module contains functions to transform importEntities to form + sections for edit. The form sections are: + => aliasEditor, + => buttonBar, + => identifierEditor, + => nameSection, + => entitySection (where entity could any of the ) + These functions are used when we choose to edit and approve the + importEntities - i.e. we need to transform the existing importEntity to + the form layout as described in the `client/entity-editor` +*/ + +import _ from 'lodash'; + + +export function areaToOption(area) { + if (!area) { + return null; + } + const {id} = area; + return { + disambiguation: area.comment, + id, + text: area.name, + type: 'area' + }; +} + +function getAliasEditor(importEntity) { + const aliases = importEntity.aliasSet ? + importEntity.aliasSet.aliases.map(({language, ...rest}) => ({ + language: language.id, + ...rest + })) : []; + + const {defaultAlias} = importEntity; + defaultAlias.language = defaultAlias.languageId; + + const aliasEditor = {}; + aliases.forEach((alias) => { + if (alias.id !== defaultAlias.id) { + aliasEditor[alias.id] = alias; + aliasEditor[alias.id].default = alias.id === defaultAlias.id; + } + }); + + return aliasEditor; +} + +function getButtonBar(importEntity) { + return { + aliasEditorVisible: false, + disambiguationVisible: Boolean(importEntity.disambiguation), + identifierEditorVisible: false + }; +} + +function getNameSection(importEntity) { + const {defaultAlias, disambiguation} = importEntity; + const nameSection = defaultAlias ? defaultAlias : { + language: null, + name: '', + sortName: '' + }; + + nameSection.disambiguation = + disambiguation && disambiguation.comment; + + return nameSection; +} + +function getIdentifierEditor(importEntity) { + const {identifierSet} = importEntity; + const identifiers = identifierSet ? + identifierSet.identifiers.map(({type, ...rest}) => ({ + type: type.id, + ...rest + })) : []; + + const identifierEditor = {}; + identifiers.forEach( + (identifier) => { identifierEditor[identifier.id] = identifier; } + ); + + return identifierEditor; +} + +function getCreatorSection(creatorImport) { + return { + beginArea: areaToOption(creatorImport.beginArea), + beginDate: creatorImport.beginDate, + endArea: areaToOption(creatorImport.endArea), + endDate: creatorImport.endDate, + ended: creatorImport.ended, + gender: creatorImport.gender && creatorImport.gender.id, + type: creatorImport.creatorType && creatorImport.creatorType.id + }; +} + +function getEditionSection(editionImport) { + const physicalVisible = !( + _.isNull(editionImport.depth) && _.isNull(editionImport.height) && + _.isNull(editionImport.pages) && _.isNull(editionImport.weight) && + _.isNull(editionImport.width) + ); + + const releaseDate = editionImport.releaseEventSet && ( + _.isEmpty(editionImport.releaseEventSet.releaseEvents) ? + null : editionImport.releaseEventSet.releaseEvents[0].date + ); + + return { + depth: editionImport.depth, + format: editionImport.editionFormat && editionImport.editionFormat.id, + height: editionImport.height, + languages: + editionImport.languageSet ? + editionImport.languageSet.languages.map( + ({id, name}) => ({label: name, value: id}) + ) : [], + pages: editionImport.pages, + physicalVisible, + releaseDate, + status: editionImport.editionStatus && editionImport.editionStatus.id, + weight: editionImport.weight, + width: editionImport.width + }; +} + +function getPublicationSection(publicationImport) { + const {publicationType} = publicationImport; + return { + type: publicationType && publicationType.id + }; +} + +function getPublisherSection(publisherImport) { + return { + area: areaToOption(publisherImport.area), + beginDate: publisherImport.beginDate, + endDate: publisherImport.endDate, + ended: publisherImport.ended, + type: publisherImport.publisherType && publisherImport.publisherType.id + }; +} + +function getWorkSection(workImport) { + return { + languages: + workImport.languageSet ? workImport.languageSet.languages.map( + ({id, name}) => ({label: name, value: id}) + ) : [], + type: workImport.workType && workImport.workType.id + }; +} + +export const entitySectionMap = { + Creator: getCreatorSection, + Edition: getEditionSection, + Publication: getPublicationSection, + Publisher: getPublisherSection, + Work: getWorkSection +}; + +export function entityToFormState(importEntity) { + const entitySection = `${importEntity.type.toLowerCase()}Section`; + return { + aliasEditor: getAliasEditor(importEntity), + buttonBar: getButtonBar(importEntity), + [entitySection]: entitySectionMap[importEntity.type](importEntity), + identifierEditor: getIdentifierEditor(importEntity), + nameSection: getNameSection(importEntity) + }; +} diff --git a/src/server/routes/import-entity/work.js b/src/server/routes/import-entity/work.js new file mode 100644 index 0000000000..0caf8de5b2 --- /dev/null +++ b/src/server/routes/import-entity/work.js @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2018 Shivam Tripathi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import * as auth from '../../helpers/auth'; +import * as importRoutes from './import-routes'; +import * as middleware from '../../helpers/middleware'; +import * as utils from '../../helpers/utils'; +import express from 'express'; + + +const router = express.Router(); + +/* If the route specifies an importId, load the importEntity for it. */ +router.param( + 'importId', + middleware.makeImportLoader( + 'WorkImport', + ['workType', 'languageSet.languages'], + 'Work Import not found' + ) +); + + +function _setWorkTitle(res) { + res.locals.title = utils.createEntityPageTitle( + res.locals.entity, + 'Work (Import)', + utils.template`Work (Import) “${'name'}”` + ); +} + +router.get('/:importId', (req, res) => { + _setWorkTitle(res); + importRoutes.displayImport(req, res); +}); + +router.get('/:importId/discard', auth.isAuthenticated, (req, res) => { + _setWorkTitle(res); + importRoutes.displayDiscardImportEntity(req, res); +}); + +router.post( + '/:importId/discard/handler', + auth.isAuthenticatedForHandler, + importRoutes.handleDiscardImportEntity +); + +router.get( + '/:importId/approve', + auth.isAuthenticatedForHandler, + importRoutes.approveImportEntity +); + +router.get( + '/:importId/edit', + auth.isAuthenticated, middleware.loadIdentifierTypes, + middleware.loadWorkTypes, middleware.loadLanguages, + importRoutes.editImportEntity +); + +router.post( + '/:importId/edit/approve', + auth.isAuthenticatedForHandler, + importRoutes.approveImportPostEditing +); + +export default router;