Skip to content

Commit

Permalink
Fair Workflows Release 1 (#296)
Browse files Browse the repository at this point in the history
* Fair Workflows Feature 4

* Fair Workflows Feature 1 - Related Works List (#292)

* Added tabs for new relation types

* Added prototype connection type facet

* Replaced tabs with left-side facet

* Removed unnecessary Tab code

* Fixed bug where connection type wasn't selected by default

* Removed testing code

* Fixed undefined bug

* Reorder Conneciton types facets and allow Radio icons

* Added OtherRelated query

---------

Co-authored-by: jrhoads <[email protected]>

* Fair work feature 2 - Downloadable Reports (#303)

* Refactored DownloadReports to be more flexible

* Restructured API routes

* Added download related works for DOI

* Added connectionType column

---------

Co-authored-by: jrhoads <[email protected]>

* Fixed failing test

* Fixed failing test

* Added related works header (#308)

* Fixed small related works header rendering issue

---------

Co-authored-by: jrhoads <[email protected]>
  • Loading branch information
bklaing2 and jrhoads authored Dec 13, 2023
1 parent e2de83c commit 63cf389
Show file tree
Hide file tree
Showing 9 changed files with 441 additions and 163 deletions.
2 changes: 1 addition & 1 deletion cypress/integration/workContainer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ describe('workContainer with usage', () => {
it('download', () => {
cy.get('#download-metadata-button', { timeout: 30000 }).click()
cy.get('.download-list', { timeout: 30000 })
.should('have.length', 2)
.should('have.length', 3)
.should('contain', 'DataCite XML')
cy.get('#close-modal', { timeout: 30000 }).click()
})
Expand Down
62 changes: 24 additions & 38 deletions src/components/DownloadReports/DownloadReports.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,58 +2,44 @@ import React from 'react'
import { Row, Col } from 'react-bootstrap'
import HelpIcon from '../HelpIcon/HelpIcon'


type DownloadType = 'doi/related-works' | 'ror/related-works' | 'ror/funders'


type Props = {
url: string
title?: string
variables: {
id: string,
gridId: string,
crossrefFunderId: string,
cursor: string,
filterQuery: string,
published: string,
resourceTypeId: string,
fieldOfScience: string,
language: string,
license: string,
registrationAgency: string
}
links: { title: string, helpText?: string, type: DownloadType }[]
variables: { [prop: string]: string }
}

const API_URL_BASE = '/api/download-reports'


const link = ({ title, helpText, type }: Props['links'][number], params: string) => {
return (
<div id={`download-${type}`} key={title}>
<a rel="noreferrer" href={`${API_URL_BASE}/${type}?${params}`} download>
{title}{' '}
</a>
{ helpText && <HelpIcon text={helpText} size={20} position='inline' /> }
</div>
)
}

const DownloadReports: React.FunctionComponent<Props> = ({ variables}) => {
const DownloadReports: React.FunctionComponent<Props> = ({ links, variables}) => {

if (links.length === 0) return

const filteredVariables = Object.fromEntries(Object.entries(variables).filter(([, value]) => value))
const params = new URLSearchParams(filteredVariables).toString()

const apiurlBase = '/api/download-reports'


const downloadReports = () => {
return (
<div className="panel panel-transparent download-reports">
<div className="panel-body">
<Row>
<Col className="download-list" id="full-metadata" xs={12}>
<div id="download-related-works">
<a
rel="noreferrer"
href={`${apiurlBase}/related-works?${params}`}
download
>
Related Works (CSV)
</a>
<HelpIcon text='Includes descriptions and formatted citations in APA style for up to 200 DOIs associated with this organization.' size={20} position='inline' />
</div>
<div id="download-funders" className="download">
<a
rel="noreferrer"
href={`${apiurlBase}/funders?${params}`}
download
>
Funders (CSV)
</a>
<HelpIcon text='Includes up to 200 funders associated with related works.' size={20} position='inline' />
</div>
{links.map(l => link(l, params))}
</Col>
</Row>
</div>
Expand Down
51 changes: 45 additions & 6 deletions src/components/WorkFacets/WorkFacets.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import {
faSquare,
faCheckSquare
faSquare, faCheckSquare,
faCircle, faDotCircle,
} from '@fortawesome/free-regular-svg-icons'
import { useRouter } from 'next/router'
import { WorkType } from '../../pages/doi.org/[...doi]'
Expand All @@ -15,6 +15,7 @@ type Props = {
model: string
url: string
loading: boolean
connectionTypesCounts?: { references: number, citations: number, parts: number, partOf: number, otherRelated: number }
}

interface Facets {
Expand Down Expand Up @@ -45,14 +46,17 @@ const WorkFacets: React.FunctionComponent<Props> = ({
data,
model,
url,
loading
loading,
connectionTypesCounts
}) => {
const router = useRouter()

if (loading) return <div className="col-md-3"></div>

function facetLink(param: string, value: string) {
let icon = faSquare
function facetLink(param: string, value: string, checked = false, radio = false) {
const checkIcon = radio ? faDotCircle : faCheckSquare
const uncheckIcon = radio ? faCircle : faSquare
let icon = checked ? checkIcon : uncheckIcon

// get current query parameters from next router
const params = new URLSearchParams(router.query as any)
Expand All @@ -64,7 +68,7 @@ const WorkFacets: React.FunctionComponent<Props> = ({
if (params.get(param) == value) {
// if param is present, delete from query and use checked icon
params.delete(param)
icon = faCheckSquare
icon = checkIcon
} else {
// otherwise replace param with new value and use unchecked icon
params.set(param, value)
Expand All @@ -82,6 +86,16 @@ const WorkFacets: React.FunctionComponent<Props> = ({
// remove %2F? at the end of url
const path = url.substring(0, url.length - 2)

const connectionTypeList: Facet[] = connectionTypesCounts ? [
{ id: 'references', title: 'References', count: connectionTypesCounts.references },
{ id: 'citations', title: 'Citations', count: connectionTypesCounts.citations },
{ id: 'parts', title: 'Parts', count: connectionTypesCounts.parts },
{ id: 'partOf', title: 'Is Part Of', count: connectionTypesCounts.partOf },
{ id: 'otherRelated', title: 'Other', count: connectionTypesCounts.otherRelated }
] : []

const isConnectionTypeSet = new URLSearchParams(router.query as any).has('connection-type')

return (
<div className="panel panel-transparent">
{!['/doi.org?', '/orcid.org?', '/ror.org?'].includes(url) && (
Expand All @@ -91,12 +105,37 @@ const WorkFacets: React.FunctionComponent<Props> = ({
</div>
</div>
)}
{connectionTypesCounts && connectionTypesCounts.references +
connectionTypesCounts.citations +
connectionTypesCounts.parts +
connectionTypesCounts.partOf +
connectionTypesCounts.otherRelated
> 0 && (
<div className="panel facets add">
<div className="panel-body">
<h4>Connection Types</h4>
<ul id="connections-type-facets">
{connectionTypeList.filter(f => f.count > 0).map((facet, i) => (
<li key={facet.id}>
{facetLink('connection-type', facet.id, !isConnectionTypeSet && i == 0, true)}
<div className="facet-title">{facet.title}</div>
<span className="number pull-right">
{facet.count.toLocaleString('en-US')}
</span>
<div className="clearfix" />
</li>
))}
</ul>
</div>
</div>
)}

{model == "person"
? <AuthorsFacet authors={data.authors} title="Co-Authors" url={url} model={model} />
: <AuthorsFacet authors={data.creatorsAndContributors} title="Creators & Contributors" url={url} model={model} />
}


{data.published && data.published.length > 0 && (
<div className="panel facets add">
<div className="panel-body">
Expand Down
3 changes: 3 additions & 0 deletions src/components/WorksListing/WorksListing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type Props = {
showSankey?: boolean
sankeyTitle?: string
showFacets: boolean
connectionTypesCounts?: { references: number, citations: number, parts: number, partOf: number, otherRelated: number }
showClaimStatus: boolean
loading: boolean
model: string
Expand All @@ -34,6 +35,7 @@ const WorksListing: React.FunctionComponent<Props> = ({
works,
showAnalytics,
showFacets,
connectionTypesCounts,
showSankey,
sankeyTitle = 'Contributions to Related Works',
showClaimStatus,
Expand All @@ -56,6 +58,7 @@ const WorksListing: React.FunctionComponent<Props> = ({
url={url}
data={works}
loading={loading}
connectionTypesCounts={connectionTypesCounts}
></WorkFacets>
</div>
)
Expand Down
173 changes: 173 additions & 0 deletions src/pages/api/download-reports/doi/related-works.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import { NextApiRequest, NextApiResponse } from "next"
import { gql } from '@apollo/client';
import apolloClient from '../../../../utils/apolloClient'
import { stringify } from 'csv-stringify/sync'
import { WorkQueryData, WorkType } from "src/pages/doi.org/[...doi]";

const QUERY = gql`
query getDoiQuery(
$id: ID!
$filterQuery: String
$cursor: String
$published: String
$resourceTypeId: String
$fieldOfScience: String
$language: String
$license: String
$registrationAgency: String
$repositoryId: String
) {
work(id: $id) {
citations(
first: 25
query: $filterQuery
after: $cursor
published: $published
resourceTypeId: $resourceTypeId
fieldOfScience: $fieldOfScience
language: $language
license: $license
registrationAgency: $registrationAgency
repositoryId: $repositoryId
) {
nodes {
...WorkFragment
}
}
references(
first: 25
query: $filterQuery
after: $cursor
published: $published
resourceTypeId: $resourceTypeId
fieldOfScience: $fieldOfScience
language: $language
license: $license
registrationAgency: $registrationAgency
repositoryId: $repositoryId
) {
nodes {
...WorkFragment
}
}
parts(
first: 25
query: $filterQuery
after: $cursor
published: $published
resourceTypeId: $resourceTypeId
fieldOfScience: $fieldOfScience
language: $language
license: $license
registrationAgency: $registrationAgency
repositoryId: $repositoryId
) {
nodes {
...WorkFragment
}
}
partOf(
first: 25
query: $filterQuery
after: $cursor
published: $published
resourceTypeId: $resourceTypeId
fieldOfScience: $fieldOfScience
language: $language
license: $license
registrationAgency: $registrationAgency
repositoryId: $repositoryId
) {
nodes {
...WorkFragment
}
}
otherRelated(
first: 25
query: $filterQuery
after: $cursor
published: $published
resourceTypeId: $resourceTypeId
fieldOfScience: $fieldOfScience
language: $language
license: $license
registrationAgency: $registrationAgency
repositoryId: $repositoryId
) {
nodes {
...WorkFragment
}
}
}
}
fragment WorkFragment on Work {
titles {
title
}
descriptions {
description
descriptionType
}
types {
resourceTypeGeneral
resourceType
}
doi
formattedCitation(style: "apa", locale: "en-US", format: text)
publicationYear
}
`


function addConnectionType(w: WorkType, connectionType: string) {
return { ...w, connectionType: connectionType }
}



export default async function downloadReportsHandler(
req: NextApiRequest,
res: NextApiResponse
) {
const variables = req.query

const { data } = await apolloClient.query<WorkQueryData, unknown>({
query: QUERY,
variables: variables
})


const references = data.work.references.nodes.map(w => addConnectionType(w, 'Reference'))
const citations = data.work.citations.nodes.map(w => addConnectionType(w, 'Citation'))
const parts = data.work.parts.nodes.map(w => addConnectionType(w, 'Part'))
const partOf = data.work.partOf.nodes.map(w => addConnectionType(w, 'Is Part Of'))
const otherRelated = data.work.otherRelated.nodes.map(w => addConnectionType(w, 'Other Relation'))

const works = references.concat(citations, parts, partOf, otherRelated)
const sortedData = works.sort((a, b) => b.publicationYear - a.publicationYear)

const csv = stringify(sortedData, {
header: true,
columns: [
{ key: 'titles[0].title', header: 'Title' },
{ key: 'publicationYear', header: 'Publication Year' },
{ key: 'doi', header: 'DOI' },
{ key: 'descriptions[0].description', header: 'Description' },
{ key: 'formattedCitation', header: 'Formatted Citation' },
{ key: 'types.resourceTypeGeneral', header: 'Resource Type (General)' },
{ key: 'types.resourceType', header: 'Resource Type' },
{ key: 'connectionType', header: 'Connection Type' }
]
})

try {
res.status(200)
res.setHeader('Content-Type', 'text/csv')
res.setHeader('Content-Disposition', `attachment; filename="related-works_${variables.id}.csv"`)
res.send(csv)
} catch (error) {
res.status(400).json({ error })
}

}
Loading

0 comments on commit 63cf389

Please sign in to comment.