Commit 8329f0f5 authored by Markus Scheidgen's avatar Markus Scheidgen
Browse files

Added tests and fixes for structure list sort and pagination. #450

parent af410df1
Pipeline #87896 passed with stages
in 35 minutes and 44 seconds
......@@ -44,11 +44,23 @@ class CalculationList(Resource):
filter = request.args.get('filter', None)
page_limit = int(request.args.get('page_limit', 10))
page_number = int(request.args.get('page_number', 1))
sort = request.args.get('sort', 'chemical_formula_reduced')
sort = request.args.get('sort', request.args.get('sort', 'chemical_formula_reduced'))
order = 1
if sort[0:1] == '-':
order = -1
sort = sort[1:]
except Exception:
abort(400, message='bad parameter types') # TODO Specific json API error handling
sort_quantity = OptimadeEntry.m_def.all_quantities.get(sort, None)
if sort_quantity is None:
abort(400, message='cannot sort by %s' % sort) # TODO Specific json API error handling
sort_quantity_a_optimade = sort_quantity.m_get_annotations('optimade')
if not sort_quantity_a_optimade.sortable:
abort(400, message='cannot sort by %s' % sort) # TODO Specific json API error handling
search_request = base_search_request().include('calc_id', 'upload_id')
if filter is not None:
......@@ -59,8 +71,9 @@ class CalculationList(Resource):
result = search_request.execute_paginated(
page=page_number,
per_page=page_limit)
# order_by='optimade.%s' % sort) # TODO map the Optimade property
per_page=page_limit,
order=order,
order_by='dft.optimade.%s' % sort)
returned = result['pagination']['total']
results = to_calc_with_metadata(result['results'])
......@@ -71,7 +84,7 @@ class CalculationList(Resource):
query=request.url,
returned=returned,
available=nentries(),
last_id=results[-1].calc_id if returned > 0 else None),
last_id=results[-1].calc_id if len(results) > 0 else None),
links=LinksModel(
'calculations',
returned=returned,
......
......@@ -234,10 +234,16 @@ json_api_info_model = api.model('CalculationInfo', {
@cached({})
def get_entry_properties(include_optimade: bool = True):
properties = {
attr.name: dict(description=attr.description)
for attr in OptimadeEntry.m_def.all_properties.values()
if include_optimade}
properties = {}
if include_optimade:
for attr in OptimadeEntry.m_def.all_properties.values():
prop = dict(description=attr.description)
a_optimade = attr.m_get_annotations('optimade')
if a_optimade is not None:
if a_optimade.type is not None:
prop['type'] = a_optimade.type
if a_optimade.sortable is not None:
prop['sortable'] = a_optimade.sortable
def add_nmd_properties(prefix, section_cls):
for quantity in section_cls.m_def.all_quantities.values():
......
......@@ -44,11 +44,23 @@ class StructureList(Resource):
filter = request.args.get('filter', None)
page_limit = int(request.args.get('page_limit', 10))
page_number = int(request.args.get('page_number', 1))
sort = request.args.get('sort', 'chemical_formula_reduced')
sort = request.args.get('sort', request.args.get('sort', 'chemical_formula_reduced'))
order = 1
if sort[0:1] == '-':
order = -1
sort = sort[1:]
except Exception:
abort(400, message='bad parameter types') # TODO Specific json API error handling
sort_quantity = OptimadeEntry.m_def.all_quantities.get(sort, None)
if sort_quantity is None:
abort(400, message='cannot sort by %s' % sort) # TODO Specific json API error handling
sort_quantity_a_optimade = sort_quantity.m_get_annotations('optimade')
if not sort_quantity_a_optimade.sortable:
abort(400, message='cannot sort by %s' % sort) # TODO Specific json API error handling
search_request = base_search_request().include('calc_id', 'upload_id')
if filter is not None:
......@@ -59,8 +71,9 @@ class StructureList(Resource):
result = search_request.execute_paginated(
page=page_number,
per_page=page_limit)
# order_by='optimade.%s' % sort) # TODO map the Optimade property
per_page=page_limit,
order=order,
order_by='dft.optimade.%s' % sort)
returned = result['pagination']['total']
results = to_calc_with_metadata(result['results'])
......@@ -71,7 +84,7 @@ class StructureList(Resource):
query=request.url,
returned=returned,
available=nentries(),
last_id=results[-1].calc_id if returned > 0 else None),
last_id=results[-1].calc_id if len(results) > 0 else None),
links=LinksModel(
'structures',
returned=returned,
......
......@@ -45,8 +45,9 @@ class ElementRatio(InnerDoc):
class Optimade(DefinitionAnnotation):
def __init__(self, query: bool = False, entry: bool = False):
pass
def __init__(self, query: bool = False, entry: bool = False, sortable: bool = False, type: str = None):
self.sortable = sortable
self.type = type
class Species(MSection):
......@@ -59,13 +60,13 @@ class Species(MSection):
m_def = Section(links=optimade_links('h.6.2.13'))
name = Quantity(
type=str, a_optimade=Optimade(entry=True), description='''
type=str, a_optimade=Optimade(entry=True, sortable=False, type='string'), description='''
The name of the species; the name value MUST be unique in the species list.
''')
chemical_symbols = Quantity(
type=MEnum(chemical_symbols + ['x', 'vacancy']), shape=['1..*'],
a_optimade=Optimade(entry=True), description='''
a_optimade=Optimade(entry=True, sortable=False, type='list'), description='''
A list of strings of all chemical elements composing this species.
It MUST be one of the following:
......@@ -83,7 +84,7 @@ class Species(MSection):
concentration = Quantity(
type=float, shape=['1..*'],
a_optimade=Optimade(entry=True), description='''
a_optimade=Optimade(entry=True, sortable=False, type='list'), description='''
A list of floats, with same length as chemical_symbols. The numbers represent the
relative concentration of the corresponding chemical symbol in this species. The
numbers SHOULD sum to one. Cases in which the numbers do not sum to one typically fall
......@@ -101,9 +102,9 @@ class Species(MSection):
species).
''')
mass = Quantity(type=float, unit=ureg.amu, a_optimade=dict(entry='optional'))
mass = Quantity(type=float, unit=ureg.amu, a_optimade=dict(entry='optional', sortable=False, type='float'))
original_name = Quantity(type=str, a_optimade=dict(entry='optional'), description='''
original_name = Quantity(type=str, a_optimade=dict(entry='optional', sortable=False, type='string'), description='''
Can be any valid Unicode string, and SHOULD contain (if specified) the name of the
species that is used internally in the source database.
......@@ -123,7 +124,7 @@ class OptimadeEntry(MSection):
type=MEnum(chemical_symbols), shape=['1..*'],
links=optimade_links('h.6.2.1'),
a_search=Search(),
a_optimade=Optimade(query=True, entry=True),
a_optimade=Optimade(query=True, entry=True, sortable=False, type='list'),
description='''
Names of the different elements present in the structure.
''')
......@@ -132,7 +133,7 @@ class OptimadeEntry(MSection):
type=int, default=0,
links=optimade_links('h.6.2.2'),
a_search=Search(),
a_optimade=Optimade(query=True, entry=True),
a_optimade=Optimade(query=True, entry=True, sortable=True, type='integer'),
description='''
Number of different elements in the structure as an integer.
''')
......@@ -141,7 +142,7 @@ class OptimadeEntry(MSection):
type=float, shape=['nelements'],
links=optimade_links('h.6.2.3'),
a_search=Search(mapping=Nested(ElementRatio), value=ElementRatio.from_structure_entry),
a_optimade=Optimade(query=True, entry=True),
a_optimade=Optimade(query=True, entry=True, sortable=False, type='list'),
description='''
Relative proportions of different elements in the structure.
''')
......@@ -150,7 +151,7 @@ class OptimadeEntry(MSection):
type=str,
links=optimade_links('h.6.2.4'),
a_search=Search(),
a_optimade=Optimade(query=True, entry=True),
a_optimade=Optimade(query=True, entry=True, sortable=True, type='string'),
description='''
The chemical formula for a structure as a string in a form chosen by the API
implementation.
......@@ -160,7 +161,7 @@ class OptimadeEntry(MSection):
type=str,
links=optimade_links('h.6.2.5'),
a_search=Search(),
a_optimade=Optimade(query=True, entry=True),
a_optimade=Optimade(query=True, entry=True, sortable=True, type='string'),
description='''
The reduced chemical formula for a structure as a string with element symbols and
integer chemical proportion numbers. The proportion number MUST be omitted if it is 1.
......@@ -170,7 +171,7 @@ class OptimadeEntry(MSection):
type=str,
links=optimade_links('h.6.2.6'),
a_search=Search(),
a_optimade=Optimade(query=True, entry=False),
a_optimade=Optimade(query=True, entry=False, sortable=True, type='string'),
description='''
The chemical formula for a structure in Hill form with element symbols followed by
integer chemical proportion numbers. The proportion number MUST be omitted if it is 1.
......@@ -180,7 +181,7 @@ class OptimadeEntry(MSection):
type=str,
links=optimade_links('h.6.2.7'),
a_search=Search(),
a_optimade=Optimade(query=True, entry=True),
a_optimade=Optimade(query=True, entry=True, sortable=True, type='string'),
description='''
The anonymous formula is the chemical_formula_reduced, but where the elements are
instead first ordered by their chemical proportion number, and then, in order left to
......@@ -192,7 +193,7 @@ class OptimadeEntry(MSection):
type=int, shape=[3], default=[0, 0, 0],
links=optimade_links('h.6.2.8'),
a_search=Search(value=lambda a: sum(a.dimension_types), mapping=Integer()),
a_optimade=Optimade(query=True, entry=True),
a_optimade=Optimade(query=True, entry=True, sortable=False, type='list'),
description='''
List of three integers. For each of the three directions indicated by the three lattice
vectors (see property lattice_vectors). This list indicates if the direction is
......@@ -204,7 +205,7 @@ class OptimadeEntry(MSection):
lattice_vectors = Quantity(
type=np.dtype('f8'), shape=[3, 3], unit=ureg.angstrom,
links=optimade_links('h.6.2.9'),
a_optimade=Optimade(query=False, entry=True),
a_optimade=Optimade(query=False, entry=True, sortable=False, type='list'),
description='''
The three lattice vectors in Cartesian coordinates, in ångström (Å).
''')
......@@ -212,7 +213,7 @@ class OptimadeEntry(MSection):
cartesian_site_positions = Quantity(
type=np.dtype('f8'), shape=['nsites', 3], unit=ureg.angstrom,
links=optimade_links('h.6.2.10'),
a_optimade=Optimade(query=False, entry=True), description='''
a_optimade=Optimade(query=False, entry=True, sortable=False, type='list'), description='''
Cartesian positions of each site. A site is an atom, a site potentially occupied by
an atom, or a placeholder for a virtual mixture of atoms (e.g., in a virtual crystal
approximation).
......@@ -222,14 +223,14 @@ class OptimadeEntry(MSection):
type=int, default=0,
links=optimade_links('h.6.2.11'),
a_search=Search(),
a_optimade=Optimade(query=True, entry=True), description='''
a_optimade=Optimade(query=True, entry=True, sortable=True, type='integer'), description='''
An integer specifying the length of the cartesian_site_positions property.
''')
species_at_sites = Quantity(
type=str, shape=['nsites'],
links=optimade_links('h.6.2.12'),
a_optimade=Optimade(query=False, entry=True), description='''
a_optimade=Optimade(query=False, entry=True, sortable=False, type='list'), description='''
Name of the species at each site (where values for sites are specified with the same
order of the cartesian_site_positions property). The properties of the species are
found in the species property.
......@@ -241,7 +242,7 @@ class OptimadeEntry(MSection):
type=MEnum(['disorder', 'unknown_positions', 'assemblies']), shape=['1..*'],
links=optimade_links('h.6.2.15'),
a_search=Search(),
a_optimade=Optimade(query=True, entry=True), description='''
a_optimade=Optimade(query=True, entry=True, sortable=False, type='list'), description='''
A list of strings that flag which special features are used by the structure.
- disorder: This flag MUST be present if any one entry in the species list has a
......
......@@ -179,6 +179,33 @@ def assert_eq_attrib(data, key, ref, item=None):
assert data['data'][item]['attributes'][key] == ref
@pytest.mark.parametrize('limit, number, results', [
(1, 1, 1), (1, 5, 0), (5, 1, 4)
])
def test_list_endpoint_pagination(api, example_structures, limit, number, results):
rv = api.get('/structures?page_limit=%d&page_number=%d' % (limit, number))
assert rv.status_code == 200
data = json.loads(rv.data)
assert len(data['data']) == results
@pytest.mark.parametrize('sort, order', [
('nelements', 1), ('-nelements', -1)
])
def test_list_endpoint_sort(api, example_structures, sort, order):
rv = api.get('/structures?sort=%s' % sort)
assert rv.status_code == 200
data = json.loads(rv.data)['data']
assert len(data) > 0
for i, item in enumerate(data):
if i > 0:
if order == 1:
assert item['attributes']['nelements'] >= data[i - 1]['attributes']['nelements']
else:
assert item['attributes']['nelements'] <= data[i - 1]['attributes']['nelements']
def test_list_endpoint_response_fields(api, example_structures):
rv = api.get('/structures?response_fields=nelements,elements')
assert rv.status_code == 200
......
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