Skip to content

Commit 40768ed

Browse files
committed
Implement sorting of collections in REST API
1 parent 0e19af2 commit 40768ed

File tree

3 files changed

+110
-61
lines changed

3 files changed

+110
-61
lines changed

doc/rest.txt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,20 @@ Other data types: Date, Interval Integer, Number need examples and may
344344
need work to allow range searches. Full text search (e.g. over the
345345
body of msgs) is a work in progress.
346346

347+
Sorting
348+
^^^^^^^
349+
350+
Collection endpoints support sorting. This is controlled by specifying a
351+
``@sort`` parameter with a list of properties of the searched class.
352+
Optionally properties can include a sign ('+' or '-') to specify
353+
ascending or descending sort, respectively. If no sign is given,
354+
ascending sort is selected for this property. The following example
355+
would sort by status (in ascending order of the status.order property)
356+
and then by id of an issue::
357+
358+
@sort=status,-id
359+
360+
347361
Pagination
348362
^^^^^^^^^^
349363

roundup/rest.py

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -650,6 +650,7 @@ def get_collection(self, class_name, input):
650650
}
651651
verbose = 1
652652
display_props = {}
653+
sort = []
653654
for form_field in input.value:
654655
key = form_field.name
655656
value = form_field.value
@@ -670,6 +671,24 @@ def get_collection(self, class_name, input):
670671
except KeyError as err:
671672
raise UsageError("Failed to find property '%s' "
672673
"for class %s."%(i, class_name))
674+
elif key == "@sort":
675+
f = value.split(",")
676+
allprops=class_obj.getprops(protected=True)
677+
for p in f :
678+
if not p:
679+
raise UsageError("Empty property "
680+
"for class %s."%(class_name))
681+
if p[0] in ('-', '+'):
682+
pn = p[1:]
683+
ss = p[0]
684+
else:
685+
ss = '+'
686+
pn = p
687+
# Only include properties where we have search permission
688+
if self.db.security.hasSearchPermission(
689+
uid, class_name, pn
690+
):
691+
sort.append((ss, pn))
673692
elif key.startswith("@"):
674693
# ignore any unsupported/previously handled control key
675694
# like @apiver
@@ -707,17 +726,13 @@ def get_collection(self, class_name, input):
707726
filter_props[key]=[filter_props[key],value]
708727
else:
709728
filter_props[key] = value
710-
if not filter_props:
711-
obj_list = class_obj.list()
712-
else:
713-
obj_list = class_obj.filter(None, filter_props)
714-
715-
# Sort list as specified by sortorder
716-
# This is more useful for things where there is an
717-
# explicit order. E.G. status has an order that is
718-
# roughly the progression of the issue through
719-
# the states so open is before closed.
720-
obj_list.sort()
729+
l = [filter_props]
730+
if sort:
731+
l.append(sort)
732+
obj_list = class_obj.filter(None, *l)
733+
734+
# Note: We don't sort explicitly in python. The filter implementation
735+
# of the DB already sorts by ID if no sort option was given.
721736

722737
# add verbose elements. 2 and above get identifying label.
723738
if verbose > 1:

test/rest_common.py

Lines changed: 70 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,46 @@ def get_header (self, header, not_found=None):
118118
except (AttributeError, KeyError, TypeError):
119119
return not_found
120120

