Skip to content

Commit 73f1483

Browse files
authored
update sortby to cover multiple sortables (#1211)
1 parent a617388 commit 73f1483

File tree

4 files changed

+81
-19
lines changed

4 files changed

+81
-19
lines changed

pycsw/ogc/api/records.py

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import logging
3535
from operator import itemgetter
3636
import os
37+
from typing import List, Union
3738
from urllib.parse import urlencode, quote
3839

3940
from owslib.ogcapi.records import Records
@@ -136,7 +137,8 @@ def __init__(self, config: dict):
136137
self.config['repository']['database'],
137138
self.context,
138139
table=self.config['repository']['table'],
139-
repo_filter=repo_filter
140+
repo_filter=repo_filter,
141+
stable_sort=self.config['repository'].get('stable_sort', False)
140142
)
141143
LOGGER.debug(f'Repository loaded {self.repository.dbtype}')
142144
except Exception as err:
@@ -598,9 +600,7 @@ def items(self, headers_, json_post_data, args, collection='metadata:main'):
598600

599601
if 'sortby' in json_post_data:
600602
LOGGER.debug('Detected sortby')
601-
args['sortby'] = json_post_data['sortby'][0]['field']
602-
if json_post_data['sortby'][0].get('direction', 'asc') == 'desc':
603-
args['sortby'] = f"-{args['sortby']}"
603+
args['sortby'] = json_post_data['sortby']
604604

605605
LOGGER.debug(f'Transformed args: {args}')
606606

@@ -757,19 +757,12 @@ def items(self, headers_, json_post_data, args, collection='metadata:main'):
757757
sortby = args['sortby']
758758

759759
if sortby is not None:
760-
LOGGER.debug('processing sortby')
761-
if sortby.startswith('-'):
762-
sortby = sortby.lstrip('-')
760+
sortbys = sortby_to_order_by(sortby, self.repository.query_mappings)
761+
query = query.order_by(*sortbys)
763762

764-
if sortby not in list(self.repository.query_mappings.keys()):
765-
msg = 'Invalid sortby property'
766-
LOGGER.exception(msg)
767-
return self.get_exception(400, headers_, 'InvalidParameterValue', msg)
768-
769-
if args['sortby'].startswith('-'):
770-
query = query.order_by(self.repository.query_mappings[sortby].desc())
771-
else:
772-
query = query.order_by(self.repository.query_mappings[sortby])
763+
if self.repository.stable_sort:
764+
LOGGER.debug('Adding additional stable sort on identifier')
765+
query = query.order_by(self.repository.query_mappings['identifier'].asc())
773766

774767
if limit is None and 'limit' in args:
775768
limit = int(args['limit'])
@@ -1519,3 +1512,38 @@ def build_anytext(name, value):
15191512
predicates.append(f"{name} ILIKE '%{token}%'")
15201513

15211514
return f"({' OR '.join(predicates)})"
1515+
1516+
1517+
def sortby_to_order_by(sortby: Union[str, List[dict]], mappings: dict) -> list:
1518+
1519+
sortby_ = []
1520+
value_list = []
1521+
1522+
if isinstance(sortby, str):
1523+
LOGGER.debug('Normalizing sortby into list of dicts')
1524+
for s in sortby.split(','):
1525+
s2 = s.lstrip('-')
1526+
if s.startswith('-'):
1527+
s2dir = 'desc'
1528+
else:
1529+
s2dir = 'asc'
1530+
1531+
sortby_.append({
1532+
'field': s2,
1533+
'direction': s2dir
1534+
})
1535+
else:
1536+
sortby_ = sortby
1537+
1538+
for sb in sortby_:
1539+
if sb['field'] not in list(mappings.keys()):
1540+
msg = 'Invalid sortby property'
1541+
LOGGER.exception(msg)
1542+
raise ValueError(msg)
1543+
1544+
if sb['direction'] == 'desc':
1545+
value_list.append(mappings[sb['field']].desc())
1546+
else:
1547+
value_list.append(mappings[sb['field']].asc())
1548+
1549+
return value_list

