diff --git a/gui/src/components/FAQ.js b/gui/src/components/FAQ.js index a5412742d8b7170d58782f0c988a7b5a9bd412a6..75a0ceaa97bc11bdee047e09a1e069868466e7b0 100644 --- a/gui/src/components/FAQ.js +++ b/gui/src/components/FAQ.js @@ -23,7 +23,8 @@ class FAQ extends React.Component { <Markdown>{` # Frequently Asked Questions (FAQ) - These are often repeated questions that cover the basic NOMAD use-cases. + These are often repeated questions that cover the basic NOMAD use-cases. If you have + further questions, please write use: [${email}](mailto:${email}). ## Upload data, datasets, embargo, and DOIs @@ -129,9 +130,6 @@ class FAQ extends React.Component { If you are familiar with the input and output format of other relevant codes, write us an Email ([${email}](mailto:${email})) and we will figure our if and how to support this code in the future. - - ## I have a new question - Please write use: [${email}](mailto:${email}). `}</Markdown> </div> ) diff --git a/gui/src/components/UserdataPage.js b/gui/src/components/UserdataPage.js index aca6dfa0dc7c042f03a0dc6f1e026d7fa83d7388..fc449fa5d288c09d891359b0a3ac8e48fa074cf9 100644 --- a/gui/src/components/UserdataPage.js +++ b/gui/src/components/UserdataPage.js @@ -50,7 +50,7 @@ On the dataset table, you can also click the DOI button to assign a DOI to a dat This DOI can be used in publications to link to your dataset. If people resovle the DOI, they will be redirected to a NOMAD view that shows the dataset and allows its download. -Once you assigned a DOI to a dataset, not entries can be removed or added to the dataset. +Once you assigned a DOI to a dataset, no entries can be removed or added to the dataset. ` class UserdataPage extends React.Component { diff --git a/gui/src/components/domains.js b/gui/src/components/domains.js index 816261774f035927ee33ec18f7493b52292bd3d7..b4614b7304137ccaefafcb5cab4f701c4ec8cef3 100644 --- a/gui/src/components/domains.js +++ b/gui/src/components/domains.js @@ -47,7 +47,7 @@ class DomainProviderBase extends React.Component { still be missing when you are exploring Nomad data using the new search and data exploring capabilities (menu items on the left). `, - entryLabel: 'code run', + entryLabel: 'entry', searchPlaceholder: 'enter atoms, codes, functionals, or other quantity values', /** * A component that is used to render the search aggregations. The components needs diff --git a/gui/src/components/entry/ArchiveLogView.js b/gui/src/components/entry/ArchiveLogView.js index cae410c5a1e321c00669ef37d19aefe5229a165e..ee83581cea0f3dc818b124451908042191258715 100644 --- a/gui/src/components/entry/ArchiveLogView.js +++ b/gui/src/components/entry/ArchiveLogView.js @@ -85,7 +85,7 @@ class ArchiveLogView extends React.Component { <Download classes={{root: classes.downloadFab}} tooltip="download logfile" - component={Fab} className={classes.downloadFab} color="secondary" size="medium" + component={Fab} className={classes.downloadFab} size="medium" url={`archive/logs/${uploadId}/${calcId}`} fileName={`${calcId}.log`} > <DownloadIcon /> diff --git a/gui/src/components/entry/RawFiles.js b/gui/src/components/entry/RawFiles.js index e1b92632041a77f8955d661a7f69c8a9aabac6c5..9f7b07ff41a04e1d99052cd75e836ac8e5b0210e 100644 --- a/gui/src/components/entry/RawFiles.js +++ b/gui/src/components/entry/RawFiles.js @@ -89,7 +89,7 @@ class RawFiles extends React.Component { } update() { - const { uploadId, calcId } = this.props + const { uploadId, calcId, raiseError } = this.props // this might accidentally happen, when the user logs out and the ids aren't // necessarily available anymore, but the component is still mounted if (!uploadId || !calcId) { @@ -98,13 +98,16 @@ class RawFiles extends React.Component { this.props.api.getRawFileListFromCalc(uploadId, calcId).then(data => { const files = data.contents.map(file => `${data.directory}/${file.name}`) + if (files.length > 500) { + raiseError('There are more than 500 files in this entry. We can only show the first 500.') + } this.setState({files: files}) }).catch(error => { this.setState({files: null}) if (error.name === 'DoesNotExist') { this.setState({doesNotExist: true}) } else { - this.props.raiseError(error) + raiseError(error) } }) } diff --git a/gui/src/components/entry/RepoEntryView.js b/gui/src/components/entry/RepoEntryView.js index 3a22b5dfe522fa3dff71cf9b21ac71424c40c985..d95d521b5f666e034cb50ae263b1bc197238b774 100644 --- a/gui/src/components/entry/RepoEntryView.js +++ b/gui/src/components/entry/RepoEntryView.js @@ -130,6 +130,7 @@ class RepoEntryView extends React.Component { <CardHeader title="Ids / processing" /> <CardContent classes={{root: classes.cardContent}}> <Quantity column style={{maxWidth: 350}}> + <Quantity quantity="calc_id" label={`${domain.entryLabel} id`} noWrap withClipboard {...quantityProps} /> <Quantity quantity="pid" label='PID' loading={loading} placeholder="not yet assigned" noWrap {...quantityProps} withClipboard /> <Quantity quantity="upload_id" label='upload id' {...quantityProps} noWrap withClipboard /> <Quantity quantity="upload_time" label='upload time' noWrap {...quantityProps} > @@ -137,7 +138,6 @@ class RepoEntryView extends React.Component { {new Date(calcData.upload_time * 1000).toLocaleString()} </Typography> </Quantity> - <Quantity quantity="calc_id" label={`${domain.entryLabel} id`} noWrap withClipboard {...quantityProps} /> <Quantity quantity='mainfile' loading={loading} noWrap {...quantityProps} withClipboard /> <Quantity quantity="calc_hash" label={`${domain.entryLabel} hash`} loading={loading} noWrap {...quantityProps} /> <Quantity quantity="raw_id" label='raw id' loading={loading} noWrap {...quantityProps} withClipboard /> diff --git a/gui/src/components/metaInfoBrowser/MetaInfoBrowser.js b/gui/src/components/metaInfoBrowser/MetaInfoBrowser.js index 7b0d46e3b13f8e0e537d86183985ee40a8305e4c..2078545168ef7426634505596572648902bf4dc2 100644 --- a/gui/src/components/metaInfoBrowser/MetaInfoBrowser.js +++ b/gui/src/components/metaInfoBrowser/MetaInfoBrowser.js @@ -45,7 +45,7 @@ const MenuProps = { PaperProps: { style: { maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP, - width: 300 + width: 300, maxHeight: '90vh' } } } diff --git a/gui/src/components/search/EntryList.js b/gui/src/components/search/EntryList.js index 2f1220b3ceec5e31aea9df98960d46643179e7bd..4b7b96c8206895821b5828cc5ccb56456139ad1e 100644 --- a/gui/src/components/search/EntryList.js +++ b/gui/src/components/search/EntryList.js @@ -27,6 +27,7 @@ export class EntryListUnstyled extends React.Component { columns: PropTypes.object, title: PropTypes.string, actions: PropTypes.element, + showEntryActions: PropTypes.func, selectedColumns: PropTypes.arrayOf(PropTypes.string) } @@ -224,8 +225,8 @@ export class EntryListUnstyled extends React.Component { <div className={classes.entryDetailsRow} style={{maxWidth: '33%', paddingRight: 0}}> <Quantity column > {/* <Quantity quantity="pid" label='PID' placeholder="not yet assigned" noWrap data={row} withClipboard /> */} - <Quantity quantity="upload_id" label='upload id' data={row} noWrap withClipboard /> <Quantity quantity="calc_id" label={`${domain.entryLabel} id`} noWrap withClipboard data={row} /> + <Quantity quantity="upload_id" label='upload id' data={row} noWrap withClipboard /> <Quantity quantity='mainfile' noWrap data={row} withClipboard /> <Quantity quantity="upload_time" label='upload time' noWrap data={row} > <Typography noWrap> @@ -255,11 +256,15 @@ export class EntryListUnstyled extends React.Component { } renderEntryActions(row, selected) { - return <Tooltip title="View entry page"> - <IconButton style={selected ? {color: 'white'} : null} onClick={event => this.handleViewEntryPage(event, row)}> - <DetailsIcon /> - </IconButton> - </Tooltip> + if (!this.props.showEntryActions || this.props.showEntryActions(row)) { + return <Tooltip title="View entry page"> + <IconButton style={selected ? {color: 'white'} : null} onClick={event => this.handleViewEntryPage(event, row)}> + <DetailsIcon /> + </IconButton> + </Tooltip> + } else { + return '' + } } render() { @@ -277,7 +282,7 @@ export class EntryListUnstyled extends React.Component { const defaultSelectedColumns = this.props.selectedColumns || [ ...domain.defaultSearchResultColumns, - 'datasets', 'authors'] + 'authors'] const pagination = <TablePagination count={totalNumber} diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index 3c3c9b739667b842253f4e23168798d694780aa2..84d3150500d8eb97a29b41e2652a3368bc60ffec 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -47,8 +47,8 @@ class SearchContext extends React.Component { response: SearchContext.emptyResponse, request: { statistics: true, - order_by: 'formula', - order: 1, + order_by: 'upload_time', + order: -1, page: 1, per_page: 10 }, diff --git a/gui/src/components/search/SearchPage.js b/gui/src/components/search/SearchPage.js index 9279b3f2cc181cd0ee63655c7f6252ccf09e5ed0..652e6bb3098e22778cb71f404e5d1d060b36717d 100644 --- a/gui/src/components/search/SearchPage.js +++ b/gui/src/components/search/SearchPage.js @@ -29,8 +29,21 @@ The visual representations show metrics for all data that fit your criteria. You can display *entries* (e.g. code runs), *unique entries*, and *datasets*. Other more specific metrics might be available. -The results table gives you a quick overview of all entries and datasets that fit your search. -You can click entries to see more details, download data, see the archive, etc. +The results tabs gives you a quick overview of all entries and datasets that fit your search. +You can click entries to see more details, download data, see the archive, etc. The *entries* +tab displays individual entries (e.g. code runs), the *grouped entries* tab will group +entries with similar metadata (e.g. it will group entries for the same material from the + same user). The *dataset* tab, shows entry curated by user created datasets. You can + click on datasets for a search page that will only display entries from the respective + dataset. + +The table columns can be configured. The *entries* tab also supports sorting. Selected +entries (or all entries) can be downloaded. The download will contain all user provided +raw calculation input and output files. + +You can click entries to see more details about them. The details button will navigate +you to an entries page. These entry pages will show more metadata, raw files, the +entry's archive, and processing logs. ` class SearchPage extends React.Component { diff --git a/gui/src/components/uploads/Upload.js b/gui/src/components/uploads/Upload.js index 8b5d338da4175dd775c91be8ab5ebfd338a4678d..de5929d5dafb5a149de2931d06701d783fa370e2 100644 --- a/gui/src/components/uploads/Upload.js +++ b/gui/src/components/uploads/Upload.js @@ -496,6 +496,7 @@ class Upload extends React.Component { data={data} onChange={this.handleChange} actions={actions} + showEntryActions={entry => entry.processed} {...this.state.params} /> } diff --git a/gui/src/components/uploads/Uploads.js b/gui/src/components/uploads/Uploads.js index 0dd782f67a2775607ba1a6ca65559965c20d8d0f..848ec0b4162bf3d27b6e336561c3d0e750f0465a 100644 --- a/gui/src/components/uploads/Uploads.js +++ b/gui/src/components/uploads/Uploads.js @@ -24,7 +24,7 @@ and then it will parse these files. The result will be a list of entries (one pe Each entry is associated with metadata. This is data that NOMAD acquired from your files and that describe your calculations (e.g. chemical formula, used code, system type and symmetry, etc.). Furthermore, you can provide your own metadata (comments, references, co-authors, etc.). -First uploaded data is only visible to you. Before others can actually see and download +At first, uploaded data is only visible to you. Before others can actually see and download your data, you need to publish your upload. #### Prepare and upload files @@ -65,19 +65,19 @@ If you press publish, a dialog will appear that allows you to set an *embargo* or publish your data as *Open Access* right away. The *embargo* allows you to share data with selected users, create a DOI for your data, and later publish the data. The *embargo* might last up to 36 month before data becomes public automatically. -During an *embargo* the data (and datasets created from this data) are only visible to you. +During an *embargo* the data (and datasets created from this data) are only visible to you +and users you *share with* the data. #### Processing errors We distinguish between uploads that fail processing completely and uploads that contain entries that could not be processed. The former might be caused by issues during the upload, bad file formats, etc. The latter (for more common) case means that not all of the provided -code output files could not be parsed by our parsers for various reasons. -The processing logs of the failed entries might provide some insight. +code output files could be parsed by our parsers. The processing logs of the failed entries might provide some insight. -We do not allow the publishing of uploads that fail processing completely. Frankly, in most +You can not publish uploads that failed processing completely. Frankly, in most cases there won't be any data to publish anyways. In the case of failed processing of -some entries, the data can still be published. You will be able to share it and create +some entries however, the data can still be published. You will be able to share it and create DOIs for it, etc. The only shortcomings will be missing metadata (labeled *not processed* or *unavailable*) and missing archive data. We continuously improve our parsers and the now missing information might become available in the future automatically. @@ -88,8 +88,8 @@ You can edit additional *user metadata*. This data is assigned to individual ent you can select and edit many entries at once. Edit buttons for user metadata are available in many views on this web-page. For example, you can edit user metadata when you click on an upload to open its details, and press the edit button there. User metadata can also -be changed after publishing data. The documentation on the [user data page](${guiBase}/userdata) contains more -information. +be changed after publishing data. The documentation on the [user data page](${guiBase}/userdata) +contains more information. ` class Uploads extends React.Component { diff --git a/nomad/app/api/repo.py b/nomad/app/api/repo.py index 5dd7769f2f3d5e7117d061e7ca0dcbdf583b39a5..4d95938488606204c094f7350fa3ad57c9ae0b91 100644 --- a/nomad/app/api/repo.py +++ b/nomad/app/api/repo.py @@ -23,6 +23,7 @@ from flask import request, g from elasticsearch_dsl import Q from elasticsearch.exceptions import NotFoundError import elasticsearch.helpers +from datetime import datetime from nomad import search, utils, datamodel, processing as proc, infrastructure from nomad.app.utils import rfc3339DateTime, RFC3339DateTime, with_logger @@ -502,7 +503,7 @@ class EditRepoCalcsResource(Resource): if not verify: dataset = Dataset( dataset_id=utils.create_uuid(), user_id=g.user.user_id, - name=action_value) + name=action_value, created=datetime.utcnow()) dataset.m_x('me').create() mongo_value = dataset.dataset_id @@ -568,6 +569,7 @@ class EditRepoCalcsResource(Resource): return json_data, 400 # perform the change + mongo_update['metadata__last_edit'] = datetime.utcnow() upload_ids = edit(parsed_query, logger, mongo_update, True) # lift embargo diff --git a/nomad/datamodel/base.py b/nomad/datamodel/base.py index 6f533a62d719d524b695542f8408b67379cd74f8..8ab73b172602b267779e0c7b01262bc2cfc4fa15 100644 --- a/nomad/datamodel/base.py +++ b/nomad/datamodel/base.py @@ -106,6 +106,7 @@ class CalcWithMetadata(Mapping): self.references: List[str] = [] self.datasets: List[str] = [] self.external_id: str = None + self.last_edit: datetime.datetime = None # parser related general (not domain specific) metadata self.parser_name = None diff --git a/nomad/datamodel/metainfo.py b/nomad/datamodel/metainfo.py index c294ce3b1a2d4eb47d518bf34a6993e07cd5f69f..ff6eaebd254ae01b4d462eeeeaaa74c59d2022ba 100644 --- a/nomad/datamodel/metainfo.py +++ b/nomad/datamodel/metainfo.py @@ -93,6 +93,7 @@ class Dataset(metainfo.MSection): full URL, e.g. "10.17172/nomad/2019.10.29-1". pid: The original NOMAD CoE Repository dataset PID. Old DOIs still reference datasets based on this id. Is not used for new datasets. + created: The date when the dataset was first created. """ dataset_id = metainfo.Quantity( type=str, @@ -109,6 +110,9 @@ class Dataset(metainfo.MSection): pid = metainfo.Quantity( type=str, a_me=dict(index=True)) + created = metainfo.Quantity( + type=metainfo.Datetime, + a_me=dict(index=True)) class UserMetadata(metainfo.MSection): diff --git a/nomad/metainfo/mongoengine.py b/nomad/metainfo/mongoengine.py index 2a40afce57d2811220851c4822ece0d1feb780ed..47455a28d6e209a36547d4440ef632a47777aef8 100644 --- a/nomad/metainfo/mongoengine.py +++ b/nomad/metainfo/mongoengine.py @@ -66,11 +66,13 @@ class MESection(): return self.to_metainfo(me_obj) def to_metainfo(self, me_obj): - dct = me_obj.to_mongo().to_dict() - del(dct['_id']) - dct[self.id_quantity] = getattr(me_obj, self.id_quantity) - section = self.section_cls.m_from_dict(dct) # pylint: disable=no-member + section = self.section_cls() section.m_x('me').me_obj = me_obj + for quantity in self.section_cls.m_def.all_quantities.keys(): + value = getattr(me_obj, quantity) + if value is not None: + setattr(section, quantity, value) + return section diff --git a/tests/app/test_api.py b/tests/app/test_api.py index f8eb2b1e75155e234888053139b3f1fe99ec8324..65423030254e53542c2b402a4e5bafbce852a222 100644 --- a/tests/app/test_api.py +++ b/tests/app/test_api.py @@ -1096,11 +1096,13 @@ class TestEditRepo(): assert not has_failure == success assert has_message == message - def mongo(self, *args, **kwargs): + def mongo(self, *args, edited: bool = True, **kwargs): for calc_id in args: calc = Calc.objects(calc_id=str(calc_id)).first() assert calc is not None metadata = calc.metadata + if edited: + assert metadata.get('last_edit') is not None for key, value in kwargs.items(): if metadata.get(key) != value: return False @@ -1151,30 +1153,30 @@ class TestEditRepo(): self.assert_edit(rv, quantity='comment', success=True, message=False) assert self.mongo(1, 2, 3, comment='test_edit_all') assert self.elastic(1, 2, 3, comment='test_edit_all') - assert not self.mongo(4, comment='test_edit_all') - assert not self.elastic(4, comment='test_edit_all') + assert not self.mongo(4, comment='test_edit_all', edited=False) + assert not self.elastic(4, comment='test_edit_all', edited=False) def test_edit_multi(self): rv = self.perform_edit(comment='test_edit_multi', query=dict(upload_id='upload_1,upload_2')) self.assert_edit(rv, quantity='comment', success=True, message=False) assert self.mongo(1, 2, 3, comment='test_edit_multi') assert self.elastic(1, 2, 3, comment='test_edit_multi') - assert not self.mongo(4, comment='test_edit_multi') - assert not self.elastic(4, comment='test_edit_multi') + assert not self.mongo(4, comment='test_edit_multi', edited=False) + assert not self.elastic(4, comment='test_edit_multi', edited=False) def test_edit_some(self): rv = self.perform_edit(comment='test_edit_some', query=dict(upload_id='upload_1')) self.assert_edit(rv, quantity='comment', success=True, message=False) assert self.mongo(1, comment='test_edit_some') assert self.elastic(1, comment='test_edit_some') - assert not self.mongo(2, 3, 4, comment='test_edit_some') - assert not self.elastic(2, 3, 4, comment='test_edit_some') + assert not self.mongo(2, 3, 4, comment='test_edit_some', edited=False) + assert not self.elastic(2, 3, 4, comment='test_edit_some', edited=False) def test_edit_verify(self): rv = self.perform_edit( comment='test_edit_verify', verify=True, query=dict(upload_id='upload_1')) self.assert_edit(rv, quantity='comment', success=True, message=False) - assert not self.mongo(1, comment='test_edit_verify') + assert not self.mongo(1, comment='test_edit_verify', edited=False) def test_edit_empty_list(self, other_test_user): rv = self.perform_edit(coauthors=[other_test_user.user_id], query=dict(upload_id='upload_1'))