Mercurial > p > roundup > code
comparison roundup/rest.py @ 5639:f576957cbb1f
Add support for prev/next/self links when returning paginated results.
To do this:
1) change "data" envelope from an array to a dict
2) move the "data" array to the "collection" property,
which is an array of elements in the collection.
3) add @links dict keyed by link relation: self, next, prev.
Each relation is an array of dicts with uri and rel keys.
In this case there is only one element, but there is nothing
preventing a relation from having multiple url's. So this follows
the formatting needed for the general case.
Relations are present only if it makes sense. So first page has no
prev and last page has no next.
4) add @total_size with number of element selected if they were
not paginated. Replicates data in X-Count-Total header.
Changed index to start at 1. So the first page is page_index 1 and not
page_index 0. (So I am no longer surprised when I set page_index to 1
and am missing a bunch of records 8-)).
Also a small fixup, json response ends with a newline so printing
the data, or using curl makes sure that anything printing after the
json output (like shell prompts) is on a new line.
Tests added for all cases.
| author | John Rouillard <rouilj@ieee.org> |
|---|---|
| date | Sat, 09 Mar 2019 11:06:10 -0500 |
| parents | 7e3cceec3f4f |
| children | a60cbbcc9309 |
comparison
equal
deleted
inserted
replaced
| 5638:7e3cceec3f4f | 5639:f576957cbb1f |
|---|---|
| 482 | 482 |
| 483 # Handle filtering and pagination | 483 # Handle filtering and pagination |
| 484 filter_props = {} | 484 filter_props = {} |
| 485 page = { | 485 page = { |
| 486 'size': None, | 486 'size': None, |
| 487 'index': None | 487 'index': 1 # setting just size starts at page 1 |
| 488 } | 488 } |
| 489 for form_field in input.value: | 489 for form_field in input.value: |
| 490 key = form_field.name | 490 key = form_field.name |
| 491 value = form_field.value | 491 value = form_field.value |
| 492 if key.startswith("where_"): # serve the filter purpose | 492 if key.startswith("where_"): # serve the filter purpose |
| 504 obj_list = class_obj.list() | 504 obj_list = class_obj.list() |
| 505 else: | 505 else: |
| 506 obj_list = class_obj.filter(None, filter_props) | 506 obj_list = class_obj.filter(None, filter_props) |
| 507 | 507 |
| 508 # extract result from data | 508 # extract result from data |
| 509 result = [ | 509 result={} |
| 510 result['collection'] = [ | |
| 510 {'id': item_id, 'link': class_path + item_id} | 511 {'id': item_id, 'link': class_path + item_id} |
| 511 for item_id in obj_list | 512 for item_id in obj_list |
| 512 if self.db.security.hasPermission( | 513 if self.db.security.hasPermission( |
| 513 'View', self.db.getuid(), class_name, itemid=item_id | 514 'View', self.db.getuid(), class_name, itemid=item_id |
| 514 ) | 515 ) |
| 515 ] | 516 ] |
| 516 | 517 result_len = len(result['collection']) |
| 517 # pagination | 518 |
| 518 if page['size'] is not None and page['index'] is not None: | 519 # pagination - page_index from 1...N |
| 519 page_start = max(page['index'] * page['size'], 0) | 520 if page['size'] is not None: |
| 520 page_end = min(page_start + page['size'], len(result)) | 521 page_start = max((page['index']-1) * page['size'], 0) |
| 521 result = result[page_start:page_end] | 522 page_end = min(page_start + page['size'], result_len) |
| 522 | 523 result['collection'] = result['collection'][page_start:page_end] |
| 523 self.client.setHeader("X-Count-Total", str(len(result))) | 524 result['@links'] = {} |
| 525 for rel in ('next', 'prev', 'self'): | |
| 526 if rel == 'next': | |
| 527 # if current index includes all data, continue | |
| 528 if page['index']*page['size'] > result_len: continue | |
| 529 index=page['index']+1 | |
| 530 if rel == 'prev': | |
| 531 if page['index'] <= 1: continue | |
| 532 index=page['index']-1 | |
| 533 if rel == 'self': index=page['index'] | |
| 534 | |
| 535 result['@links'][rel] = [] | |
| 536 result['@links'][rel].append({ | |
| 537 'rel': rel, | |
| 538 'uri': "%s/%s?page_index=%s&"%(self.data_path, | |
| 539 class_name,index) \ | |
| 540 + '&'.join([ "%s=%s"%(field.name,field.value) \ | |
| 541 for field in input.value \ | |
| 542 if field.name != "page_index"]) }) | |
| 543 | |
| 544 result['@total_size'] = result_len | |
| 545 self.client.setHeader("X-Count-Total", str(result_len)) | |
| 524 return 200, result | 546 return 200, result |
| 525 | 547 |
| 526 @Routing.route("/data/<:class_name>/<:item_id>", 'GET') | 548 @Routing.route("/data/<:class_name>/<:item_id>", 'GET') |
| 527 @_data_decorator | 549 @_data_decorator |
| 528 def get_element(self, class_name, item_id, input): | 550 def get_element(self, class_name, item_id, input): |
| 1354 output = dicttoxml(output, root=False) | 1376 output = dicttoxml(output, root=False) |
| 1355 else: | 1377 else: |
| 1356 self.client.response_code = 406 | 1378 self.client.response_code = 406 |
| 1357 output = "Content type is not accepted by client" | 1379 output = "Content type is not accepted by client" |
| 1358 | 1380 |
| 1359 return output | 1381 # Make output json end in a newline to |
| 1382 # separate from following text in logs etc.. | |
| 1383 return output + "\n" | |
| 1360 | 1384 |
| 1361 | 1385 |
| 1362 class RoundupJSONEncoder(json.JSONEncoder): | 1386 class RoundupJSONEncoder(json.JSONEncoder): |
| 1363 """RoundupJSONEncoder overrides the default JSONEncoder to handle all | 1387 """RoundupJSONEncoder overrides the default JSONEncoder to handle all |
| 1364 types of the object without returning any error""" | 1388 types of the object without returning any error""" |