pycsw/stac/api.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -451,9 +451,7 @@ def items(self, headers_, json_post_data, args, collection='metadata:main'):
451451

452452
if 'sortby' in json_post_data2:
453453
LOGGER.debug('Detected sortby parameter')
454-
args['sortby'] = json_post_data2['sortby'][0]['field']
455-
if json_post_data2['sortby'][0].get('direction', 'asc') == 'desc':
456-
args['sortby'] = f"-{args['sortby']}"
454+
args['sortby'] = json_post_data2['sortby']
457455

458456
if 'collections' in json_post_data2:
459457
LOGGER.debug('Detected collections query parameter')

tests/functionaltests/suites/oarec/test_oarec_functional.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,24 @@ def test_items(config):
237237
assert content['numberReturned'] == 10
238238
assert content['features'][5]['properties']['title'] == 'Lorem ipsum dolor sit amet' # noqa
239239

240+
params = {'sortby': '-title,description'}
241+
content = json.loads(api.items({}, None, params)[2])
242+
assert content['numberMatched'] == 12
243+
assert content['numberReturned'] == 10
244+
assert content['features'][0]['properties']['title'] == 'Ñunç elementum'
245+
246+
params = {'sortby': 'title,description'}
247+
content = json.loads(api.items({}, None, params)[2])
248+
assert content['numberMatched'] == 12
249+
assert content['numberReturned'] == 10
250+
assert content['features'][5]['properties']['title'] == 'Lorem ipsum'
251+
252+
params = {'sortby': 'title,-description'}
253+
content = json.loads(api.items({}, None, params)[2])
254+
assert content['numberMatched'] == 12
255+
assert content['numberReturned'] == 10
256+
assert content['features'][6]['properties']['title'] == 'Lorem ipsum dolor sit amet' # noqa
257+
240258
cql_json = {'op': '=', 'args': [{'property': 'title'}, 'Lorem ipsum']}
241259
content = json.loads(api.items({}, cql_json, {})[2])
242260
assert content['numberMatched'] == 1

tests/functionaltests/suites/stac_api/test_stac_api_functional.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,16 @@ def test_items(config):
206206
assert content['numberMatched'] == 26
207207
assert content['features'][6]['properties']['title'] == 'S2B_MSIL1C_20190910T095029_N0208_R079_T33UWQ_20190910T120910.SAFE' # noqa
208208

209+
content = json.loads(api.items({}, None, {'sortby': '-description,-title'})[2]) # noqa
210+
211+
assert content['numberMatched'] == 26
212+
assert content['features'][6]['properties']['title'] == 'S2B_MSIL2A_20190910T095029_N0213_R079_T33UXQ_20190910T124513.SAFE' # noqa
213+
214+
content = json.loads(api.items({}, None, {'sortby': '-description,title'})[2]) # noqa
215+
216+
assert content['numberMatched'] == 26
217+
assert content['features'][6]['properties']['title'] == 'S2B_MSIL1C_20190910T095029_N0208_R079_T33UWQ_20190910T120910.SAFE' # noqa
218+
209219
params = {'filter': "title LIKE '%%T33TWN%%'"}
210220
content = json.loads(api.items({}, None, params)[2])
211221
assert content['numberMatched'] == 4
@@ -246,6 +256,14 @@ def test_items(config):
246256
assert content['numberMatched'] == 26
247257
assert content['features'][5]['properties']['title'] == 'S2B_MSIL2A_20190910T095029_N0500_R079_T33TWN_20230430T083712.SAFE' # noqa
248258

259+
content = json.loads(api.items({},
260+
{'sortby': [{'field': 'title', 'direction': 'asc'}, # noqa
261+
{'field': 'description', 'direction': 'desc'} # noqa
262+
]}, # noqa
263+
{})[2])
264+
assert content['numberMatched'] == 26
265+
assert content['features'][6]['properties']['title'] == 'S2B_MSIL1C_20190910T095029_N0208_R079_T33UWQ_20190910T120910.SAFE' # noqa
266+
249267
headers, status, content = api.items({}, None, {}, 'foo')
250268
assert status == 400
251269

0 commit comments

Comments
 (0)