From 7aa5f58922381b35e668f4ad220c7e4d3bfe00dd Mon Sep 17 00:00:00 2001 From: Matthew Connolly Date: Mon, 17 Feb 2020 16:15:42 -0500 Subject: [PATCH] Initial commit --- .gitignore | 1 + README | 3 + client/README.md | 68 ++++++++++ client/package.json | 42 ++++++ client/public/favicon.ico | Bin 0 -> 3870 bytes client/public/index.html | 43 ++++++ client/public/logo192.png | Bin 0 -> 5347 bytes client/public/logo512.png | Bin 0 -> 9664 bytes client/public/manifest.json | 25 ++++ client/public/robots.txt | 2 + client/src/App.css | 129 ++++++++++++++++++ client/src/App.js | 168 ++++++++++++++++++++++++ client/src/App.test.js | 9 ++ client/src/ArticleView.js | 65 ++++++++++ client/src/Bookbag.js | 49 +++++++ client/src/Citation.js | 14 ++ client/src/FirstDocuments.js | 26 ++++ client/src/SearchHeader.js | 30 +++++ client/src/SearchPage.js | 47 +++++++ client/src/SearchResult.js | 35 +++++ client/src/SearchResultList.js | 43 ++++++ client/src/SearchTips.js | 32 +++++ client/src/index.css | 13 ++ client/src/index.js | 13 ++ client/src/logo.svg | 7 + client/src/serviceWorker.js | 137 ++++++++++++++++++++ client/src/setupTests.js | 5 + client/src/utils.js | 68 ++++++++++ package.json | 16 +++ scripts/pubmed_to_mongo_new.rb | 230 +++++++++++++++++++++++++++++++++ server/article.js | 27 ++++ server/package.json | 18 +++ server/server.js | 139 ++++++++++++++++++++ 33 files changed, 1504 insertions(+) create mode 100644 .gitignore create mode 100644 README create mode 100644 client/README.md create mode 100644 client/package.json create mode 100644 client/public/favicon.ico create mode 100644 client/public/index.html create mode 100644 client/public/logo192.png create mode 100644 client/public/logo512.png create mode 100644 client/public/manifest.json create mode 100644 client/public/robots.txt create mode 100755 client/src/App.css create mode 100755 client/src/App.js create mode 100644 client/src/App.test.js create mode 100755 client/src/ArticleView.js create mode 100755 client/src/Bookbag.js create mode 100644 client/src/Citation.js create mode 100644 client/src/FirstDocuments.js create mode 100644 client/src/SearchHeader.js create mode 100755 client/src/SearchPage.js create mode 100755 client/src/SearchResult.js create mode 100644 client/src/SearchResultList.js create mode 100755 client/src/SearchTips.js create mode 100644 client/src/index.css create mode 100644 client/src/index.js create mode 100644 client/src/logo.svg create mode 100644 client/src/serviceWorker.js create mode 100644 client/src/setupTests.js create mode 100644 client/src/utils.js create mode 100644 package.json create mode 100644 scripts/pubmed_to_mongo_new.rb create mode 100644 server/article.js create mode 100644 server/package.json create mode 100644 server/server.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/README b/README new file mode 100644 index 0000000..0bfba70 --- /dev/null +++ b/README @@ -0,0 +1,3 @@ +* Requires a Mongo database named `pubmed` and a collection named `articles`. + +* The `pubmed_to_mongo_new.rb` script is designed to parse and convert Medline citation abstract files as described and accessed here: https://www.nlm.nih.gov/databases/download/pubmed_medline.html \ No newline at end of file diff --git a/client/README.md b/client/README.md new file mode 100644 index 0000000..89b278a --- /dev/null +++ b/client/README.md @@ -0,0 +1,68 @@ +This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). + +## Available Scripts + +In the project directory, you can run: + +### `yarn start` + +Runs the app in the development mode.
+Open [http://localhost:3000](http://localhost:3000) to view it in the browser. + +The page will reload if you make edits.
+You will also see any lint errors in the console. + +### `yarn test` + +Launches the test runner in the interactive watch mode.
+See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. + +### `yarn build` + +Builds the app for production to the `build` folder.
+It correctly bundles React in production mode and optimizes the build for the best performance. + +The build is minified and the filenames include the hashes.
+Your app is ready to be deployed! + +See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. + +### `yarn eject` + +**Note: this is a one-way operation. Once you `eject`, you can’t go back!** + +If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. + +Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. + +You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. + +## Learn More + +You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). + +To learn React, check out the [React documentation](https://reactjs.org/). + +### Code Splitting + +This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting + +### Analyzing the Bundle Size + +This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size + +### Making a Progressive Web App + +This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app + +### Advanced Configuration + +This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration + +### Deployment + +This section has moved here: https://facebook.github.io/create-react-app/docs/deployment + +### `yarn build` fails to minify + +This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify diff --git a/client/package.json b/client/package.json new file mode 100644 index 0000000..ccc2899 --- /dev/null +++ b/client/package.json @@ -0,0 +1,42 @@ +{ + "name": "pubmed-search", + "version": "0.1.0", + "private": true, + "dependencies": { + "@testing-library/jest-dom": "^4.2.4", + "@testing-library/react": "^9.3.2", + "@testing-library/user-event": "^7.1.2", + "axios": "^0.19.0", + "bootstrap": "^4.4.1", + "mongodb": "^3.4.0", + "mongodb-client-encryption": "^1.0.0", + "mongoose": "^5.8.1", + "react": "^16.12.0", + "react-dom": "^16.12.0", + "react-router-dom": "^5.1.2", + "react-scripts": "3.3.0", + "react-spinners": "^0.8.0" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "proxy": "http://localhost:3001", + "eslintConfig": { + "extends": "react-app" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/client/public/favicon.ico b/client/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..a11777cc471a4344702741ab1c8a588998b1311a GIT binary patch literal 3870 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 baj7?x%VD!5Cxk2YbYLNVeiXvvpMCWYo=by@ literal 0 HcmV?d00001 diff --git a/client/public/index.html b/client/public/index.html new file mode 100644 index 0000000..aa069f2 --- /dev/null +++ b/client/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React App + + + +
+ + + diff --git a/client/public/logo192.png b/client/public/logo192.png new file mode 100644 index 0000000000000000000000000000000000000000..fc44b0a3796c0e0a64c3d858ca038bd4570465d9 GIT binary patch literal 5347 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 b{|{}U^kRn2wo}j7|0ATu<;8xA7zX}7|B6mN literal 0 HcmV?d00001 diff --git a/client/public/manifest.json b/client/public/manifest.json new file mode 100644 index 0000000..080d6c7 --- /dev/null +++ b/client/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/client/public/robots.txt b/client/public/robots.txt new file mode 100644 index 0000000..01b0f9a --- /dev/null +++ b/client/public/robots.txt @@ -0,0 +1,2 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * diff --git a/client/src/App.css b/client/src/App.css new file mode 100755 index 0000000..621e296 --- /dev/null +++ b/client/src/App.css @@ -0,0 +1,129 @@ +.App { + /* text-align: center; */ + margin: 50px; +} + +.App-logo { + height: 40vmin; + pointer-events: none; +} + +@media (prefers-reduced-motion: no-preference) { + .App-logo { + animation: App-logo-spin infinite 20s linear; + } +} + +.App-header { + background-color: #282c34; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: calc(10px + 2vmin); + color: white; +} + +.App-link { + color: #dae2f0; +} + +.searchResult { + background-color: #ffffff; + padding: 10px; + margin: 5px; + border: 1px solid lightgray; +} + +.searchResult:hover { + background-color: #ffffcc; +} + +/* + ClickForDetail behavior taken from + https://stackoverflow.com/questions/2776109/css-rollover-one-element-and-make-another-element-visible +*/ +.clickForDetail { + display:none; + padding:2px 3px; + margin-right:8px; + width:130px; + float:right; +} + +.searchResult:hover .clickForDetail { + display:inline; + /* position:absolute; */ + background:#ffffff; + border:1px solid #cccccc; + color:#6c6c6c; +} + +.fullArticle { + background-color: #dae2f0; + padding: 5px; + margin: 10px; +} + +.fullArticle .title { + text-align: center; + font-size: larger; + margin: 3px; +} + +.fullArticle .journal { + text-align: center; +} + +.fullArticle .authors { + text-align: center; +} + +.close { + left: 20px; +} + +.keyword { + /* background-color: lightblue; */ + margin: 5px; + padding: 2px; +} + +.keyword:hover { + background-color: lightblue; +} + +.citation { + padding: 5px +} + +.hangingIndent { + padding-left: 22px ; + text-indent: -22px ; +} + +li { + padding: 8px; +} + +.example { + font-family: "Courier New", Courier, monospace; + background-color: #dddddd; + margin: 2px; + padding: 5px; +} + +.navlinks { + float: right; +} + +header { + background-color: #e0e0eb; + padding: 5px; + margin: 10px; +} + +body { + background-color: #efeff5; +} diff --git a/client/src/App.js b/client/src/App.js new file mode 100755 index 0000000..4130b00 --- /dev/null +++ b/client/src/App.js @@ -0,0 +1,168 @@ +import React, { Component, Fragment } from 'react'; +import axios from 'axios'; +import { BrowserRouter, Route, Link } from 'react-router-dom'; + +import './App.css'; +import Bookbag from './Bookbag'; +import SearchPage from './SearchPage'; +import SearchTips from './SearchTips'; + +class App extends Component { + // initialize state + state = { + articles: [], + articleCount: 0, + searchResults: [], + id: 0, + title: null, + intervalIsSet: false, + idToDelete: null, + idToUpdate: null, + objectToUpdate: null, + isSearching: false, + query: null, + success: true, + }; + + // when component mounts, first thing it does is fetch all existing data in our db + // then we incorporate a polling logic so that we can easily see if our db has + // changed and implement those changes into our UI + componentDidMount() { + this.getArticleCount() + // this.getDataFromDb(); + // if (!this.state.intervalIsSet) { + // let interval = setInterval(this.getDataFromDb, 1000); + // this.setState({ intervalIsSet: interval }); + // } + } + + // never let a process live forever + // always kill a process everytime we are done using it + componentWillUnmount() { + if (this.state.intervalIsSet) { + clearInterval(this.state.intervalIsSet); + this.setState({ intervalIsSet: null }); + } + } + + // just a note, here, in the front end, we use the id key of our data object + // in order to identify which we want to Update or delete. + // for our back end, we use the object id assigned by MongoDB to modify + // data base entries + + // our first get method that uses our backend api to + // fetch data from our data base + getDataFromDb = () => { + fetch('http://localhost:3001/api/getArticle') + .then((articles) => articles.json()) + .then((res) => this.setState({ articles: res.articles })); + }; + + getArticleCount = () => { + fetch('http://localhost:3001/api/getArticle/count') + .then(count => count.json()) + .then(res => this.setState({ articleCount: res.count})); + } + + findArticles = (query) => { + this.setState({ articles: [], isSearching: true }); + fetch(`http://localhost:3001/api/searchArticle/${query}`) + .then(articles => articles.json()) + .then(res => this.setState({ searchResults: res.articles, success: res.success, isSearching: false, query })); + } + + findKeyword = (query) => { + this.setState({ articles: [], isSearching: true }); + fetch(`http://localhost:3001/api/searchKeyword/${query}`) + .then(articles => articles.json()) + .then(res => this.setState({ searchResults: res.articles, success: res.success, isSearching: false, query: `keyword: ${query}` })); + } + + // our put method that uses our backend api + // to create new query into our data base + putDataToDB = (message) => { + let currentIds = this.state.articles.map((article) => article.id); + let idToBeAdded = 0; + while (currentIds.includes(idToBeAdded)) { + ++idToBeAdded; + } + + axios.post('http://localhost:3001/api/putArticle', { + id: idToBeAdded, + title: message, + }); + }; + + // our delete method that uses our backend api + // to remove existing database information + deleteFromDB = (idTodelete) => { + parseInt(idTodelete); + let objIdToDelete = null; + this.state.articles.forEach((article) => { + if (article.id == idTodelete) { + objIdToDelete = article._id; + } + }); + + axios.delete('http://localhost:3001/api/deleteArticle', { + data: { + id: objIdToDelete, + }, + }); + }; + + // our update method that uses our backend api + // to overwrite existing data base information + updateDB = (idToUpdate, updateToApply) => { + let objIdToUpdate = null; + parseInt(idToUpdate); + this.state.articles.forEach((article) => { + if (article.id == idToUpdate) { + objIdToUpdate = article._id; + } + }); + + axios.post('http://localhost:3001/api/updateArticle', { + id: objIdToUpdate, + update: { title: updateToApply }, + }); + }; + + // here is our UI + // it is easy to understand their functions when you + // see them render into our screen + render() { + const mainComponent = + +
+ +

CPEP - PubMed search

+
+ +
; + + return ( + +
+ {/*
*/} + mainComponent} /> + + +
+
+ ); + } +} + +export default App; diff --git a/client/src/App.test.js b/client/src/App.test.js new file mode 100644 index 0000000..4db7ebc --- /dev/null +++ b/client/src/App.test.js @@ -0,0 +1,9 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import App from './App'; + +test('renders learn react link', () => { + const { getByText } = render(); + const linkElement = getByText(/learn react/i); + expect(linkElement).toBeInTheDocument(); +}); diff --git a/client/src/ArticleView.js b/client/src/ArticleView.js new file mode 100755 index 0000000..313b28a --- /dev/null +++ b/client/src/ArticleView.js @@ -0,0 +1,65 @@ +import React, { useState } from 'react'; + +import { subjectsDisplay, publicationDisplay, saveArticle } from './utils'; + +const ArticleView = ({article, toggle, keywordSearchHandler}) => { + + const [saved, setSaved] = useState(false); + + const mySearchHandler = (search) => { + console.log("handling search", search) + keywordSearchHandler(search) + } + + console.log("article being shown", article) + + let doi = null + if (article.article_ids) { + console.log("article ids", article.article_ids) + doi = article.article_ids.doi + // if (typeof article.article_ids === 'string') { + // // Force to array + // article.article_ids = article.article_ids.split(); + // } + // article.article_ids.keys.forEach(id => { + // if (id.match(/\//)) { + // doi = id; + // } + // }) + } + + return ( +
+ X +
{article.title}
+
in {publicationDisplay(article)}
+
+
{article.authors.join('; ')}
+
+
Subjects +
+ {subjectsDisplay(article.subjects, mySearchHandler)} +
+
+ { article.abstract && +
+ Abstract +

{article.abstract}

+
+ } +
+ {doi && + DOI: {doi} + } +

+ + {saved && +

Saved!

+ } +


+
{article.sourceFile}
+
+ ); +} + +export default ArticleView; \ No newline at end of file diff --git a/client/src/Bookbag.js b/client/src/Bookbag.js new file mode 100755 index 0000000..fc3b894 --- /dev/null +++ b/client/src/Bookbag.js @@ -0,0 +1,49 @@ +import React, { Fragment } from 'react'; +import { Link, useHi } from 'react-router-dom'; + +import Citation from './Citation'; +import { getSavedArticles, clearBookbag } from './utils'; + +const Bookbag = () => { + + const savedArticles = getSavedArticles(); + + const confirmClear = () => { + if (window.confirm('Are you sure you wish to clear the bookbag? All saved article citations will be lost.')) { + clearBookbag(); + window.location.reload(false); + } + } + + const count = savedArticles.length; + const verbForm = (number) => number !== 1 ? 'are' : 'is'; + + return ( + +
+ +

CPEP - PubMed search

+

Bookbag

+
+
+ {count < 1 && +

Your bookbag is empty.

+ } + {count >= 1 && +
+ +
+ There {verbForm(count)} {savedArticles ? savedArticles.length : '0'} saved article{count !== 1 ? 's' : ''} in the bookbag. +

+

Article citations

+ {savedArticles.map(article => )} +
+
+ } +
+ ); +} + +export default Bookbag; \ No newline at end of file diff --git a/client/src/Citation.js b/client/src/Citation.js new file mode 100644 index 0000000..2d2f34c --- /dev/null +++ b/client/src/Citation.js @@ -0,0 +1,14 @@ +import React from 'react'; + +const Citation = ({article}) => { + + const authors = article.authors.length > 2 ? article.authors.slice(0,2).join('; ') + '; et al' : article.authors.join('; '); + + return ( +
+ {authors}. {article.title} {article.journal} {article.pubDate.year}; {article.issue.volume} ({article.issue.issue}). doi: {article.article_ids.doi}, pmid: {article.article_ids.pubmed}. +
+ ); +} + +export default Citation; \ No newline at end of file diff --git a/client/src/FirstDocuments.js b/client/src/FirstDocuments.js new file mode 100644 index 0000000..f48f5f5 --- /dev/null +++ b/client/src/FirstDocuments.js @@ -0,0 +1,26 @@ +import React, { Fragment } from 'react'; + +const FirstDocuments = ({index}) => { + // const docs = index.documentStore.docs; + // const numDocsToShow = Math.min(10, index.documentStore.length); + + // The document store in the index is set up as a hash in the form: + // { + // id1: { document 1} + // id2: { document 2} + // ... + // } + // This means we have to handle the (unknown) keys to traverse the structure + // let keyArray = Object.keys(docs); + + return ( + + {/*

First {numDocsToShow} articles in index

+
    + { keyArray.map(k =>
  • {docs[k].title}
  • ) } +
*/} +
+ ); +} + +export default FirstDocuments; diff --git a/client/src/SearchHeader.js b/client/src/SearchHeader.js new file mode 100644 index 0000000..f3719a1 --- /dev/null +++ b/client/src/SearchHeader.js @@ -0,0 +1,30 @@ +import React, { Fragment, useState } from 'react'; + +const SearchHeader = ({searchHandler}) => { + const [query, setQuery] = useState(); + + const handleChange = event => { + setQuery(event.target.value); + }; + + const handleSearch = event => { + event.preventDefault() + searchHandler(query); + } + + const handleKeyup = event => { + if (event.keyCode === 13) searchHandler(query); + } + + return ( + +
+ +
+ ); +} + +export default SearchHeader; \ No newline at end of file diff --git a/client/src/SearchPage.js b/client/src/SearchPage.js new file mode 100755 index 0000000..4afcb4c --- /dev/null +++ b/client/src/SearchPage.js @@ -0,0 +1,47 @@ +import React, { Fragment } from 'react'; +import { css } from "@emotion/core"; +import MoonLoader from "react-spinners/MoonLoader"; + +import FirstDocuments from './FirstDocuments'; +import SearchHeader from './SearchHeader'; +import SearchResultList from './SearchResultList'; +import { commaNumber } from './utils'; + +const SearchPage = ({articleCount, searchHandler, keywordSearchHandler, isSearching, success, searchResults, query}) => { + + // Can be a string as well. Need to ensure each key-value pair ends with ; + const override = css` + border-color: blue; + `; + + // const [searchResults, setSearchResults] = useState(); + console.log("getting results", searchResults) + return ( + +

Search Page

+

Searching {commaNumber(articleCount)} article citations

+ + + { isSearching && +
+ + Searching ... +
+ } + { !isSearching && success && + + } + { !isSearching && !success && +

Your search could not be completed. Your topic may be too broad; try narrowing your search terms.

+ } +
+ ); +} + +export default SearchPage; diff --git a/client/src/SearchResult.js b/client/src/SearchResult.js new file mode 100755 index 0000000..733864f --- /dev/null +++ b/client/src/SearchResult.js @@ -0,0 +1,35 @@ +import React, { useState } from 'react'; + +import ArticleView from './ArticleView'; +import { subjectsDisplay, pubDateDisplay } from './utils'; + +const SearchResult = ({article, keywordSearchHandler}) => { + const [showArticleView, setArticleView] = useState(false); + + const toggleArticleView = () => { + setArticleView(!showArticleView) + } + + const mySearchHandler = (search) => { + console.log("handling search", search) + keywordSearchHandler(search) + } + + if (showArticleView) { + return + } + else { + return ( +
+ Click for details + {article.title} {pubDateDisplay(article.pubDate)} +

+ Subjects: {subjectsDisplay(article.subjects, mySearchHandler)} +
+ ); + } +} + +export default SearchResult; + +//
  • {r.title}
  • \ No newline at end of file diff --git a/client/src/SearchResultList.js b/client/src/SearchResultList.js new file mode 100644 index 0000000..63539b6 --- /dev/null +++ b/client/src/SearchResultList.js @@ -0,0 +1,43 @@ +import React, { Fragment } from 'react'; + +import SearchResult from './SearchResult'; +import { commaNumber } from './utils'; + +const SearchResultList = ({query, results, keywordSearchHandler}) => { + const top50 = results.slice(0,50); + + return ( + + { results.length > 0 && +
    +

    Search Results ({top50.length} of {commaNumber(results.length)}) for '{query}'

    +
      + {top50.map(r => )} +
    +
    + } +
    + ); +} + +export default SearchResultList; + + + + +// const query = $('#query').val(); +// results = index.search(query); +// console.log(results) +// resultText = '

    Search results

    '; +// if (results.length < 1) { +// resultText += 'No results found'; +// } +// else { +// resultText += '

    Found ' + results.length + ' results. Top 50:'; +// resultText += '

      ' +// let maxCount = (results.length < 10) ? results.length : 50; +// for (let i = 0; i < maxCount; i++) { +// console.log("") +// resultText += '
    1. ' + results[i]['doc']['title'] + ' (relevance: ' + results[i]['score'].toFixed(2) + ')
    2. '; +// } +// resultText += '
    ' \ No newline at end of file diff --git a/client/src/SearchTips.js b/client/src/SearchTips.js new file mode 100755 index 0000000..42cd37a --- /dev/null +++ b/client/src/SearchTips.js @@ -0,0 +1,32 @@ +import React, { Fragment } from 'react'; +import { Link } from 'react-router-dom'; + +const SearchTips = () => { + return ( + +
    + +

    CPEP - PubMed search

    +
    +

    How to search

    +
      +
    1. By default, words entered into the search box are treated as an OR boolean search. For example, typing aspirin tylenol will return a list of all article citations that include the term "aspirin" or the term "tylenol." +
    2. +
    3. You can search for a phrase by entering a set of words in quotation marks. E.g., typing "patient care" will result in a search for the words patient and care together, not individually.
    4. +
    5. You can exclude terms from your search by prefacing them with a hypen (-). If you wanted to modify the previous search so that you only saw articles about patient care that did not involve cancer, you could type "patient care" -cancer.
    6. +
    7. Clicking one of the highlighted keywords in an article citation from a previous search result will immediately perform a keyword search for that term. This type of search returns a list of article citations that include that term only as a subject keyword.
    8. +
    +

    Notes on search

    +
      +
    1. Searches are case-insensitive and diacritic-insensitive. Regular text searches (i.e., non-keyword searches) look for query terms in a citation's title, abstract, and keywords, and are ranked by relevance.
    2. +
    3. The search engine uses a stemmer. Thus, for example, a search for "puppies" will include results for "puppy" as well.
    4. +
    5. Searches that are too broad (e.g., "cancer"), will result in an error. You will see the message Your search could not be completed. Your topic may be too broad; try narrowing your search terms.. This is a limitation of the system due to computer memory constraints. If you encounter this error, redo your search with a more limited query, e.g. "pancreatic cancer."
    6. +
    7. More complex searches may take longer to produce results.
    8. +
    +
    + ); +} + +export default SearchTips; \ No newline at end of file diff --git a/client/src/index.css b/client/src/index.css new file mode 100644 index 0000000..ec2585e --- /dev/null +++ b/client/src/index.css @@ -0,0 +1,13 @@ +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} diff --git a/client/src/index.js b/client/src/index.js new file mode 100644 index 0000000..f4729f8 --- /dev/null +++ b/client/src/index.js @@ -0,0 +1,13 @@ +import "bootstrap/dist/css/bootstrap.min.css"; +import React from 'react'; +import ReactDOM from 'react-dom'; +import './index.css'; +import App from './App'; +import * as serviceWorker from './serviceWorker'; + +ReactDOM.render(, document.getElementById('root')); + +// If you want your app to work offline and load faster, you can change +// unregister() to register() below. Note this comes with some pitfalls. +// Learn more about service workers: https://bit.ly/CRA-PWA +serviceWorker.unregister(); diff --git a/client/src/logo.svg b/client/src/logo.svg new file mode 100644 index 0000000..6b60c10 --- /dev/null +++ b/client/src/logo.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/client/src/serviceWorker.js b/client/src/serviceWorker.js new file mode 100644 index 0000000..8703ddb --- /dev/null +++ b/client/src/serviceWorker.js @@ -0,0 +1,137 @@ +// This optional code is used to register a service worker. +// register() is not called by default. + +// This lets the app load faster on subsequent visits in production, and gives +// it offline capabilities. However, it also means that developers (and users) +// will only see deployed updates on subsequent visits to a page, after all the +// existing tabs open on the page have been closed, since previously cached +// resources are updated in the background. + +// To learn more about the benefits of this model and instructions on how to +// opt-in, read https://bit.ly/CRA-PWA + +const isLocalhost = Boolean( + window.location.hostname === 'localhost' || + // [::1] is the IPv6 localhost address. + window.location.hostname === '[::1]' || + // 127.0.0.0/8 are considered localhost for IPv4. + window.location.hostname.match( + /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ + ) +); + +export function register(config) { + if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { + // The URL constructor is available in all browsers that support SW. + const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); + if (publicUrl.origin !== window.location.origin) { + // Our service worker won't work if PUBLIC_URL is on a different origin + // from what our page is served on. This might happen if a CDN is used to + // serve assets; see https://github.com/facebook/create-react-app/issues/2374 + return; + } + + window.addEventListener('load', () => { + const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; + + if (isLocalhost) { + // This is running on localhost. Let's check if a service worker still exists or not. + checkValidServiceWorker(swUrl, config); + + // Add some additional logging to localhost, pointing developers to the + // service worker/PWA documentation. + navigator.serviceWorker.ready.then(() => { + console.log( + 'This web app is being served cache-first by a service ' + + 'worker. To learn more, visit https://bit.ly/CRA-PWA' + ); + }); + } else { + // Is not localhost. Just register service worker + registerValidSW(swUrl, config); + } + }); + } +} + +function registerValidSW(swUrl, config) { + navigator.serviceWorker + .register(swUrl) + .then(registration => { + registration.onupdatefound = () => { + const installingWorker = registration.installing; + if (installingWorker == null) { + return; + } + installingWorker.onstatechange = () => { + if (installingWorker.state === 'installed') { + if (navigator.serviceWorker.controller) { + // At this point, the updated precached content has been fetched, + // but the previous service worker will still serve the older + // content until all client tabs are closed. + console.log( + 'New content is available and will be used when all ' + + 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' + ); + + // Execute callback + if (config && config.onUpdate) { + config.onUpdate(registration); + } + } else { + // At this point, everything has been precached. + // It's the perfect time to display a + // "Content is cached for offline use." message. + console.log('Content is cached for offline use.'); + + // Execute callback + if (config && config.onSuccess) { + config.onSuccess(registration); + } + } + } + }; + }; + }) + .catch(error => { + console.error('Error during service worker registration:', error); + }); +} + +function checkValidServiceWorker(swUrl, config) { + // Check if the service worker can be found. If it can't reload the page. + fetch(swUrl, { + headers: { 'Service-Worker': 'script' } + }) + .then(response => { + // Ensure service worker exists, and that we really are getting a JS file. + const contentType = response.headers.get('content-type'); + if ( + response.status === 404 || + (contentType != null && contentType.indexOf('javascript') === -1) + ) { + // No service worker found. Probably a different app. Reload the page. + navigator.serviceWorker.ready.then(registration => { + registration.unregister().then(() => { + window.location.reload(); + }); + }); + } else { + // Service worker found. Proceed as normal. + registerValidSW(swUrl, config); + } + }) + .catch(() => { + console.log( + 'No internet connection found. App is running in offline mode.' + ); + }); +} + +export function unregister() { + if ('serviceWorker' in navigator) { + navigator.serviceWorker.ready.then(registration => { + registration.unregister(); + }); + } +} diff --git a/client/src/setupTests.js b/client/src/setupTests.js new file mode 100644 index 0000000..74b1a27 --- /dev/null +++ b/client/src/setupTests.js @@ -0,0 +1,5 @@ +// jest-dom adds custom jest matchers for asserting on DOM nodes. +// allows you to do things like: +// expect(element).toHaveTextContent(/react/i) +// learn more: https://github.com/testing-library/jest-dom +import '@testing-library/jest-dom/extend-expect'; diff --git a/client/src/utils.js b/client/src/utils.js new file mode 100644 index 0000000..165e790 --- /dev/null +++ b/client/src/utils.js @@ -0,0 +1,68 @@ +import React from 'react'; + +export const subjectsDisplay = (subjects, searchHandler) => { + const linkedSubjects = subjects.map(s => {e.stopPropagation(); searchHandler(s)}}>{s}); + return linkedSubjects; +} + +// Display publication date. Input date should be an object of the form +// { year, month, day } +export const pubDateDisplay = (date) => { + if (!date) return ''; + + var months = { + 'Jan' : '01', + 'Feb' : '02', + 'Mar' : '03', + 'Apr' : '04', + 'May' : '05', + 'Jun' : '06', + 'Jul' : '07', + 'Aug' : '08', + 'Sep' : '09', + 'Oct' : '10', + 'Nov' : '11', + 'Dec' : '12' + } + // If the month is represented by an abbreviation (Jan, Feb, etc.), convert to numeric for display + // (this is inconsistent in the source data) + if (date && date.month && months[date.month]) date.month = months[date.month]; + if (date.month && date.year) + return [date.month, date.day, date.year].filter(Boolean).join('/'); + else if (date.year) + return date.year; +} + +export const publicationDisplay = (article) => { + console.log("issue", article.issue) + const journal = article.journal ? article.journal: null + const volume = article.issue && article.issue.volume ? `vol. ${article.issue.volume}` : null; + const issue = article.issue && article.issue.issue ? `issue ${article.issue.issue}` : null; + const date = pubDateDisplay(article.pubDate); + return [journal, volume, issue, date].filter(Boolean).join(', '); +} + +// See https://stackoverflow.com/questions/2901102/how-to-print-a-number-with-commas-as-thousands-separators-in-javascript +// for following regex +export const commaNumber = (number) => number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); + +// Retrieve an array of saved articles from sessionStorage +export const getSavedArticles = () => { + const savedArticlesString = sessionStorage.getItem('pubmedSavedArticles'); + return savedArticlesString ? JSON.parse(savedArticlesString) : []; +}; + +// Add a new article to the array of saved articles in sessionStorage +export const saveArticle = (article) => { + if (article) { + let savedArticles = getSavedArticles(); + savedArticles.push(article); + sessionStorage.setItem('pubmedSavedArticles', JSON.stringify(savedArticles)); + } +} + +// Delete all items from sessionStorage +export const clearBookbag = () => { + sessionStorage.removeItem('pubmedSavedArticles'); +}; + diff --git a/package.json b/package.json new file mode 100644 index 0000000..8f06d8b --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "pubmed-offline", + "version": "1.0.0", + "description": "A simple app for browsing PubMed/Medline citations using an offline Mongo database", + "main": "index.js", + "scripts": { + "start": "concurrently \"cd server && node --max-old-space-size=8192 server.js\" \"cd client && npm start\"", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "Matthew Connolly", + "license": "ISC", + "dependencies": { + "concurrently": "^5.0.1" + } +} diff --git a/scripts/pubmed_to_mongo_new.rb b/scripts/pubmed_to_mongo_new.rb new file mode 100644 index 0000000..e83bc0c --- /dev/null +++ b/scripts/pubmed_to_mongo_new.rb @@ -0,0 +1,230 @@ +# Parse Pubmed XML files directly into Mongo docs without bothering with JSON conversion +require 'nokogiri' +require 'mongo' +require 'json' +require 'benchmark' + +class CitationParser < Nokogiri::XML::SAX::Document + ############ SET THESE TO DEBUG ############ + # NOT YET IMPLEMENTED -- good idea but too ambitious for now + # @@debug = true # if set, print the original article citation and the result after processing + # @@stop_after_article = 1 # Number of articles to process before stopping + # # @@stop_after_file = 1 # Number of files to process before stopping + # @@articles_to_print = [] # Print the citation and post-process record for specific article PMIDs + # @@elements_to_print = [] + # IMPLEMENTED: + @@print_result = false # If set, print the post-process record + @@no_mongo = false # if set, skip the save to Mongo + @@show_tags_and_content = false # if set, show each opening tag, content, and closing tag as parsed + ############################################ + + # We need the parser to pull out the relevant fields that we want to retain + # for each article in the index + # Currently: + # 1. ids (esp doi) + # 2. journal title + # 3. article title + # 4. publication date + # 5. abstract + # 6. subjects [could be mesh headings or keywords] + # 7. authors + # 8. journal issue (volume and issue number) + + def start_document + @processed_articles = [] + @text = '' + + # Set up connection to Mongo DB and articles collection + Mongo::Logger.logger.level = Logger::ERROR # (https://stackoverflow.com/questions/30292100/how-can-i-disable-mongodb-log-messages-in-console) + @client = Mongo::Client.new([ 'localhost:27017' ], :database => 'pubmed') + @collection = @client[:articles] + @file_collection = @client[:processed_files] + end + + + + def start_element(element, attrs) + puts "opened tag: #{element}" if @@show_tags_and_content + + case element + when 'PubmedArticle' + # Start of article -- begin with a clean data structure + @article = { + :article_ids => {}, # PubMed ID, DOI, etc + :authors => [], + :pubDate => {}, + :issue => {}, + :subjects => [] # MESH headings and keywords + } + when 'ArticleId' + # id_type can be :pubmed, :doi, etc. + @id_type = attrs[0][1].to_sym + when 'ReferenceList' + # We don't care about references, and their s get confused + # with those of the actual article + @ignore_children = true + when 'PubDate' + @in_pub_date = true + @pub_date = {} + when 'Author' + @in_author = true + @author = {} + when 'Title' + # This is *journal* title, not article title + @in_journal_title = true + @journal_title = '' + when 'MeshHeading' + @in_mesh_heading = true + @mesh_descriptor = nil + @mesh_qualifiers = [] + when 'ArticleTitle', 'Abstract' + # These are the 'parent tags' of concern, as they're the ones that sometimes have + # and tags in them. When that happens, the text gets fragmented and has to be + # processed as an array, paying special attention to the parent and child tags so + # nothing gets lost. + @parent_tag_parts = [] + when 'sup', 'sub' + # 'sup' and 'sub' require special handling -- they're HTML embedded in the XML text fields, + # usually in or , but Nokogiri doesn't seem to know the difference + # and treats them as new XML tags, confusing matters. + @parent_tag_parts << @text.strip! + @text = '' + end + end + + + + def characters(str) + @text += str + end + + + + def end_element(element) + @text.strip! + + if @@show_tags_and_content + puts "content: #{@text}" + puts "closed tag: #{element}" + end + + # End of article + if element == 'PubmedArticle' + #@article[:sourceFile] = f + @processed_articles << @article + puts "Post-process record: #{JSON.pretty_generate(@article)}" if @@print_result + # exit + end + + # Main tag handling logic + case element + when 'ArticleId' + @article[:article_ids][@id_type] = @text + when 'Title' + # This is *journal* title + @article[:journal] = @text + when 'Volume' + @article[:issue][:volume] = @text + when 'Issue' + @article[:issue][:issue] = @text + when 'Year' + @pub_date[:year] = @text if @in_pub_date + when 'Month' + @pub_date[:month] = @text if @in_pub_date + when 'Day' + @pub_date[:day] = @text if @in_pub_date + when 'PubDate' + if @in_pub_date + @article[:pubDate] = @pub_date + @in_pub_date = false + end + when 'ForeName' + @author[:forename] = @text if @in_author + when 'LastName' + @author[:lastname] = @text if @in_author + when 'Author' + if @in_author + author = nil + if @author[:lastname] + author = @author[:lastname] + if @author[:forename] + author += ", #{@author[:forename]}" + end + end + @article[:authors] << author unless author.nil? + @in_author = false + end + when 'sup', 'sub' + @parent_tag_parts << "<#{element}>#{@text}" + when 'ArticleTitle' + @parent_tag_parts << @text + @article[:title] = @parent_tag_parts.join(' ') + #puts "Final #{element}: " + @parent_tag_parts.join(' ') + when 'AbstractText' + @parent_tag_parts << @text + when 'Abstract' + @article[:abstract] = @parent_tag_parts.join(' ') + #puts "Final #{element}: " + @parent_tag_parts.join(' ') + when 'Keyword' + @article[:subjects] << @text + when 'DescriptorName' + @mesh_descriptor = @text if @in_mesh_heading + when 'QualifierName' + @mesh_qualifiers << @text if @in_mesh_heading + when 'MeshHeading' + if @in_mesh_heading + if @mesh_qualifiers.length > 0 + @mesh_qualifiers.each do |mq| + mq.strip! + @article[:subjects] << "#{@mesh_descriptor} -- #{mq}" + end + else + @article[:subjects] << @mesh_descriptor + end + @in_mesh_heading = false + @mesh_qualifiers = [] + @mesh_descriptor = nil + end + when 'ReferenceList' + @ignore_children = false + end + + @text = '' + end + + + def end_document + puts "Done! Processed #{@processed_articles.count} articles" + # Add to Mongo + if !@@no_mongo + result = @collection.insert_many(@processed_articles) + puts "Inserted #{result.inserted_count} records into Mongo DB" + else + puts "Skipping Mongo insert as directed" + end + end + +end + +# Main program +parser = Nokogiri::XML::SAX::Parser.new(CitationParser.new) +if ARGV[0] + # PRocess the single file specified + files = [ARGV[0]] +else + files = Dir['source_files/originals/*'] +end +files.each do |f| + puts "Processing #{f}" + puts Benchmark.measure { + parser.parse_file f + } +end + +# Save list of processed files to Mongo +Mongo::Logger.logger.level = Logger::ERROR # (https://stackoverflow.com/questions/30292100/how-can-i-disable-mongodb-log-messages-in-console) +@client = Mongo::Client.new([ 'localhost:27017' ], :database => 'pubmed') +@collection = @client[:processed_files] +processed_files = files.map { |f| { :filename => File.basename(f) } } +@collection.insert_many(processed_files) +puts "\nDone with everything!\n\n" \ No newline at end of file diff --git a/server/article.js b/server/article.js new file mode 100644 index 0000000..26e0351 --- /dev/null +++ b/server/article.js @@ -0,0 +1,27 @@ +const mongoose = require("mongoose"); +const Schema = mongoose.Schema; + +// this will be our data base's data structure +const ArticleSchema = new Schema( + { + id: Number, + article_ids: { + type: Object, + }, + journal: String, + title: String, + authors: [String], + pubDate: { + year: String, + month: String, + day: String, + }, + subjects: [String], + abstract: String, + sourceFile: String, + }, + { timestamps: true } +); + +// export the new Schema so we could modify it using Node.js +module.exports = mongoose.model("Article", ArticleSchema); \ No newline at end of file diff --git a/server/package.json b/server/package.json new file mode 100644 index 0000000..fb77ac5 --- /dev/null +++ b/server/package.json @@ -0,0 +1,18 @@ +{ + "name": "backend", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "body-parser": "^1.19.0", + "cors": "^2.8.5", + "express": "^4.17.1", + "mongoose": "^5.8.1", + "morgan": "^1.9.1" + } +} diff --git a/server/server.js b/server/server.js new file mode 100644 index 0000000..c3f0e92 --- /dev/null +++ b/server/server.js @@ -0,0 +1,139 @@ + +const mongoose = require('mongoose'); +const express = require('express'); +var cors = require('cors'); +const bodyParser = require('body-parser'); +const logger = require('morgan'); +const Article = require('./article'); + +const API_PORT = 3001; +const app = express(); +app.use(cors()); +const router = express.Router(); + +// this is our MongoDB database +const dbRoute = 'mongodb://localhost:27017/pubmed' + +// connects our back end code with the database +mongoose.connect(dbRoute, { useNewUrlParser: true }); + +let db = mongoose.connection; + +db.once('open', () => console.log('connected to the database')); + +// checks if connection with the database is successful +db.on('error', console.error.bind(console, 'MongoDB connection error:')); + +// (optional) only made for logging and +// bodyParser, parses the request body to be a readable json format +app.use(bodyParser.urlencoded({ extended: false })); +app.use(bodyParser.json()); +app.use(logger('dev')); + +router.get('/getArticle/count', (req, res) => { + Article.countDocuments().exec((err, count) => { + if (err) { + res.send(err); + return; + } + console.log("count is", count) + return res.json({ count: count }); + }); +}) + +// Search method +router.get('/searchArticle/:query', (req, res) => { + console.log("request", req.params) + //Article.find({$text: {$search: req.params.query}}, { score: { $meta: "textScore" } }, (err, articles) => { + Article.find( + { $text: { $search : req.params.query }}, + { score: { $meta: 'textScore'}} + ) + //.sort({ score: { $meta : 'textScore' } }) + .exec((err, articles) => { + console.log("execing - err: ", err) + if (err) return res.json({ success: false, error: err }); + return res.json({ success: true, articles: articles }); + }); +}); + +// Search by subject/keyword +router.get('/searchKeyword/:query', (req, res) => { + Article.find({ subjects: req.params.query }, (err, articles) => { + if (err) return res.json({ success: false, error: err }); + return res.json({ success: true, articles: articles }); + }); +}); + +// this is our get method +// this method fetches all available data in our database +router.get('/getArticle', (req, res) => { + console.log("request", req.params) + Article.find( (err, articles) => { + if (err) return res.json({ success: false, error: err }); + return res.json({ success: true, articles: articles }); + }); +}); + +// this is our update method +// this method overwrites existing data in our database +router.post('/updateArticle', (req, res) => { + const { id, update } = req.body; + Article.findByIdAndUpdate(id, update, (err) => { + if (err) return res.json({ success: false, error: err }); + return res.json({ success: true }); + }); +}); + +// this is our delete method +// this method removes existing data in our database +router.delete('/deleteArticle', (req, res) => { + const { id } = req.body; + Article.findByIdAndRemove(id, (err) => { + if (err) return res.send(err); + return res.json({ success: true }); + }); +}); + +// this is our create methid +// this method adds new data in our database +router.post('/putArticle', (req, res) => { + let article = new Article(); + + const { + id, + title, + journal, + authors, + pubDate, + subjects, + abstract, + sourceFile, + } = req.body; + + if ((!id && id !== 0) || !title) { + return res.json({ + success: false, + error: 'INVALID INPUTS', + }); + } + article.title = title; + article.id = id; + article.journal = journal; + article.authors = authors; + article.pubDate = pubDate; + article.subjects = subjects; + article.abstract = abstract; + article.sourceFile = sourceFile; + + article.save((err) => { + if (err) return res.json({ success: false, error: err }); + return res.json({ success: true }); + }); +}); + +// append /api for our http requests +app.use('/api', router); + +// launch our backend into a port +app.listen(API_PORT, () => console.log(`LISTENING ON PORT ${API_PORT}`)); \ No newline at end of file