Commit 182701f1 authored by Darnell, Wade's avatar Darnell, Wade
Browse files

Initial commit

parents
.DS_Store
src/main/resources/static
node_modules/
dist/
package-lock.json
/target/
tmp/
bu/
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
# Editor directories and files
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
### NetBeans ###
/nbproject/private/
/build/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### Misc ###
.settings
.classpath
.factorypath
.project
# poster-api-wrapper
The Poster API Wrapper is a standalone web application which enables the searching, storing,
and displaying of abstracts featured at the annual AGU/AMS meetings.
This application is designed for those who wish to host a standalone web application and feature a select subset of
posters scheduled for the AGU/AMS meetings.
Please read the entirety of this README.md file before attempting to download and use the application.
There are three views available via the UI:
* Lookup - enables searching and submitting posters for displaying on the summary view
* Manual Submission - allows individual manual submission of posters given a poster ID
* Summary - displays all posters submitted by users from the lookup view
## Minimum requirements
* npm - `6.14.6`
* Maven - `3.6.0`
* Java - `1.8`
* A running MySQL database - `15.1`
* Access to Google reCAPTCHA site and secret keys
* [(reCAPTCHA intro)](https://developers.google.com/recaptcha/intro)
* A running Tomcat 8 instance
* [(Download link)](https://tomcat.apache.org/download-80.cgi)
* Note the `tomcat-users.xml` file needs to have proper `manager-script` permissions in order to deploy
## Setup
The first step is to configure the project.
There are three configuration files that must be updated prior to building the project.
These files, along with descriptions of the fields that must be updated, are listed below.
- `/pom.xml` - Edit the following fields in the `default` profile
- `<url>` - The URL of your Tomcat 8 instance where the project will be deployed (for example, `http://localhost:8080/manager/text`)
- `<server>` - The name of the server in your Maven `settings.xml` file
- `/app/.env`
- `VUE_APP_CHA` - The Google reCAPTHCA site key for the UI
- `/src/main/resources/application.properties`
- `poster.api.meeting` - The meeting name, lowercase (example: `agu` or `ams`)
- `poster.api.meeting-key` - The meeting key; see details and examples below
- `poster.api.year-full` - All four digits of year of event (for example, 2021)
- `app.api.google-recaptcha-key` - The Google reCAPTCHA secret key for the API
- `spring.datasource.url` - NOTE: Only change host and port; do not alter any other part of the datasource URL
- `spring.datasource.username` - The datasource username
- `spring.datasource.password` - The datasource password
- `app.allowed-origins` - The list of allowed origins, comma separated
- `logging.path` - The path to the desired logging directory on the server
#### `poster.api.meeting-key`
You will need to know the meeting key used in the AGU/AMS Confex API.
Below are examples of AGU and AMS Confex API URLs from 2021:
https://agu.confex.com/agu/fm21/meetingapi.cgi/
https://ams.confex.com/ams/101ANNUAL/meetingapi.cgi/
In the AGU example, the meeting key is `fm21`, whereas in the AMS example, the meeting key is `101ANNUAL`.
You will need to determine the correct meeting key to use depending on the site in which you are interested.
If you know the website UI URL, you can get the meeting key from there, as the Confex site UI and API URLs are very similar.
Below are the corresponding UI URLs - notice the only difference between the these and the API URLs above is
`meetingapi.cgi` is changed to `meetingapp.cgi`:
https://agu.confex.com/agu/fm21/meetingapp.cgi/
https://ams.confex.com/ams/101ANNUAL/meetingapp.cgi/
As shown above, the meeting keys are the same in the UI URLs as in the API URLs.
### Install
Once the project is configured as previously described, we can build the application.
Building the application is as simple as running the install script.
To run the install script, open a terminal, and at the project's root directory run:
./install.sh
The script will first ensure the required dependencies exist, and if so, it will build and deploy the project.
Only run the install script after configuring the project as described in the previous section.
## Project structure
This project combines a user interface (UI) and an application programming interface (API) into one application.
The UI is designed using Vue.js and the API is designed using Spring Boot.
The UI code is located in the `/app` directory and the API code is located in the `/src` directory.
The API will automatically generate a MySQL database catalog and the required tables when built for the first time,
assuming a valid database connection exists and the application configuration files are setup correctly.
Assuming a MySQL database exists and the connection is configured correctly, the API will automatically create the
required catalog and tables the first time the application is built and deployed.
Note that the MySQL database must include a user with CREATE, INSERT, SELECT, and UPDATE permissions,
and this user must be the one specified in the `spring.datasource.username` field in the `application.properties` file.
The structure of the generated tables are as follows:
- `poster` - Main database catalog
- `event` - Table for storing event overview information
- `year` - Year of the event
- `city` - City of the event
- `paper_link` - Confex link templates for papers
- `session_link` - Confex link templates for sessions
- `state` - State of the event
- `title` - Title of the event
- `paper` - Table for storing papers (abstracts) for an event
- `year` - Year of event featuring paper
- `id` - Confex ID assigned to paper
- `author` - Author(s) of paper
- `code` - Confex code assigned to paper
- `invited` - Whether the paper is flagged as invited
- `pdate` - Date of the presentation
- `plocation` - Location of the presentation
- `poster` - Whether this abstract is considered a poster
- `proom` - Room of the presentation
- `ptime` - Time of the presentation
- `title` - Title of the paper
- `program` - Table for storing programs for an event
- `year` - Year of the event featuring program
- `id` - Confex ID assigned to program
- `name` - Name of the program
- `program_session` - Table that relates programs to sessions
- `year` - Year of the event featuring the program and session
- `sid` - Confex ID assigned to session
- `pid` - Confex ID assigned to program
- `session` - Table for storing sessions for an event
- `year` - Year of the event featuring session
- `id` - Confex ID assigned to session
- `code` - Confex code assigned to session
- `location` - Location of the session
- `name` - Name of the session
- `poster` - Whether the session is flagged as a poster
- `room` - Room of the session
- `sdate` - Date of the session
- `stime` - Time of the session
- `session_paper` - Table that relates sessions to papers
- `year` - Year of the event featuring the session and paper
- `sid` - Confex ID assigned to session
- `pid` - Confex ID assigned to paper
## Using the application
### Searching for posters
Once the application is successfully deployed, navigate to the UI in a web browser by visiting
`<YOUR_DOMAIN>/poster-api-wrapper`, where `<YOUR_DOMAIN>` is the domain name where the application was deployed.
The default view will be the `/summary` view, where you can view posters previously submitted from the lookup view.
When the application is launched for the first time, the summary view will be empty since no posters will have been selected yet.
Navigate to the lookup view by clicking the `Lookup` link in the navigation at the top of the page.
Enter some text into the search box and press enter, or click the magnifying glass to perform a search.
It may take a few seconds to load the results from the AGU/AMS API.
### Selecting posters for displaying on the summary view
Once the search view is populated with search results, you may select which posters you'd like to appear on the summary
view by clicking the check box to the right of the search result. After making your selections, scroll to the bottom
of the page and complete the reCAPTCHA challenge. Once the reCAPTCHA challenge is complete, click the submit button.
A popup should display indicating the submission was successful.
### Viewing posters on the summary view
To view posters selected for displaying on the summary view, click the `Summary` link in the navigation at the top of
the page. The summary view provides an overview of all the posters selected from the lookup view.
Posters are grouped by session and details shown include the title, authors, location, and time.
The poster and session titles are also hyperlinked, and clicking them will take the user to the corresponding page
on the AGU/AMS site. Posters with multiple authors have the author column collapsed, and it can be expanded by clicking
the `More` text beneath the first author name.
### Manual submissions
Sometime a poster may exist in the AGU/AMS API but may not show up as a search result when searching over the AGU/AMS API. In these
rare cases, it is possible to manually add posters via the manual submission view. To go to the manual submission view,
click the `Manual Submission` link in the navigation bar at the top of the page. You must know the ID assigned by AGU/AMS
to the poster in order to manually add the poster to the summary view. If you know the AGU/AMS URL for the poster page,
the ID is the number at the end of the URL. For example, for the AGU URL
`https://agu.confex.com/agu/fm21/meetingapp.cgi/Paper/999999`, the poster ID would be `999999`.
Note this is for example only and at the time of this writing the URL in the previous example does not lead to an
actual poster page.
# Citation
Darnell, W., Crow, M., Devarakonda, R., Prakash, G., Poster API Wrapper. Computer Software.
https://code.ornl.gov/arm-data-science-integration/poster-api-wrapper.
13 Jul. 2021. Web. doi:
# License
UT-Battelle
|
3-Clause BSD License
Copyright 2021, UT-Battelle, LLC
VUE_APP_API=/poster-api-wrapper
VUE_APP_CHA=
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}
{
"name": "poster-ui",
"version": "1.0.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"serve:local": "vue-cli-service serve --mode local",
"build": "vue-cli-service build",
"build:prod": "vue-cli-service build --mode prod",
"deploy": "dev/deploy.sh",
"deploy:dev": "dev/deploy.sh dev",
"deploy:prod": "dev/deploy.sh prod",
"lint": "vue-cli-service lint"
},
"dependencies": {
"axios": "^0.21.0",
"bootstrap": "^4.3.1",
"bootstrap-vue": "^2.18.1",
"core-js": "^3.6.5",
"moment": "^2.29.1",
"vue": "^2.6.11",
"vue-recaptcha": "^1.3.0",
"vue-router": "^3.4.8"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"babel-eslint": "^10.1.0",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^6.2.2",
"vue-template-compiler": "^2.6.11"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "babel-eslint"
},
"rules": {
"no-unused-vars": "warn"
}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
<script>
import Navbar from "@/components/Navbar"
export default {
components: {
Navbar,
}
}
</script>
<template>
<div
id="app"
class="container"
>
<Navbar />
<router-view
id="content"
class="mt-4 mb-5"
/>
</div>
</template>
<style>
#app {
font-size: 0.8em;
}
.overview {
font-size: 1.2em;
}
</style>
<script>
export default {
}
</script>
<template>
<b-navbar
type="dark"
variant="dark"
>
<b-navbar-brand>Abstracts</b-navbar-brand>
<b-navbar-nav>
<b-nav-item to="summary">Summary</b-nav-item>
<b-nav-item to="lookup">Lookup</b-nav-item>
<b-nav-item to="manual">Manual Submission</b-nav-item>
</b-navbar-nav>
</b-navbar>
</template>
<style scoped>
</style>
<script>
export default {
props: {
value: { default: () => { return [] }, type: Array },
posters: { default: () => { return [] }, type: Array },
},
data: () => ({
selected: [],
}),
methods: {
updateSelected(id, value) {
if (this.selected && Array.isArray(this.selected)) {
if (value === true) {
if (!this.selected.includes(id)) {
this.selected.push(id)
}
} else if (value === false) {
const index = this.selected.indexOf(id)
if (index > -1) {
this.selected.splice(index, 1)
}
}
}
},
},
watch: {
selected: {
handler(val) {
this.$emit("input", val)
},
deep: true
},
},
}
</script>
<template>
<b-table
:items="posters"
striped
>
<template v-slot:cell(abstract)="data">
<b-row class="mb-2">
<b-col>
<b>{{ data.value.title }}</b>
</b-col>
</b-row>
<b-row>
<b-col>
<span v-html="data.value.authors" />
</b-col>
</b-row>
</template>
<template v-slot:cell(select)="data">
<b-form-checkbox @change="updateSelected(data.value, $event)" />
</template>
</b-table>
</template>
<style scoped>
</style>
<script>
import moment from "moment"
import axios from "axios"
export default {
name: 'Results',
data: () => ({
details: null,
results: [],
showAuthors: {},
}),
computed: {
resultGroups() {
const dates = []
const processed = []
this.results.forEach(result => {
if (!dates.includes(result.poster.pdate)) {
dates.push(result.poster.pdate)
}
})
if (dates && Array.isArray(dates) && dates.length > 0) {
dates.sort((a, b) => {
const first = moment(a, "dddd, D MMMM YYYY", true)
const second = moment(b, "dddd, D MMMM YYYY", true)
// console.log(first, second)
return first.isBefore(second) ? -1 : 1
})
dates.forEach(date => {
const postersByDate = this.results.filter(result => result.poster.pdate === date)
const sessions = []
postersByDate.forEach(poster => {
const sessionIds = poster.sessions.map(session => session.id)
sessionIds.forEach(sessionId => {
if (!sessions.includes(sessionId)) {
sessions.push(sessionId)
}
})
})
const sessionGroups = []
sessions.forEach(session => {
const posters = postersByDate.filter(poster => {
return poster.sessions.some(posterSession => posterSession.id === session)
})
// console.log(posters)
sessionGroups.push({
session: posters[0].sessions.filter(posterSession => posterSession.id === session)[0],
posters,
})
})
processed.push({
sessionGroups,
date,
})
})
}
return processed
},
},
methods: {
getSummary() {
const url = `${this.$api}/summary`
axios.get(url).then(response => {
if (response && response.data) {
if (response.data.event && Object.keys(response.data.event).length > 0) {
this.details = response.data.event
}
const posters = response.data.posters
if (posters && Array.isArray(posters) && posters.length > 0) {
this.results = response.data.posters
}
}
})
},
toggleAuthors(paperId) {
this.$set(this.showAuthors, paperId, !this.showAuthors[paperId] === true)
},
processAuthor(author, paperId) {
return this.showAuthors[paperId] ?
author :
author.split(",")[0]
},
sortSessions(sessions) {
if (sessions && Array.isArray(sessions) && sessions.length > 0) {
return sessions.sort((a, b) => {
if (a && a.session && a.session.stime && b && b.session && b.session.stime) {
const aTime = parseInt(a.session.stime.replace(/:/g, ""))
const bTime = parseInt(b.session.stime.replace(/:/g, ""))
if (!isNaN(aTime) && !isNaN(bTime)) {
return aTime < bTime ? -1 : 1
} else {
return 1
}
} else {
return 1
}
})
} else {
return []
}
},
sortPosters(posters) {
if (posters && Array.isArray(posters) && posters.length > 0) {
return posters.sort((a, b) => {
if (a && a.poster && a.poster.ptime && b && b.poster && b.poster.ptime) {
const aTime = parseInt(a.poster.ptime.replace(/:/g, ""))
const bTime = parseInt(b.poster.ptime.replace(/:/g, ""))
if (!isNaN(aTime) && !isNaN(bTime)) {
return aTime < bTime ? -1 : 1
} else {
return 1
}
} else {
return 1
}
})
} else {
return []
}
},
print: (e) => {
e.target.hidden = true
window.print()
e.target.hidden = false
},
},
mounted() {
this.getSummary()
},
}
</script>
<template>
<div>
<template v-if="details">
<b-row>
<b-col cols="10">
<h1>{{ details.title }}</h1>
</b-col>
<b-col>
<b-btn
id="print-button"
class="float-right"
squared
@click="print"
>
<b-icon
class="mr-1"
icon="printer"
/>
Print
</b-btn>
</b-col>
</b-row>
City: {{ details.city }}
<br />
State: {{ details.state }}
</template>
<template v-if="resultGroups && resultGroups.length > 0">
<div
v-for="(resultGroup, i) in resultGroups"
:key="i"
class="mt-4"
>
<h3>{{ resultGroup.date }}</h3>
<table class="table">
<thead>
<tr>
<th scope="col">Presentation Type</th>
<th scope="col">Session ID and Presentation Title</th>
<th scope="col">Presenters</th>
<th
class="text-right"
scope="col"
>
Time and Location (Pacific Time)
</th>
</tr>
</thead>
<tbody
v-for="(session, j) in sortSessions(resultGroup.sessionGroups)"
:key="j"
>
<tr>
<td
class="table-active"
colspan="3"
>
<a
:href="`${details.sessionLink}${session.session.id}`"
target="_blank"
>
{{ session.session.name }}
</a>
</td>
<td
class="table-active"
colspan="1"
>
<span class="float-right">
{{ session.session.stime }}
</span>
</td>