121+
def create_stati(self):
122+
try:
123+
self.db.status.create(name='open', order='9')
124+
except ValueError:
125+
pass
126+
try:
127+
self.db.status.create(name='closed', order='91')
128+
except ValueError:
129+
pass
130+
try:
131+
self.db.priority.create(name='normal')
132+
except ValueError:
133+
pass
134+
try:
135+
self.db.priority.create(name='critical')
136+
except ValueError:
137+
pass
138+
139+
def create_sampledata(self):
140+
""" Create sample data common to some test cases
141+
"""
142+
self.create_stati()
143+
self.db.issue.create(
144+
title='foo1',
145+
status=self.db.status.lookup('open'),
146+
priority=self.db.priority.lookup('normal'),
147+
nosy = [ "1", "2" ]
148+
)
149+
issue_open_norm = self.db.issue.create(
150+
title='foo2',
151+
status=self.db.status.lookup('open'),
152+
priority=self.db.priority.lookup('normal'),
153+
assignedto = "3"
154+
)
155+
issue_open_crit = self.db.issue.create(
156+
title='foo5',
157+
status=self.db.status.lookup('open'),
158+
priority=self.db.priority.lookup('critical')
159+
)
160+
121161
def testGet(self):
122162
"""
123163
Retrieve all three users
@@ -167,40 +207,7 @@ def testOutputFormat(self):
167207
""" test of @fields and @verbose implementation """
168208

169209
self.maxDiff = 4000
170-
# create sample data
171-
try:
172-
self.db.status.create(name='open')
173-
except ValueError:
174-
pass
175-
try:
176-
self.db.status.create(name='closed')
177-
except ValueError:
178-
pass
179-
try:
180-
self.db.priority.create(name='normal')
181-
except ValueError:
182-
pass
183-
try:
184-
self.db.priority.create(name='critical')
185-
except ValueError:
186-
pass
187-
self.db.issue.create(
188-
title='foo1',
189-
status=self.db.status.lookup('open'),
190-
priority=self.db.priority.lookup('normal'),
191-
nosy = [ "1", "2" ]
192-
)
193-
issue_open_norm = self.db.issue.create(
194-
title='foo2',
195-
status=self.db.status.lookup('open'),
196-
priority=self.db.priority.lookup('normal'),
197-
assignedto = "3"
198-
)
199-
issue_open_crit = self.db.issue.create(
200-
title='foo5',
201-
status=self.db.status.lookup('open'),
202-
priority=self.db.priority.lookup('critical')
203-
)
210+
self.create_sampledata()
204211
base_path = self.db.config['TRACKER_WEB'] + 'rest/data/issue/'
205212

206213

@@ -425,28 +432,41 @@ def testOutputFormat(self):
425432
results['data']['@etag'] = '' # etag depends on date, set to empty
426433
self.assertDictEqual(expected,results)
427434

435+
def testSorting(self):
436+
self.maxDiff = 4000
437+
self.create_sampledata()
438+
self.db.issue.set('1', status='7')
439+
self.db.issue.set('2', status='2')
440+
self.db.issue.set('3', status='2')
441+
self.db.commit()
442+
base_path = self.db.config['TRACKER_WEB'] + 'rest/data/issue/'
443+
# change some data for sorting on later
444+
form = cgi.FieldStorage()
445+
form.list = [
446+
cgi.MiniFieldStorage('@fields', 'status'),
447+
cgi.MiniFieldStorage('@sort', 'status,-id'),
448+
cgi.MiniFieldStorage('@verbose', '0')
449+
]
450+
451+
# status is sorted by orderprop (property 'order')
452+
# which provides the same ordering as the status ID
453+
expected={'data': {
454+
'@total_size': 3,
455+
'collection': [
456+
{'link': base_path + '3', 'status': '2', 'id': '3'},
457+
{'link': base_path + '2', 'status': '2', 'id': '2'},
458+
{'link': base_path + '1', 'status': '7', 'id': '1'}]}}
459+
460+
results = self.server.get_collection('issue', form)
461+
self.assertDictEqual(expected, results)
462+
428463
def testFilter(self):
429464
"""
430465
Retrieve all three users
431466
obtain data for 'joe'
432467
"""
433468
# create sample data
434-
try:
435-
self.db.status.create(name='open')
436-
except ValueError:
437-
pass
438-
try:
439-
self.db.status.create(name='closed')
440-
except ValueError:
441-
pass
442-
try:
443-
self.db.priority.create(name='normal')
444-
except ValueError:
445-
pass
446-
try:
447-
self.db.priority.create(name='critical')
448-
except ValueError:
449-
pass
469+
self.create_stati()
450470
self.db.issue.create(
451471
title='foo4',
452472
status=self.db.status.lookup('closed'),

0 commit comments

Comments
 (0)