Mercurial > p > roundup > code
changeset 7854:171ff2e487df
Add @group for grouping in rest interface.
Helpful for using optgroup in select boxes.
| author | John Rouillard <rouilj@ieee.org> |
|---|---|
| date | Mon, 01 Apr 2024 14:42:36 -0400 |
| parents | 03c1b7ae3a68 |
| children | 047c02dfa267 |
| files | CHANGES.txt doc/rest.txt roundup/rest.py test/rest_common.py |
| diffstat | 4 files changed, 121 insertions(+), 1 deletions(-) [+] |
line wrap: on
line diff
--- a/CHANGES.txt Mon Apr 01 09:57:16 2024 -0400 +++ b/CHANGES.txt Mon Apr 01 14:42:36 2024 -0400 @@ -161,6 +161,8 @@ jinja function. expandfile allows setting a dictionary and tokens in the file of the form "%(token_name)s" will be replaced in the file with the values from the dict. (John Rouillard) +- add @group to rest interface collection queries. Useful when using + optgroup in select elements. (John Rouillard) 2023-07-13 2.3.0
--- a/doc/rest.txt Mon Apr 01 09:57:16 2024 -0400 +++ b/doc/rest.txt Mon Apr 01 14:42:36 2024 -0400 @@ -650,6 +650,29 @@ @sort=status,-id +Grouping +~~~~~~~~ + +Collection endpoints support grouping. This is controlled by +specifying a ``@group`` parameter with a list of properties of +the searched class. Optionally properties can include a sign +('+' or '-') to specify the groups are sorted in ascending or +descending order, respectively. If no sign is given, the groups +are returned in ascending order. The following example would +return the issues grouped by status (in order from +unread->reolved) then within each status, by priority in +descending order (wish -> critical):: + + @group=status,-priority + +Adding ``@fields=status,priority`` to the query will allow you to see +the status and priority values change so you can identify the items in +each group. + +If combined with ``@sort=-id`` within each group he items would be +sorted in descending order by id. + +This is useful for select elements that use optgroup. Pagination ~~~~~~~~~~
--- a/roundup/rest.py Mon Apr 01 09:57:16 2024 -0400 +++ b/roundup/rest.py Mon Apr 01 14:42:36 2024 -0400 @@ -804,6 +804,7 @@ verbose = 1 display_props = set() sort = [] + group = [] for form_field in input.value: key = form_field.name value = form_field.value @@ -841,6 +842,29 @@ raise (Unauthorised( 'User does not have search permission on "%s.%s"' % (class_name, pn))) + elif key == "@group": + f = value.split(",") + for p in f: + if not p: + raise UsageError("Empty property " + "for class %s." % (class_name)) + if p[0] in ('-', '+'): + pn = p[1:] + ss = p[0] + else: + ss = '+' + pn = p + # Only include properties where we have search permission + # Note that hasSearchPermission already returns 0 for + # non-existing properties. + if self.db.security.hasSearchPermission( + uid, class_name, pn + ): + group.append((ss, pn)) + else: + raise (Unauthorised( + 'User does not have search permission on "%s.%s"' + % (class_name, pn))) elif key.startswith("@"): # ignore any unsupported/previously handled control key # like @apiver @@ -912,6 +936,8 @@ kw = {} if sort: l.append(sort) + if group: + l.append(group) if exact_props: kw['exact_match_spec'] = exact_props if page['size'] is None:
--- a/test/rest_common.py Mon Apr 01 09:57:16 2024 -0400 +++ b/test/rest_common.py Mon Apr 01 14:42:36 2024 -0400 @@ -316,7 +316,7 @@ except ValueError: pass - def create_sampledata(self): + def create_sampledata(self, data_max=3): """ Create sample data common to some test cases """ self.create_stati() @@ -338,6 +338,34 @@ priority=self.db.priority.lookup('critical') ) + if data_max > 10: + raise ValueError('data_max must be less than 10') + + if data_max == 3: + return + + sample_data = [ + ["foo6", "normal", "closed"], + ["foo7", "critical", "open"], + ["foo8", "normal", "open"], + ["foo9", "critical", "open"], + ["foo10", "normal", "closed"], + ["foo11", "critical", "open"], + ["foo12", "normal", "closed"], + ["foo13", "normal", "open"], + + ] + + for title, priority, status in sample_data: + new_issue = self.db.issue.create( + title=title, + status=self.db.status.lookup(status), + priority=self.db.priority.lookup(priority) + ) + + if int(new_issue) == data_max: + break + def test_no_next_link_on_full_last_page(self): """Make sure that there is no next link on the last page where the total number of entries @@ -971,6 +999,47 @@ results = self.server.get_collection('issue', form) self.assertDictEqual(expected, results) + def testGrouping(self): + self.maxDiff = 4000 + self.create_sampledata(data_max=5) + self.db.issue.set('1', status='7', priority='4') + self.db.issue.set('2', status='2', priority='4') + self.db.issue.set('3', status='2', priority='4') + self.db.issue.set('4', status='2', priority='2') + self.db.issue.set('5', status='2', priority='2') + self.db.commit() + base_path = self.db.config['TRACKER_WEB'] + 'rest/data/issue/' + # change some data for sorting on later + form = cgi.FieldStorage() + form.list = [ + cgi.MiniFieldStorage('@fields', 'status,priority'), + cgi.MiniFieldStorage('@sort', '-id'), + cgi.MiniFieldStorage('@group', '-status,priority'), + cgi.MiniFieldStorage('@verbose', '0') + ] + + # status is sorted by orderprop (property 'order') + expected={'data': { + '@total_size': 5, + 'collection': [ + {'link': base_path + '1', 'priority': '4', + 'status': '7', 'id': '1'}, + {'link': base_path + '5', 'priority': '2', + 'status': '2', 'id': '5'}, + {'link': base_path + '4', 'priority': '2', + 'status': '2', 'id': '4'}, + {'link': base_path + '3', 'priority': '4', + 'status': '2', 'id': '3'}, + {'link': base_path + '2', 'priority': '4', + 'status': '2', 'id': '2'}, + ] + }} + + + results = self.server.get_collection('issue', form) + print(results) + self.assertDictEqual(expected, results) + def testTransitiveField(self): """ Test a transitive property in @fields """ base_path = self.db.config['TRACKER_WEB'] + 'rest/data/'
