diff --git a/gui/src/components/App.js b/gui/src/components/App.js index 2d29fc2fe3c067cdf02ce53fa74c072b4b490dfd..5658c8388ce57cb82ec6ccc38092c647251f9d9c 100644 --- a/gui/src/components/App.js +++ b/gui/src/components/App.js @@ -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, diff --git a/gui/src/components/DatasetPage.js b/gui/src/components/DatasetPage.js index 67546f5dfba6a2a933bf561f7a1267fcbb69451a..7f788ebd87f15680972a1738256e94381984bb3a 100644 --- a/gui/src/components/DatasetPage.js +++ b/gui/src/components/DatasetPage.js @@ -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> diff --git a/gui/src/components/EditUserMetadataDialog.js b/gui/src/components/EditUserMetadataDialog.js index 60f629717267363cc051fd2d41e56579682e253d..6be7dab205a7e4f7932cbb3ac75af11560913f3a 100644 --- a/gui/src/components/EditUserMetadataDialog.js +++ b/gui/src/components/EditUserMetadataDialog.js @@ -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> diff --git a/gui/src/components/Quantity.js b/gui/src/components/Quantity.js index 0ce94668cb081ced177353fe7c9cac7205254f18..992c31dc81e8f0f9f7052e2b20303ac7c547a74d 100644 --- a/gui/src/components/Quantity.js +++ b/gui/src/components/Quantity.js @@ -32,8 +32,7 @@ class Quantity extends React.Component { }, valueAction: {}, valueActionButton: { - padding: 2, - paddingButtom: 3 + padding: 4 }, valueActionIcon: { fontSize: 16 diff --git a/gui/src/components/api.js b/gui/src/components/api.js index 74aed766bf070804ee0ca34b50efdb0d5f9523a7..f7e68e42cb5b30e2e83b198e01dac8ce7fdf0cc4 100644 --- a/gui/src/components/api.js +++ b/gui/src/components/api.js @@ -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() diff --git a/gui/src/components/dataset/ResolveDOI.js b/gui/src/components/dataset/ResolveDOI.js new file mode 100644 index 0000000000000000000000000000000000000000..3df0b4f7763c41424ba9851e31e5f058c1689ec4 --- /dev/null +++ b/gui/src/components/dataset/ResolveDOI.js @@ -0,0 +1,49 @@ +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) diff --git a/gui/src/components/entry/RepoEntryView.js b/gui/src/components/entry/RepoEntryView.js index dd2d773c754c32b4b6012e2baa73d8df2ad87678..43e2eb2036edd081dd69c4a79a3170e184067afe 100644 --- a/gui/src/components/entry/RepoEntryView.js +++ b/gui/src/components/entry/RepoEntryView.js @@ -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> (<Link href={ds.doi}>{ds.doi}</Link>)</span> : ''} + {ds.doi ? <span> (<DOI doi={ds.doi}/>)</span> : ''} </Typography>))} </div> </Quantity> diff --git a/gui/src/components/search/DatasetList.js b/gui/src/components/search/DatasetList.js index d8e82eb5280bf1caada37eb9e94025897c4b6e1b..80de2470912a75c68fafb6eb849ed718d3c6702b 100644 --- a/gui/src/components/search/DatasetList.js +++ b/gui/src/components/search/DatasetList.js @@ -1,6 +1,6 @@ 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', diff --git a/nomad/app/api/dataset.py b/nomad/app/api/dataset.py index 90d130efc2a7f5bd1850c96f5aa3ba47184309da..87b959a6f62e84e601365a4e3975d262e61e1af3 100644 --- a/nomad/app/api/dataset.py +++ b/nomad/app/api/dataset.py @@ -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 diff --git a/nomad/app/api/upload.py b/nomad/app/api/upload.py index a98a9cd3b281aad235a138e3c99359bda860555d..5f6ae42d92e3ec5d11dc16c734bef4726a093d28 100644 --- a/nomad/app/api/upload.py +++ b/nomad/app/api/upload.py @@ -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 diff --git a/nomad/config.py b/nomad/config.py index 17e202260049d529106970699a959b4a852b9ef0..3a12ba95e30c2c3b7514a5443a7ef973363faeb0 100644 --- a/nomad/config.py +++ b/nomad/config.py @@ -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, diff --git a/nomad/doi.py b/nomad/doi.py index c2e965addd66db8791649a289c3b27ea7ae168a1..ed0c12e5ddd195d114966526d3d99c9febd34614 100644 --- a/nomad/doi.py +++ b/nomad/doi.py @@ -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: diff --git a/nomad/processing/data.py b/nomad/processing/data.py index d03ed24f026ff2d5e115a67dc5aec5759e9587be..6c5e2b5b1428254347e98bb84dfe8964e445f40b 100644 --- a/nomad/processing/data.py +++ b/nomad/processing/data.py @@ -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.', '', diff --git a/tests/app/test_api.py b/tests/app/test_api.py index 63b94a1f0f7a27d1115fea21816e3e1cc213021c..f1a02b19998da5a228ff309ae7262b578db50b2d 100644 --- a/tests/app/test_api.py +++ b/tests/app/test_api.py @@ -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)