Commit 31c6a1f7 authored by Markus Scheidgen's avatar Markus Scheidgen
Browse files

Assign URL to DOIs and resolve those in the GUI.

parent 8dd5e944
Pipeline #63519 passed with stages
in 16 minutes and 16 seconds
......@@ -35,6 +35,7 @@ import { capitalize } from '../utils'
import { amber } from '@material-ui/core/colors'
import KeepState from './KeepState'
import {help as userdataHelp, default as UserdataPage} from './UserdataPage'
import ResolveDOI from './dataset/ResolveDOI'
export class VersionMismatch extends Error {
constructor(msg) {
......@@ -466,6 +467,18 @@ export default class App extends React.Component {
}
}
},
'dataset_doi': {
path: '/dataset/doi/:doi*',
key: (props) => `dataset/doi/${props.match.params.doi}`,
render: props => {
const { match, ...rest } = props
if (match && match.params.doi) {
return (<ResolveDOI {...rest} doi={match.params.doi} />)
} else {
return ''
}
}
},
'uploads': {
exact: true,
singleton: true,
......
......@@ -6,8 +6,8 @@ import { withErrors } from './errors'
import { withApi, DoesNotExist } from './api'
import Search from './search/Search'
import SearchContext from './search/SearchContext'
import { Typography, Link } from '@material-ui/core'
import { DatasetActions } from './search/DatasetList'
import { Typography } from '@material-ui/core'
import { DatasetActions, DOI } from './search/DatasetList'
import { withRouter } from 'react-router'
export const help = `
......@@ -97,7 +97,7 @@ class DatasetPage extends React.Component {
<div className={classes.description}>
<Typography variant="h4">{dataset.name || 'loading ...'}</Typography>
<Typography>
dataset{dataset.doi ? <span>, with DOI <Link href={dataset.doi}>{dataset.doi}</Link></span> : ''}
dataset{dataset.doi ? <span>, with DOI <DOI doi={dataset.doi} /></span> : ''}
</Typography>
</div>
......
......@@ -320,7 +320,8 @@ class EditUserMetadataDialogUnstyled extends React.Component {
raiseError: PropTypes.func.isRequired,
user: PropTypes.object,
onEditComplete: PropTypes.func,
disabled: PropTypes.bool
disabled: PropTypes.bool,
title: PropTypes.string
}
static styles = theme => ({
......@@ -478,7 +479,7 @@ class EditUserMetadataDialogUnstyled extends React.Component {
}
render() {
const { classes, buttonProps, total, api, user, example, disabled } = this.props
const { classes, buttonProps, total, api, user, example, disabled, title } = this.props
const { open, actions, verified, submitting } = this.state
const dialogEnabled = user && example.uploader && example.uploader.user_id === user.sub && !disabled
......@@ -511,7 +512,7 @@ class EditUserMetadataDialogUnstyled extends React.Component {
return (
<React.Fragment>
<IconButton {...(buttonProps || {})} onClick={this.handleButtonClick} disabled={!dialogEnabled}>
<Tooltip title={`Edit user metadata${dialogEnabled ? '' : '. You can only edit your data.'}`}>
<Tooltip title={title || `Edit user metadata${dialogEnabled ? '' : '. You can only edit your data.'}`}>
<EditIcon />
</Tooltip>
</IconButton>
......
......@@ -32,8 +32,7 @@ class Quantity extends React.Component {
},
valueAction: {},
valueActionButton: {
padding: 2,
paddingButtom: 3
padding: 4
},
valueActionIcon: {
fontSize: 16
......
......@@ -333,13 +333,23 @@ class Api {
async resolvePid(pid) {
this.onStartLoading()
return this.swaggerPromise
return this.swagger()
.then(client => client.apis.repo.resolve_pid({pid: pid}))
.catch(handleApiError)
.then(response => response.body)
.finally(this.onFinishLoading)
}
async resolveDoi(doi) {
this.onStartLoading()
console.log(doi)
return this.swagger()
.then(client => client.apis.datasets.resolve_doi({doi: doi}))
.catch(handleApiError)
.then(response => response.body)
.finally(this.onFinishLoading)
}
async search(search) {
this.onStartLoading()
return this.swagger()
......
import React from 'react'
import PropTypes from 'prop-types'
import { withStyles, Typography } from '@material-ui/core'
import { compose } from 'recompose'
import { withApi } from '../api'
import { withRouter } from 'react-router'
class ResolveDOI extends React.Component {
static styles = theme => ({
root: {
padding: theme.spacing.unit * 3
}
})
static propTypes = {
classes: PropTypes.object.isRequired,
doi: PropTypes.string.isRequired,
api: PropTypes.object.isRequired,
history: PropTypes.object.isRequired,
raiseError: PropTypes.func.isRequired
}
update() {
const { doi, api, history, raiseError } = this.props
api.resolveDoi(doi).then(dataset => {
history.push(`/dataset/id/${dataset.dataset_id}`)
}).catch(raiseError)
}
componentDidMount() {
this.update()
}
componentDidUpdate(prevProps) {
if (prevProps.doi !== this.props.doi || prevProps.api !== this.props.api) {
this.update()
}
}
render() {
const { classes } = this.props
return (
<Typography className={classes.root}>loading ...</Typography>
)
}
}
export default compose(withRouter, withApi(false), withStyles(ResolveDOI.styles))(ResolveDOI)
......@@ -7,6 +7,7 @@ import ApiDialogButton from '../ApiDialogButton'
import Quantity from '../Quantity'
import { withDomain } from '../domains'
import { Link as RouterLink } from 'react-router-dom'
import { DOI } from '../search/DatasetList'
class RepoEntryView extends React.Component {
static styles = theme => ({
......@@ -120,7 +121,7 @@ class RepoEntryView extends React.Component {
{(calcData.datasets || []).map(ds => (
<Typography key={ds.id}>
<Link component={RouterLink} to={`/dataset/id/${ds.id}`}>{ds.name}</Link>
{ds.doi ? <span>&nbsp; (<Link href={ds.doi}>{ds.doi}</Link>)</span> : ''}
{ds.doi ? <span>&nbsp; (<DOI doi={ds.doi}/>)</span> : ''}
</Typography>))}
</div>
</Quantity>
......
import React from 'react'
import PropTypes from 'prop-types'
import { withStyles, TableCell, Toolbar, IconButton, FormGroup, Tooltip } from '@material-ui/core'
import { withStyles, TableCell, Toolbar, IconButton, FormGroup, Tooltip, Link } from '@material-ui/core'
import { compose } from 'recompose'
import { withRouter } from 'react-router'
import { withDomain } from '../domains'
......@@ -13,6 +13,42 @@ import DeleteIcon from '@material-ui/icons/Delete'
import { withApi } from '../api'
import EditUserMetadataDialog from '../EditUserMetadataDialog'
import DownloadButton from '../DownloadButton'
import ClipboardIcon from '@material-ui/icons/Assignment'
import { CopyToClipboard } from 'react-copy-to-clipboard'
class DOIUnstyled extends React.Component {
static propTypes = {
doi: PropTypes.string.isRequired
}
static styles = theme => ({
root: {
display: 'inline-flex',
alignItems: 'center',
flexDirection: 'row',
flexWrap: 'nowrap'
}
})
render() {
const {classes, doi} = this.props
const url = `https://dx.doi.org/${doi}`
return <span className={classes.root}>
<Link href={url}>{doi}</Link>
<CopyToClipboard
text={url} onCopy={() => null}
>
<Tooltip title={`Copy DOI to clipboard`}>
<IconButton style={{margin: 3, marginRight: 0, padding: 4}}>
<ClipboardIcon style={{fontSize: 16}} />
</IconButton>
</Tooltip>
</CopyToClipboard>
</span>
}
}
export const DOI = withStyles(DOIUnstyled.styles)(DOIUnstyled)
class DatasetActionsUnstyled extends React.Component {
static propTypes = {
......@@ -102,6 +138,7 @@ class DatasetActionsUnstyled extends React.Component {
</IconButton>
</Tooltip>}
{editable && <EditUserMetadataDialog
title="Edit metadata of all dataset entries"
example={dataset.example} query={query}
total={dataset.total} onEditComplete={this.handleEdit}
/>}
......@@ -160,7 +197,7 @@ class DatasetListUnstyled extends React.Component {
},
DOI: {
label: 'Dataset DOI',
render: (dataset) => dataset.doi
render: (dataset) => dataset.doi && <DOI doi={dataset.doi} />
},
entries: {
label: 'Entries',
......
......@@ -138,7 +138,11 @@ class DatasetResource(Resource):
# set the DOI
doi = DOI.create(title='NOMAD dataset: %s' % result.name, user=g.user)
doi.create_draft()
doi.make_findable()
result.doi = doi.doi
result.m_x('me').save()
if doi.state != 'findable':
logger.warning(
......@@ -176,3 +180,17 @@ class DatasetResource(Resource):
result.m_x('me').delete()
return result
@ns.route('/doi/<path:doi>')
class RepoPidResource(Resource):
@api.doc('resolve_doi')
@api.response(404, 'Dataset with DOI does not exist')
@api.marshal_with(dataset_model, skip_none=True, code=200, description='DOI resolved')
@authenticate()
def get(self, doi: str):
dataset_me = Dataset.m_def.m_x('me').objects(doi=doi).first()
if dataset_me is None:
abort(404, 'Dataset with DOI %s does not exist' % doi)
return dataset_me, 200
......@@ -320,7 +320,7 @@ class UploadListResource(Resource):
Thanks for uploading your data to nomad.
Go back to %s and press reload to see the progress on your upload and publish your data.
''' % upload.gui_url,
''' % config.gui_url(),
200, {'Content-Type': 'text/plain; charset=utf-8'})
return upload, 200
......
......@@ -158,13 +158,12 @@ def api_url(ssl: bool = True):
services.api_base_path.strip('/'))
migration_source_db = NomadConfig(
host='db-repository.nomad.esc',
port=5432,
dbname='nomad_prod',
user='nomadlab',
password='*'
)
def gui_url():
base = api_url(True)[:-3]
if base.endswith('/'):
base = base[:-1]
return '%s/gui' % base
mail = NomadConfig(
enabled=False,
......
......@@ -76,6 +76,7 @@ class DOI(Document):
doi.doi_url = '%s/doi/%s' % (config.datacite.mds_host, doi_str)
doi.state = 'created'
doi.create_time = create_time
doi.url = '%s/dataset/doi/%s' % (config.gui_url(), doi_str)
affiliation = ''
if user.affiliation is not None:
......
......@@ -841,13 +841,6 @@ class Upload(Proc):
self.joined = False
super().reset()
@property
def gui_url(self):
base = config.api_url()[:-3]
if base.endswith('/'):
base = base[:-1]
return '%s/gui/uploads/' % base
def _cleanup_after_processing(self):
# send email about process finish
user = self.uploader
......@@ -857,7 +850,7 @@ class Upload(Proc):
'',
'your data %suploaded at %s has completed processing.' % (
'"%s" ' % self.name if self.name else '', self.upload_time.isoformat()), # pylint: disable=no-member
'You can review your data on your upload page: %s' % self.gui_url,
'You can review your data on your upload page: %s' % config.gui_url(),
'',
'If you encouter any issues with your upload, please let us know and replay to this email.',
'',
......
......@@ -1512,7 +1512,6 @@ class TestDataset:
search.refresh()
def test_delete_dataset(self, api, test_user_auth, example_dataset_with_entry):
# delete dataset
rv = api.delete('/datasets/ds1', headers=test_user_auth)
assert rv.status_code == 200
data = json.loads(rv.data)
......@@ -1525,9 +1524,14 @@ class TestDataset:
assert rv.status_code == 400
def test_assign_doi(self, api, test_user_auth, example_dataset_with_entry):
# assign doi
rv = api.post('/datasets/ds1', headers=test_user_auth)
assert rv.status_code == 200
data = json.loads(rv.data)
self.assert_dataset(data, name='ds1', doi=True)
self.assert_dataset_entry(api, '1', True, True, headers=test_user_auth)
def test_resolve_doi(self, api, example_dataset_with_entry):
rv = api.get('/datasets/doi/test_doi')
assert rv.status_code == 200
data = json.loads(rv.data)
self.assert_dataset(data, name='ds2', doi=True)
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment