diff --git a/gui/package.json b/gui/package.json index 4f6ea6b3d875dfce77670d520ef2c4ed77a93ca2..d423dab50af8dc3962ea7ed5c49cd04a5d17295c 100644 --- a/gui/package.json +++ b/gui/package.json @@ -16,6 +16,7 @@ "file-saver": "^2.0.0", "html-to-react": "^1.3.3", "keycloak-js": "^6.0.0", + "lodash": "^4.17.15", "marked": "^0.6.0", "material-ui-chip-input": "^1.0.0-beta.14", "material-ui-flat-pagination": "^3.2.0", diff --git a/gui/src/components/DataTable.js b/gui/src/components/DataTable.js index 36253f6488ac71888e967ce2d43cd9753236c51b..1ce28c096407e0040463d9c4c4abff4bd38184eb 100644 --- a/gui/src/components/DataTable.js +++ b/gui/src/components/DataTable.js @@ -17,6 +17,7 @@ import ViewColumnIcon from '@material-ui/icons/ViewColumn' import { Popover, List, ListItemText, ListItem, Collapse } from '@material-ui/core' import { compose } from 'recompose' import { withDomain } from './domains' +import _ from 'lodash' class DataTableToolbarUnStyled extends React.Component { static propTypes = { @@ -489,7 +490,7 @@ class DataTableUnStyled extends React.Component { key={key} align={column.align || 'left'} > - {column.render ? column.render(row) : row[key]} + {column.render ? column.render(row) : _.get(row, key)} </TableCell> ) })} diff --git a/gui/src/components/Quantity.js b/gui/src/components/Quantity.js index a71b7221f25499e039c6c9ebe76fa9532e7d0ede..0609cd24decbf1f1c8072b20a2c483a71ba0adee 100644 --- a/gui/src/components/Quantity.js +++ b/gui/src/components/Quantity.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types' import { withStyles, Typography, Tooltip, IconButton } from '@material-ui/core' import ClipboardIcon from '@material-ui/icons/Assignment' import { CopyToClipboard } from 'react-copy-to-clipboard' +import _ from 'lodash' class Quantity extends React.Component { static propTypes = { @@ -79,22 +80,14 @@ class Quantity extends React.Component { } if (!loading) { - if (!(data && quantity && !data[quantity])) { - if (!children || children.length === 0) { - const value = data && quantity ? data[quantity] : null - if (value) { - clipboardContent = value - content = <Typography noWrap={noWrap} variant={typography} className={valueClassName}> - {value} - </Typography> - } else { - content = <Typography noWrap={noWrap} variant={typography} className={valueClassName}> - <i>{placeholder || 'unavailable'}</i> - </Typography> - } - } else { - content = children - } + const value = data && quantity && _.get(data, quantity) + if (value && children && children.length !== 0) { + content = children + } else if (value) { + clipboardContent = value + content = <Typography noWrap={noWrap} variant={typography} className={valueClassName}> + {value} + </Typography> } else { content = <Typography noWrap={noWrap} variant={typography} className={valueClassName}> <i>{placeholder || 'unavailable'}</i> diff --git a/gui/src/components/api.js b/gui/src/components/api.js index 9305c2f4325a3abe7978db9e89d405bc6f8e170c..e4ac5bdf52df78b5f416559004c592da8a8d8406 100644 --- a/gui/src/components/api.js +++ b/gui/src/components/api.js @@ -387,7 +387,7 @@ class Api { this.onStartLoading() return this.swagger() .then(client => client.apis.repo.search({ - exclude: ['atoms', 'only_atoms', 'files', 'quantities', 'optimade', 'labels', 'geometries'], + exclude: ['dft.atoms', 'dft.only_atoms', 'dft.files', 'dft.quantities', 'dft.optimade', 'dft.labels', 'dft.geometries'], ...search})) .catch(handleApiError) .then(response => response.body) @@ -398,7 +398,7 @@ class Api { const empty = {} Object.keys(response.statistics.total.all).forEach(metric => empty[metric] = 0) Object.keys(response.statistics) - .filter(key => !['total', 'authors', 'atoms'].includes(key)) + .filter(key => !['total', 'authors', 'dft.atoms'].includes(key)) .forEach(key => { if (!this.statistics[key]) { this.statistics[key] = new Set() diff --git a/gui/src/components/dft/DFTEntryOverview.js b/gui/src/components/dft/DFTEntryOverview.js index 30138450b84ef7e1363b2a5b58161c65810411a9..b047a20b63857b49f9efec9a6b035a62c7d4b61a 100644 --- a/gui/src/components/dft/DFTEntryOverview.js +++ b/gui/src/components/dft/DFTEntryOverview.js @@ -2,6 +2,7 @@ import React from 'react' import PropTypes from 'prop-types' import { Typography } from '@material-ui/core' import Quantity from '../Quantity' +import _ from 'lodash' export default class DFTEntryOverview extends React.Component { static propTypes = { @@ -15,22 +16,22 @@ export default class DFTEntryOverview extends React.Component { return ( <Quantity column> <Quantity row> - <Quantity quantity="formula" label='formula' noWrap {...this.props} /> + <Quantity quantity="dft.formula" label='formula' noWrap {...this.props} /> </Quantity> <Quantity row> - <Quantity quantity="code_name" label='dft code' noWrap {...this.props} /> - <Quantity quantity="code_version" label='dft code version' noWrap {...this.props} /> + <Quantity quantity="dft.code_name" label='dft code' noWrap {...this.props} /> + <Quantity quantity="dft.code_version" label='dft code version' noWrap {...this.props} /> </Quantity> <Quantity row> - <Quantity quantity="basis_set" label='basis set' noWrap {...this.props} /> - <Quantity quantity="xc_functional" label='xc functional' noWrap {...this.props} /> + <Quantity quantity="dft.basis_set" label='basis set' noWrap {...this.props} /> + <Quantity quantity="dft.xc_functional" label='xc functional' noWrap {...this.props} /> </Quantity> <Quantity row> - <Quantity quantity="system" label='system type' noWrap {...this.props} /> - <Quantity quantity="crystal_system" label='crystal system' noWrap {...this.props} /> - <Quantity quantity='spacegroup_symbol' label="spacegroup" noWrap {...this.props}> + <Quantity quantity="dft.system" label='system type' noWrap {...this.props} /> + <Quantity quantity="dft.crystal_system" label='crystal system' noWrap {...this.props} /> + <Quantity quantity='dft.spacegroup_symbol' label="spacegroup" noWrap {...this.props}> <Typography noWrap> - {data.spacegroup_symbol} ({data.spacegroup}) + {_.get(data, 'dft.spacegroup_symbol')} ({_.get(data, 'dft.spacegroup')}) </Typography> </Quantity> </Quantity> diff --git a/gui/src/components/dft/DFTSearchAggregations.js b/gui/src/components/dft/DFTSearchAggregations.js index 76a882b4579408676b4df38d7f0ce31b048db649..ad8dcb322d47105f8d7a3f54fe23a481b278d3a9 100644 --- a/gui/src/components/dft/DFTSearchAggregations.js +++ b/gui/src/components/dft/DFTSearchAggregations.js @@ -69,15 +69,15 @@ class DFTSearchAggregations extends React.Component { return ( <Grid container spacing={24}> <Grid item xs={4}> - <Quantity quantity="code_name" title="Code" scale={0.25} metric={usedMetric} /> + <Quantity quantity="dft.code_name" title="Code" scale={0.25} metric={usedMetric} /> </Grid> <Grid item xs={4}> - <Quantity quantity="system" title="System type" scale={0.25} metric={usedMetric} /> - <Quantity quantity="crystal_system" title="Crystal system" scale={1} metric={usedMetric} /> + <Quantity quantity="dft.system" title="System type" scale={0.25} metric={usedMetric} /> + <Quantity quantity="dft.crystal_system" title="Crystal system" scale={1} metric={usedMetric} /> </Grid> <Grid item xs={4}> - <Quantity quantity="basis_set" title="Basis set" scale={0.25} metric={usedMetric} /> - <Quantity quantity="xc_functional" title="XC functionals" scale={0.5} metric={usedMetric} /> + <Quantity quantity="dft.basis_set" title="Basis set" scale={0.25} metric={usedMetric} /> + <Quantity quantity="dft.xc_functional" title="XC functionals" scale={0.5} metric={usedMetric} /> </Grid> </Grid> ) diff --git a/gui/src/components/domains.js b/gui/src/components/domains.js index 6bd2f5f3f451b4bb00f15e24b0102f0725ae1632..53eee068276fd349def7367a16b3ec5fda02c230 100644 --- a/gui/src/components/domains.js +++ b/gui/src/components/domains.js @@ -53,7 +53,7 @@ export class DomainProvider extends React.Component { // tooltip: 'Aggregates the number of total energy calculations as each entry can contain many calculations.', // renderResultString: count => (<span> with <b>{count.toLocaleString()}</b> total energy calculation{count === 1 ? '' : 's'}</span>) // }, - calculations: { + 'dft.calculations': { label: 'Single configuration calculations', shortLabel: 'SCC', tooltip: 'Aggregates the number of single configuration calculations (e.g. total energy calculations) as each entry can contain many calculations.', @@ -62,7 +62,7 @@ export class DomainProvider extends React.Component { // The unique_geometries search aggregates unique geometries based on 10^8 hashes. // This takes to long in elastic search for a reasonable user experience. // Therefore, we only support geometries without uniqueness check - geometries: { + 'dft.geometries': { label: 'Geometries', shortLabel: 'Geometries', tooltip: 'Aggregates the number of simulated system geometries in all entries.', @@ -85,11 +85,11 @@ export class DomainProvider extends React.Component { mainfile: {}, calc_hash: {}, formula: {}, - optimade: {}, - quantities: {}, - spacegroup: {}, - spacegroup_symbol: {}, - labels: {}, + 'dft.optimade': {}, + 'dft.quantities': {}, + 'dft.spacegroup': {}, + 'dft.spacegroup_symbol': {}, + 'dft.labels': {}, upload_name: {} }, /** @@ -97,40 +97,40 @@ export class DomainProvider extends React.Component { * Default render */ searchResultColumns: { - formula: { + 'dft.formula': { label: 'Formula', supportsSort: true }, - code_name: { + 'dft.code_name': { label: 'Code', supportsSort: true }, - basis_set: { + 'dft.basis_set': { label: 'Basis set', supportsSort: true }, - xc_functional: { + 'dft.xc_functional': { label: 'XT treatment', supportsSort: true }, - system: { + 'dft.system': { label: 'System', supportsSort: true }, - crystal_system: { + 'dft.crystal_system': { label: 'Crystal system', supportsSort: true }, - spacegroup_symbol: { + 'dft.spacegroup_symbol': { label: 'Spacegroup', supportsSort: true }, - spacegroup: { + 'dft.spacegroup': { label: 'Spacegroup (number)', supportsSort: true } }, - defaultSearchResultColumns: ['formula', 'code_name', 'system', 'crystal_system', 'spacegroup_symbol'], + defaultSearchResultColumns: ['dft.formula', 'dft.code_name', 'dft.system', 'dft.crystal_system', 'dft.spacegroup_symbol'], /** * A component to render the domain specific quantities in the metadata card of * the entry view. Needs to work with props: data (the entry data from the API), diff --git a/gui/src/components/search/GroupList.js b/gui/src/components/search/GroupList.js index 2e034af0a6f0c18869a75afa5271608fe8e1a448..76a9852f306985050912bec8501c3ba98acb9c70 100644 --- a/gui/src/components/search/GroupList.js +++ b/gui/src/components/search/GroupList.js @@ -167,7 +167,7 @@ class GroupListUnstyled extends React.Component { render() { const { classes, data, total, groups_after, onChange, actions, domain } = this.props - const groups = data.groups || {values: []} + const groups = data['dft.groups'] || {values: []} const results = Object.keys(groups.values).map(group_hash => { const example = groups.values[group_hash].examples[0] return { diff --git a/gui/src/components/search/Search.js b/gui/src/components/search/Search.js index 6090e780bd38a35586ef9f47dde29e89dadeebdf..5aa58dfc56e4835d74643ca7679443431926c799 100644 --- a/gui/src/components/search/Search.js +++ b/gui/src/components/search/Search.js @@ -120,7 +120,7 @@ class Search extends React.Component { setRequest({ uploads: tab === 'uploads' ? true : undefined, datasets: tab === 'datasets' ? true : undefined, - groups: tab === 'groups' ? true : undefined + 'dft.groups': tab === 'groups' ? true : undefined }) }) } @@ -225,9 +225,9 @@ class ElementsVisualization extends React.Component { this.setState({exclusive: !this.state.exclusive}, () => { const {state: {query}, setQuery} = this.context if (this.state.exclusive) { - setQuery({...query, only_atoms: query.atoms, atoms: []}) + setQuery({...query, 'dft.only_atoms': query['dft.atoms'], 'dft.atoms': []}) } else { - setQuery({...query, atoms: query.only_atoms, only_atoms: []}) + setQuery({...query, 'dft.atoms': query['dft.only_atoms'], 'dft.only_atoms': []}) } }) } @@ -238,7 +238,7 @@ class ElementsVisualization extends React.Component { } const {state: {query}, setQuery} = this.context - setQuery({...query, atoms: atoms, only_atoms: []}) + setQuery({...query, 'dft.atoms': atoms, 'dft.only_atoms': []}) } render() { @@ -249,10 +249,10 @@ class ElementsVisualization extends React.Component { <Card> <CardContent> <PeriodicTable - aggregations={statistics.atoms} + aggregations={statistics['dft.atoms']} metric={metric} exclusive={this.state.exclusive} - values={[...(query.atoms || []), ...(query.only_atoms || [])]} + values={[...(query['dft.atoms'] || []), ...(query['dft.only_atoms'] || [])]} onChanged={this.handleAtomsChanged} onExclusiveChanged={this.handleExclusiveChanged} /> @@ -489,8 +489,8 @@ class SearchGroupList extends React.Component { const {state: {response}, setRequest} = this.context return <GroupList data={response} - total={response.statistics.total.all.groups} - groups_after={response.groups && response.groups.after} + total={response.statistics.total.all['dft.groups']} + groups_after={response['dft.groups'] && response['dft.groups'].after} onChange={setRequest} actions={<ReRunSearchButton/>} {...response} {...this.props} diff --git a/gui/src/components/search/SearchBar.js b/gui/src/components/search/SearchBar.js index 8063720a51adef14ce6f5d1ab176ddf82b4bcbf1..cf49114d3aa3d98d5314caa595fd6fa501b7c1b1 100644 --- a/gui/src/components/search/SearchBar.js +++ b/gui/src/components/search/SearchBar.js @@ -212,9 +212,9 @@ class SearchBar extends React.Component { } if (values[key]) { - values[key] = key === 'atoms' ? [...values[key], value] : value + values[key] = key === 'dft.atoms' ? [...values[key], value] : value } else { - values[key] = key === 'atoms' ? [value] : value + values[key] = key === 'dft.atoms' ? [value] : value } this.setState({ @@ -254,8 +254,8 @@ class SearchBar extends React.Component { getChips() { const {state: {query: {owner, ...values}}} = this.context return Object.keys(values).filter(key => values[key]).map(key => { - if (key === 'atoms') { - return `atoms=[${values[key].join(',')}]` + if (key === 'dft.atoms') { + return `dft.atoms=[${values[key].join(',')}]` } else { return `${key}=${values[key]}` } diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index 4c5b33184d02cb80dfe9a416d7448a12ea740446..e4284c21356bc294f096402f984578fe069b82bc 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -63,11 +63,11 @@ class SearchContext extends React.Component { } handleQueryChange(changes, replace) { - if (changes.atoms && changes.atoms.length === 0) { - changes.atoms = undefined + if (changes['dft.atoms'] && changes['dft.atoms'].length === 0) { + changes['dft.atoms'] = undefined } - if (changes.only_atoms && changes.only_atoms.length === 0) { - changes.only_atoms = undefined + if (changes['dft.only_atoms'] && changes['dft.only_atoms'].length === 0) { + changes['dft.only_atoms'] = undefined } if (replace) { this.setState({query: changes}) diff --git a/gui/yarn.lock b/gui/yarn.lock index 1859a513633ec280a6569cf35c4adc2bd1e99513..42d850beae8b8ab8771019a9606d9850c6391cd3 100644 --- a/gui/yarn.lock +++ b/gui/yarn.lock @@ -5162,6 +5162,11 @@ lodash.uniq@^4.5.0: version "4.17.11" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" +lodash@^4.17.15: + version "4.17.15" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" + integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== + loglevel@^1.4.1: version "1.6.1" resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.1.tgz#e0fc95133b6ef276cdc8887cdaf24aa6f156f8fa" diff --git a/nomad/app/api/repo.py b/nomad/app/api/repo.py index e48d8d12e59404d2b9fe0c6363956caa6a43a4ce..8dc03e6672ea8abf92ab999ad1b0578ff068d67d 100644 --- a/nomad/app/api/repo.py +++ b/nomad/app/api/repo.py @@ -28,7 +28,7 @@ from datetime import datetime from nomad import search, utils, datamodel, processing as proc, infrastructure from nomad.datamodel import UserMetadata, Dataset, User from nomad.app import common -from nomad.app.common import RFC3339DateTime +from nomad.app.common import RFC3339DateTime, DotKeyNested from .api import api from .auth import authenticate @@ -105,7 +105,7 @@ _repo_calcs_model_fields = { 'A string of curl command which can be executed to reproduce the api result.')), } for group_name, (group_quantity, _) in search.groups.items(): - _repo_calcs_model_fields[group_name] = fields.Nested(api.model('RepoDatasets', { + _repo_calcs_model_fields[group_name] = (DotKeyNested if '.' in group_name else fields.Nested)(api.model('RepoGroup', { 'after': fields.String(description='The after value that can be used to retrieve the next %s.' % group_name), 'values': fields.Raw(description='A dict with %s as key. The values are dicts with "total" and "examples" keys.' % group_quantity) }), skip_none=True) diff --git a/nomad/app/common.py b/nomad/app/common.py index c39a9add751644dbe84bd28269d51c23d772b100..3ca5c2999592ae2abfe58b6e649d08461ce44a18 100644 --- a/nomad/app/common.py +++ b/nomad/app/common.py @@ -16,6 +16,7 @@ from structlog import BoundLogger from flask_restplus import fields from datetime import datetime import pytz +from contextlib import contextmanager from nomad import config @@ -37,3 +38,44 @@ class RFC3339DateTime(fields.DateTime): rfc3339DateTime = RFC3339DateTime() + + +class DotKeyFieldMixin: + """ Allows use of flask_restplus fields with '.' in key names. By default, '.' + is used as a separator for accessing nested properties. Mixin prevents this, + allowing fields to use '.' in the key names. + + Example of issue: + >>> data = {"my.dot.field": 1234} + >>> model = {"my.dot.field: fields.String} + >>> marshal(data, model) + {"my.dot.field:": None} + + flask_restplus tries to fetch values for data['my']['dot']['field'] instead + of data['my.dot.field'] which is the desired behaviour in this case. + """ + + def output(self, key, obj, **kwargs): + transformed_obj = {k.replace(".", "___"): v for k, v in obj.items()} + transformed_key = key.replace(".", "___") + # if self.attribute is set and contains '.' super().output() will + # use '.' as a separator for nested access. + # -> temporarily set to None to overcome this + with self.toggle_attribute(): + data = super().output(transformed_key, transformed_obj) + return data + + @contextmanager + def toggle_attribute(self): + """ Context manager to temporarily set self.attribute to None + + Yields self.attribute before setting to None + """ + attribute = self.attribute + self.attribute = None + yield attribute + self.attribute = attribute + + +class DotKeyNested(DotKeyFieldMixin, fields.Nested): + pass diff --git a/nomad/datamodel/base.py b/nomad/datamodel/base.py index 35eed394692d08724572e7b55d764d756d061a33..fba80e74bd5aeac2f4e7e358ee6d437eb44687b7 100644 --- a/nomad/datamodel/base.py +++ b/nomad/datamodel/base.py @@ -293,7 +293,7 @@ class Domain: instances of :class:`DomainQuantity`. metrics: Tuples of elastic field name and elastic aggregation operation that can be used to create statistic values. - group_quantities: Tuple of quantity name and metric that describes quantities that + groups: Tuple of quantity name and metric that describes quantities that can be used to group entries by quantity values. root_sections: The name of the possible root sections for this domain. metainfo_all_package: The name of the full metainfo package for this domain. @@ -309,6 +309,9 @@ class Domain: uploader_id=DomainQuantity( elastic_field='uploader.user_id', multi=False, aggregations=5, description=('Search for the given uploader id.')), + uploader_name=DomainQuantity( + elastic_field='uploader.name.keyword', multi=False, + description=('Search for the exact uploader\'s full name')), comment=DomainQuantity( elastic_search_type='match', multi=True, description='Search within the comments. This is a text search ala google.'), @@ -355,10 +358,10 @@ class Domain: description='Search for a particular dataset by doi (incl. http://dx.doi.org).')) base_metrics = dict( - datasets=('datasets.id', 'cardinality'), + datasets=('datasets_id', 'cardinality'), uploads=('upload_id', 'cardinality'), - uploaders=('uploader.name.keyword', 'cardinality'), - authors=('authors.name.keyword', 'cardinality'), + uploaders=('uploader_name', 'cardinality'), + authors=('authors', 'cardinality'), unique_entries=('calc_hash', 'cardinality')) base_groups = dict( @@ -393,12 +396,7 @@ class Domain: root_sections=['section_run', 'section_entry_info'], metainfo_all_package='all.nomadmetainfo.json') -> None: - for quantity in quantities.values(): - quantity.domain = name - domain_quantities = quantities - domain_metrics = metrics - domain_groups = groups Domain.instances[name] = self @@ -416,15 +414,24 @@ class Domain: for quantity_name in reference_domain_calc.__dict__.keys(): if not hasattr(reference_general_calc, quantity_name): quantity = domain_quantities.get(quantity_name, None) - if quantity is None: - quantity = DomainQuantity() - quantity.domain = name - domain_quantities[quantity_name] = quantity + domain_quantities[quantity_name] = DomainQuantity() - # add all domain quantities + # ensure domain quantity names and domains for quantity_name, quantity in domain_quantities.items(): + quantity.domain = name quantity.name = quantity_name + + # add domain prefix to domain metrics and groups + domain_metrics = { + '%s.%s' % (name, key): (quantities[quantity].qualified_elastic_field, es_op) + for key, (quantity, es_op) in metrics.items()} + domain_groups = { + '%s.%s' % (name, key): (quantities[quantity].qualified_name, '%s.%s' % (name, metric)) + for key, (quantity, metric) in groups.items()} + + # add all domain quantities + for quantity_name, quantity in domain_quantities.items(): self.domain_quantities[quantity.name] = quantity # update the multi status from an example value diff --git a/tests/app/test_api.py b/tests/app/test_api.py index 74cb9fe1f0eb3c4c7980e4cdaecabdf9e67b1df6..32188dcf1df29315e0a3f740df0baacb13707946 100644 --- a/tests/app/test_api.py +++ b/tests/app/test_api.py @@ -922,9 +922,16 @@ class TestRepo(): @pytest.mark.parametrize('metrics', metrics_permutations) def test_search_aggregation_metrics(self, api, example_elastic_calcs, no_warn, metrics): - rv = api.get('/repo/?%s' % urlencode(dict(metrics=metrics, statistics=True, datasets=True, uploads=True), doseq=True)) + rv = api.get('/repo/?%s' % urlencode({ + 'metrics': metrics, + 'statistics': True, + 'dft.groups': True, + 'datasets': True, + 'uploads': True}, doseq=True)) + assert rv.status_code == 200 data = json.loads(rv.data) + for name, quantity in data.get('statistics').items(): for metrics_result in quantity.values(): assert 'code_runs' in metrics_result @@ -934,8 +941,14 @@ class TestRepo(): else: assert len(metrics_result) == 1 # code_runs is the only metric for authors + for group in ['dft.groups', 'uploads', 'datasets']: + assert group in data + assert 'after' in data[group] + assert 'values' in data[group] + # assert len(data[group]['values']) == data['statistics']['total']['all'][group] + def test_search_date_histogram(self, api, example_elastic_calcs, no_warn): - rv = api.get('/repo/?date_histogram=true&metrics=total_energies') + rv = api.get('/repo/?date_histogram=true&metrics=dft.total_energies') assert rv.status_code == 200 data = json.loads(rv.data) histogram = data.get('statistics').get('date_histogram') diff --git a/tests/test_cli.py b/tests/test_cli.py index 04f7ce671b663b3445028747ba9f7acf7d4162ef..adeef960e8598fb1f10ab92852c01fadd5d30d86 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -212,6 +212,7 @@ class TestAdminUploads: assert upload.tasks_status == proc.PENDING assert calc.tasks_status == proc.PENDING + @pytest.mark.usefixtures('reset_config') class TestClient: