Skip to content

Commit 6f90a24

Browse files
committed
Add @group for grouping in rest interface.
Helpful for using optgroup in select boxes.
1 parent b6dc3ff commit 6f90a24

File tree

4 files changed

+121
-1
lines changed

4 files changed

+121
-1
lines changed

CHANGES.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,8 @@ Features:
161161
jinja function. expandfile allows setting a dictionary and tokens in
162162
the file of the form "%(token_name)s" will be replaced in the file
163163
with the values from the dict. (John Rouillard)
164+
- add @group to rest interface collection queries. Useful when using
165+
optgroup in select elements. (John Rouillard)
164166

165167
2023-07-13 2.3.0
166168

doc/rest.txt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -650,6 +650,29 @@ and then by id of an issue::
650650

651651
@sort=status,-id
652652

653+
Grouping
654+
~~~~~~~~
655+
656+
Collection endpoints support grouping. This is controlled by
657+
specifying a ``@group`` parameter with a list of properties of
658+
the searched class. Optionally properties can include a sign
659+
('+' or '-') to specify the groups are sorted in ascending or
660+
descending order, respectively. If no sign is given, the groups
661+
are returned in ascending order. The following example would
662+
return the issues grouped by status (in order from
663+
unread->reolved) then within each status, by priority in
664+
descending order (wish -> critical)::
665+
666+
@group=status,-priority
667+
668+
Adding ``@fields=status,priority`` to the query will allow you to see
669+
the status and priority values change so you can identify the items in
670+
each group.
671+
672+
If combined with ``@sort=-id`` within each group he items would be
673+
sorted in descending order by id.
674+
675+
This is useful for select elements that use optgroup.
653676

654677
Pagination
655678
~~~~~~~~~~

roundup/rest.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -804,6 +804,7 @@ def get_collection(self, class_name, input):
804804
verbose = 1
805805
display_props = set()
806806
sort = []
807+
group = []
807808
for form_field in input.value:
808809
key = form_field.name
809810
value = form_field.value
@@ -841,6 +842,29 @@ def get_collection(self, class_name, input):
841842
raise (Unauthorised(
842843
'User does not have search permission on "%s.%s"'
843844
% (class_name, pn)))
845+
elif key == "@group":
846+
f = value.split(",")
847+
for p in f:
848+
if not p:
849+
raise UsageError("Empty property "
850+
"for class %s." % (class_name))
851+
if p[0] in ('-', '+'):
852+
pn = p[1:]
853+
ss = p[0]
854+
else:
855+
ss = '+'
856+
pn = p
857+
# Only include properties where we have search permission
858+
# Note that hasSearchPermission already returns 0 for
859+
# non-existing properties.
860+
if self.db.security.hasSearchPermission(
861+
uid, class_name, pn
862+
):
863+
group.append((ss, pn))
864+
else:
865+
raise (Unauthorised(
866+
'User does not have search permission on "%s.%s"'
867+
% (class_name, pn)))
844868
elif key.startswith("@"):
845869
# ignore any unsupported/previously handled control key
846870
# like @apiver
@@ -912,6 +936,8 @@ def get_collection(self, class_name, input):
912936
kw = {}
913937
if sort:
914938
l.append(sort)
939+
if group:
940+
l.append(group)
915941
if exact_props:
916942
kw['exact_match_spec'] = exact_props
917943
if page['size'] is None:

test/rest_common.py

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ def create_stati(self):
316316
except ValueError:
317317
pass
318318

319-
def create_sampledata(self):
319+
def create_sampledata(self, data_max=3):
320320
""" Create sample data common to some test cases
321321
"""
322322
self.create_stati()
@@ -338,6 +338,34 @@ def create_sampledata(self):
338338
priority=self.db.priority.lookup('critical')
339339
)
340340

341+
if data_max > 10:
342+
raise ValueError('data_max must be less than 10')
343+
344+
if data_max == 3:
345+
return
346+
347+
sample_data = [
348+
["foo6", "normal", "closed"],
349+
["foo7", "critical", "open"],
350+
["foo8", "normal", "open"],
351+
["foo9", "critical", "open"],
352+
["foo10", "normal", "closed"],
353+
["foo11", "critical", "open"],
354+
["foo12", "normal", "closed"],
355+
["foo13", "normal", "open"],
356+
357+
]
358+
359+
for title, priority, status in sample_data:
360+
new_issue = self.db.issue.create(
361+
title=title,
362+
status=self.db.status.lookup(status),
363+
priority=self.db.priority.lookup(priority)
364+
)
365+
366+
if int(new_issue) == data_max:
367+
break
368+
341369
def test_no_next_link_on_full_last_page(self):
342370
"""Make sure that there is no next link
343371
on the last page where the total number of entries
@@ -971,6 +999,47 @@ def testSorting(self):
971999
results = self.server.get_collection('issue', form)
9721000
self.assertDictEqual(expected, results)
9731001

1002+
def testGrouping(self):
1003+
self.maxDiff = 4000
1004+
self.create_sampledata(data_max=5)
1005+
self.db.issue.set('1', status='7', priority='4')
1006+
self.db.issue.set('2', status='2', priority='4')
1007+
self.db.issue.set('3', status='2', priority='4')
1008+
self.db.issue.set('4', status='2', priority='2')
1009+
self.db.issue.set('5', status='2', priority='2')
1010+
self.db.commit()
1011+
base_path = self.db.config['TRACKER_WEB'] + 'rest/data/issue/'
1012+
# change some data for sorting on later
1013+
form = cgi.FieldStorage()
1014+
form.list = [
1015+
cgi.MiniFieldStorage('@fields', 'status,priority'),
1016+
cgi.MiniFieldStorage('@sort', '-id'),
1017+
cgi.MiniFieldStorage('@group', '-status,priority'),
1018+
cgi.MiniFieldStorage('@verbose', '0')
1019+
]
1020+
1021+
# status is sorted by orderprop (property 'order')
1022+
expected={'data': {
1023+
'@total_size': 5,
1024+
'collection': [
1025+
{'link': base_path + '1', 'priority': '4',
1026+
'status': '7', 'id': '1'},
1027+
{'link': base_path + '5', 'priority': '2',
1028+
'status': '2', 'id': '5'},
1029+
{'link': base_path + '4', 'priority': '2',
1030+
'status': '2', 'id': '4'},
1031+
{'link': base_path + '3', 'priority': '4',
1032+
'status': '2', 'id': '3'},
1033+
{'link': base_path + '2', 'priority': '4',
1034+
'status': '2', 'id': '2'},
1035+
]
1036+
}}
1037+
1038+
1039+
results = self.server.get_collection('issue', form)
1040+
print(results)
1041+
self.assertDictEqual(expected, results)
1042+
9741043
def testTransitiveField(self):
9751044
""" Test a transitive property in @fields """
9761045
base_path = self.db.config['TRACKER_WEB'] + 'rest/data/'

0 commit comments

Comments
 (0)