Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better OpenAlex support #1316

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions documentation/docs/01-users/05-adding-software.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,19 +164,19 @@ This section allows you to add mentions to your software page. You can use this

### Reference papers

Use the *Search* box on the right hand side to find papers by DOI or title. All the relevant data about the publication will be retrieved automatically. A background scraper will use [OpenAlex](https://openalex.org/) to collect all citations of reference papers that have a DOI or an OpenAlex ID.
Use the *Search* box on the right hand side to find papers by DOI, OpenAlex ID or title. All the relevant data about the publication will be retrieved automatically. A background scraper will use [OpenAlex](https://openalex.org/) to collect all citations of reference papers that have a DOI or an OpenAlex ID.

### Citations

These are the citations of the reference papers that the RSD scraper was able to find on [OpenAlex](https://openalex.org/. It can take a few minutes before the citations are harvested.
These are the citations of the reference papers that the RSD scraper was able to find on [OpenAlex](https://openalex.org/). It can take a few minutes before the citations are harvested.

:::warning
You cannot edit the content of this section. All entries are automatically harvested and generated by the RSD scraper. The mentions found are displayed in the mentions section of the software page.
:::

### Related output

Here you can add all additional related output. Use search to find papers or other publications by DOI or title. It is also possible to bulk add mentions, that have a DOI (use the *Import* button). On the popup, you can add one DOI per line, with a maximum of 50. After clicking on the *Next* button, we will fetch the data, which can take a moment. When that is done, you will see an overview of the data we fetched, including possible errors, where you can check the data and possibly disable some of the mentions.
Here you can add all additional related output. Use search to find papers or other publications by DOI, OpenAlex ID or title. It is also possible to bulk add mentions, that have a DOI (use the *Import* button). On the popup, you can add one DOI per line, with a maximum of 50. After clicking on the *Next* button, we will fetch the data, which can take a moment. When that is done, you will see an overview of the data we fetched, including possible errors, where you can check the data and possibly disable some of the mentions.

## Testimonials

Expand Down
4 changes: 2 additions & 2 deletions documentation/docs/01-users/07-adding-projects.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ Here you can add output that was produced by the project itself, such as papers,

#### Add output

To add an item, the search bar on the left can be used to search the RSD, [Crossref](https://crossref.org), and [DataCite](https://datacite.org) databases using the **Title** or **DOI** of the research output. An item can be added by selecting it from the list of the search results. The RSD will automatically classify the item based on the available metadata.
To add an item, the search bar on the left can be used to search the RSD, [Crossref](https://www.crossref.org), [DataCite](https://datacite.org) and [OpenAlex](https://openalex.org/) databases using the **Title**, **DOI** or **OpenAlex ID** of the research output. An item can be added by selecting it from the list of the search results. The RSD will automatically classify the item based on the available metadata.

#### Import output

Expand Down Expand Up @@ -124,7 +124,7 @@ Here you can add mentions of your project that cannot be found automatically by

#### Search publication

To add an item, the search bar on the left can be used to search the RSD, [Crossref](https://crossref.org), and [DataCite](https://datacite.org) databases using the **Title** or **DOI** of the research output. An item can be added by selecting it from the list of search results. The RSD will automatically classify the item based on the available metadata.
To add an item, the search bar on the left can be used to search the RSD, [Crossref](https://www.crossref.org), [DataCite](https://datacite.org) and [OpenAlex](https://openalex.org/) databases using the **Title**, **DOI** or **OpenAlex ID** of the research impact item. An item can be added by selecting it from the list of search results. The RSD will automatically classify the item based on the available metadata.

#### Import publication

Expand Down
10 changes: 7 additions & 3 deletions frontend/components/admin/mentions/MentionsOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,14 @@ export default function MentionsOverview() {

const searchTypeTerm: SearchTermInfo = extractSearchTerm(sanitisedSearch)
const termEscaped = encodeURIComponent(sanitisedSearch)
if (searchTypeTerm.type === 'doi') {
return `doi=eq.${termEscaped}`
switch (searchTypeTerm.type) {
case 'doi':
return `doi=eq.${termEscaped}`
case 'openalex':
return `openalex_id=eq.${termEscaped}`
case 'title':
return `or=(title.ilike.*${termEscaped}*,authors.ilike.*${termEscaped}*,journal.ilike.*${termEscaped}*,url.ilike.*${termEscaped}*,note.ilike.*${termEscaped}*,openalex_id.ilike.*${termEscaped}*)`
}
return `or=(title.ilike.*${termEscaped}*,authors.ilike.*${termEscaped}*,journal.ilike.*${termEscaped}*,url.ilike.*${termEscaped}*,note.ilike.*${termEscaped}*,openalex_id.ilike.*${termEscaped}*)`
}

function sanitiseSearch(search: string): string | undefined {
Expand Down
67 changes: 31 additions & 36 deletions frontend/components/mention/EditMentionModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export default function EditMentionModal({open, onCancel, onSubmit, item, pos, t
const formData = watch()
// need to clear image_url error manually after the type change
// and dynamic rules change from required to not required
if (formData.mention_type!=='highlight' && errors?.hasOwnProperty('image_url')){
if (formData.mention_type !== 'highlight' && errors?.hasOwnProperty('image_url')) {
clearErrors('image_url')
}

Expand Down Expand Up @@ -131,21 +131,33 @@ export default function EditMentionModal({open, onCancel, onSubmit, item, pos, t
padding: '1rem 1.5rem'
}}>
{isAdmin &&
<>
<ControlledTextField
control={control}
options={{
name: 'doi',
label: config.doi.label,
useNull: true,
defaultValue: formData?.doi,
helperTextMessage: config.doi.help,
helperTextCnt: `${formData?.doi?.length || 0}/${config.doi.validation.maxLength.value}`,
}}
rules={config.doi.validation}
/>
<div className="py-2"></div>
</>
<>
<ControlledTextField
control={control}
options={{
name: 'doi',
label: config.doi.label,
useNull: true,
defaultValue: formData?.doi,
helperTextMessage: config.doi.help,
helperTextCnt: `${formData?.doi?.length || 0}/${config.doi.validation.maxLength.value}`,
}}
rules={config.doi.validation}
/>
<div className="py-2"></div>
<ControlledTextField
control={control}
options={{
name: 'openalex_id',
label: config.openalex_id.label,
useNull: true,
defaultValue: formData?.openalex_id,
helperTextMessage: config.openalex_id.help,
}}
rules={config.openalex_id.validation}
/>
<div className="py-2"></div>
</>
}
<ControlledTextField
control={control}
Expand Down Expand Up @@ -285,27 +297,10 @@ export default function EditMentionModal({open, onCancel, onSubmit, item, pos, t
}}
rules={config.note.validation}
/>
{isAdmin &&
<>
<div className="py-2"></div>
<ControlledTextField
control={control}
options={{
name: 'openalex_id',
label: config.openalex_id.label,
useNull: true,
defaultValue: formData?.openalex_id,
helperTextMessage: config.openalex_id.help,
}}
rules={config.openalex_id.validation}
/>
<div className="py-2"></div>
</>
}
{!isAdmin &&
<Alert severity="warning" sx={{marginTop: '1rem'}}>
The information can not be edited after creation.
</Alert>}
<Alert severity="warning" sx={{marginTop: '1rem'}}>
The information can not be edited after creation.
</Alert>}
</DialogContent>
<DialogActions sx={{
padding: '1rem 1.5rem',
Expand Down
4 changes: 3 additions & 1 deletion frontend/components/mention/FindMentionInfoPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// SPDX-FileCopyrightText: 2022 - 2023 Dusan Mijatovic (dv4all)
// SPDX-FileCopyrightText: 2022 - 2023 dv4all
// SPDX-FileCopyrightText: 2024 Ewan Cahen (Netherlands eScience Center) <[email protected]>
// SPDX-FileCopyrightText: 2024 Netherlands eScience Center
//
// SPDX-License-Identifier: Apache-2.0

Expand All @@ -12,7 +14,7 @@ export default function FindMentionInfoPanel({children}:{children:any}) {
icon={false}
>
{/* <AlertTitle>Add existing publication</AlertTitle> */}
We search in <strong> <a href="https://crossref.org" target="_blank">Crossref</a>, <a href="https://datacite.org" target="_blank">DataCite</a></strong> and the RSD.
We search in <strong> <a href="https://www.crossref.org/" target="_blank">Crossref</a>, <a href="https://datacite.org" target="_blank">DataCite</a>, <a href="https://openalex.org/" target="_blank">OpenAlex</a></strong> and the RSD.
All metadata will be imported automatically.
{ children }
</Alert>
Expand Down
79 changes: 51 additions & 28 deletions frontend/components/mention/FindMentionSection.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
// SPDX-FileCopyrightText: 2022 - 2023 Dusan Mijatovic (dv4all)
// SPDX-FileCopyrightText: 2022 - 2023 Ewan Cahen (Netherlands eScience Center) <[email protected]>
// SPDX-FileCopyrightText: 2022 - 2023 dv4all
// SPDX-FileCopyrightText: 2022 - 2024 Ewan Cahen (Netherlands eScience Center) <[email protected]>
// SPDX-FileCopyrightText: 2022 - 2024 Netherlands eScience Center
// SPDX-FileCopyrightText: 2024 Dusan Mijatovic (Netherlands eScience Center)
//
// SPDX-License-Identifier: Apache-2.0

import {useAuth} from '~/auth'
import {MentionItemProps} from '~/types/Mention'
import {getMentionByDoiFromRsd} from '~/utils/editMentions'
import {getMentionByDoiFromRsd, getMentionByOpenalexIdFromRsd} from '~/utils/editMentions'
import {getMentionByDoi} from '~/utils/getDOI'
import EditSectionTitle from '~/components/layout/EditSectionTitle'
import FindMention from '~/components/mention/FindMention'
import FindMentionInfoPanel from '~/components/mention/FindMentionInfoPanel'
import useEditMentionReducer from '~/components/mention/useEditMentionReducer'
import {extractSearchTerm} from '~/components/software/edit/mentions/utils'
import {getMentionByOpenalexId} from '~/utils/getOpenalex'

type FindProjectMentionProps={
id:string,
Expand All @@ -39,35 +40,57 @@ export default function FindMentionSection({id,config,findPublicationByTitle}:Fi
const {session: {token}} = useAuth()
const {onAdd} = useEditMentionReducer()

async function findPublication(searchFor: string) {
async function findPublication(searchFor: string): Promise<MentionItemProps[]> {
const searchData = extractSearchTerm(searchFor)
if (searchData.type === 'doi') {
searchFor = searchData.term
// look first at RSD
const rsd = await getMentionByDoiFromRsd({
doi: searchFor,
token
})
if (rsd?.status === 200 && rsd.message?.length === 1) {
// return first found item in RSD
const item:MentionItemProps = rsd.message[0]
return [item]
switch (searchData.type) {
case 'doi': {
searchFor = searchData.term
// look first at RSD
const rsd = await getMentionByDoiFromRsd({
doi: searchFor,
token
})
if (rsd?.status === 200 && rsd.message?.length === 1) {
// return first found item in RSD
const item: MentionItemProps = rsd.message[0]
return [item]
}
// else find by DOI
const resp = await getMentionByDoi(searchFor)
if (resp?.status === 200) {
return [resp.message as MentionItemProps]
}
return []
}
// else find by DOI
const resp = await getMentionByDoi(searchFor)
if (resp?.status === 200) {
return [resp.message as MentionItemProps]
case 'openalex': {
searchFor = searchData.term
// look first at RSD
const rsd = await getMentionByOpenalexIdFromRsd({
id: searchFor,
token
})
if (rsd?.status === 200 && rsd.message?.length === 1) {
// return first found item in RSD
const item: MentionItemProps = rsd.message[0]
return [item]
}
// else find by DOI
const resp = await getMentionByOpenalexId(searchFor)
if (resp?.status === 200) {
return [resp.message as MentionItemProps]
}
return []
}
case 'title': {
searchFor = searchData.term
// find by title
const mentions = await findPublicationByTitle({
id: id,
searchFor,
token
})
return mentions
}
return []
} else{
searchFor = searchData.term
// find by title
const mentions = await findPublicationByTitle({
id: id,
searchFor,
token
})
return mentions
}
}

Expand Down
43 changes: 23 additions & 20 deletions frontend/components/mention/ImportMentions/apiImportMentions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ import useEditMentionReducer from '../useEditMentionReducer'

export type DoiBulkImportReport = Map<string, SearchResult> | null

export function useValidateInputList(token:string) {
export function useValidateInputList(token: string) {
const {mentions} = useEditMentionReducer()
const [validating, setValidating] = useState(false)

async function validateInput(value:string) {
async function validateInput(value: string) {
setValidating(true)
const doiList = value.split(/\r\n|\n|\r/)
const searchResults = await validateInputList(doiList, mentions, token)
Expand All @@ -48,21 +48,24 @@ export async function validateInputList(doiList: string[], mentions: MentionItem
// filter valid DOI type entries
.filter(search => {
// debugger
if (search.type === 'doi') {
// convert to lower case
const doi = search.term.toLowerCase()
// validate if not already included
const found = mentions.find(mention => mention.doi?.toLowerCase() === doi)
if (found) {
// flag item with DOI already processed
mentionResultPerDoi.set(doi, {doi ,status: 'alreadyImported', include: false})
return false
switch (search.type) {
case 'doi': {
// convert to lower case
const doi = search.term.toLowerCase()
// validate if not already included
const found = mentions.find(mention => mention.doi?.toLowerCase() === doi)
if (found) {
// flag item with DOI already processed
mentionResultPerDoi.set(doi, {doi, status: 'alreadyImported', include: false})
return false
}
return true
}
return true
} else {
// flag invalid DOI entries
mentionResultPerDoi.set(search.term, {doi:search.term, status: 'invalidDoi', include: false})
return false
case 'openalex':
case 'title':
// flag invalid DOI entries
mentionResultPerDoi.set(search.term, {doi: search.term, status: 'invalidDoi', include: false})
return false
}
})
// extract DOI string from serch info
Expand Down Expand Up @@ -164,16 +167,16 @@ export async function validateInputList(doiList: string[], mentions: MentionItem
// flag dois that are not updated
doisNotInDatabase.forEach(doi => {
if (!mentionResultPerDoi.has(doi)) {
mentionResultPerDoi.set(doi, {doi,status: 'unknown', include: false})
mentionResultPerDoi.set(doi, {doi, status: 'unknown', include: false})
}
})
}

return mentionResultPerDoi
}

export async function linkMentionToEntity({ids, table, entityName,entityId, token}: {
ids: string[], table: string, entityName: string, entityId:string, token: string
export async function linkMentionToEntity({ids, table, entityName, entityId, token}: {
ids: string[], table: string, entityName: string, entityId: string, token: string
}) {
try {
const url = `/api/v1/${table}`
Expand Down Expand Up @@ -207,7 +210,7 @@ export async function addMentions({mentions, token}: { mentions: MentionItemProp
body: JSON.stringify(mentions)
})
if (resp.status === 201) {
const json:MentionItemProps[] = await resp.json()
const json: MentionItemProps[] = await resp.json()
return {
status: 200,
message: json
Expand Down
8 changes: 4 additions & 4 deletions frontend/components/mention/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
// SPDX-License-Identifier: Apache-2.0

import {MentionByType, MentionTypeKeys} from '~/types/Mention'
import {doiRegexStrict} from '~/components/software/edit/mentions/utils'
import {DOI_REGEX_STRICT} from '~/components/software/edit/mentions/utils'

export const findMention={
// title: 'Add publication',
// subtitle: 'We search in Crossref, DataCite and RSD databases',
label: 'Search by DOI or publication title',
help: 'Valid DOI or at least first 2 letters of publication title',
label: 'Search by DOI, OpenAlex ID or publication title',
help: 'Valid DOI, OpenAlex ID or at least first 2 letters of publication title',
validation: {
// custom validation rule, not in use by react-hook-form
minLength: 2,
Expand All @@ -31,7 +31,7 @@ export const mentionModal = {
message: 'Maximum length is 255'
},
pattern: {
value: doiRegexStrict,
value: DOI_REGEX_STRICT,
message: 'The DOI should look like 10.XXX/XXX'
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all) (dv4all)
// SPDX-FileCopyrightText: 2022 dv4all
// SPDX-FileCopyrightText: 2024 Dusan Mijatovic (Netherlands eScience Center)
// SPDX-FileCopyrightText: 2024 Ewan Cahen (Netherlands eScience Center) <[email protected]>
// SPDX-FileCopyrightText: 2024 Netherlands eScience Center
//
// SPDX-License-Identifier: Apache-2.0
Expand Down Expand Up @@ -34,7 +35,7 @@ it('findPublicationByTitle', async () => {
token: 'TEST-TOKEN'
}

const expectedUrl = `/api/fe/mention/impact?id=${props.id}&search=${encodeURIComponent(props.searchFor)}`
const expectedUrl = `/api/fe/mention/find_by_title?id=${props.id}&search=${encodeURIComponent(props.searchFor)}&relation_type=impact`
const expectBody = {
'headers': {
'Authorization': `Bearer ${props.token}`,
Expand Down
Loading