From a87309e5ddda960149e53c4aaad6c3870bd22045 Mon Sep 17 00:00:00 2001 From: Benjamin Bertrand <benjamin.bertrand@esss.se> Date: Mon, 8 Jan 2018 09:17:35 +0100 Subject: [PATCH] Add pagination to API Pagination information (next, prev, first, last) is included in the Link HTTP header. The total number of entries is provided in the X-Total-Count HTTP header. --- app/api/inventory.py | 13 ++++++----- app/api/network.py | 13 +++++------ app/api/user.py | 2 +- app/api/utils.py | 51 ++++++++++++++++++++++++++++++++++++++------ app/utils.py | 7 +++--- 5 files changed, 60 insertions(+), 26 deletions(-) diff --git a/app/api/inventory.py b/app/api/inventory.py index 2a75c36..91d0b1f 100644 --- a/app/api/inventory.py +++ b/app/api/inventory.py @@ -35,8 +35,7 @@ def get_item_by_id_or_ics_id(id_): @bp.route('/items') @jwt_required def get_items(): - # TODO: add pagination - return get_generic_model(models.Item, request.args, + return get_generic_model(models.Item, order_by=models.Item.created_at) @@ -133,13 +132,13 @@ def create_item_comment(id_): @bp.route('/actions') @jwt_required def get_actions(): - return get_generic_model(models.Action, request.args) + return get_generic_model(models.Action) @bp.route('/manufacturers') @jwt_required def get_manufacturers(): - return get_generic_model(models.Manufacturer, request.args) + return get_generic_model(models.Manufacturer) @bp.route('/manufacturers', methods=['POST']) @@ -152,7 +151,7 @@ def create_manufacturer(): @bp.route('/models') @jwt_required def get_models(): - return get_generic_model(models.Model, request.args) + return get_generic_model(models.Model) @bp.route('/models', methods=['POST']) @@ -165,7 +164,7 @@ def create_model(): @bp.route('/locations') @jwt_required def get_locations(): - return get_generic_model(models.Location, request.args) + return get_generic_model(models.Location) @bp.route('/locations', methods=['POST']) @@ -178,7 +177,7 @@ def create_locations(): @bp.route('/status') @jwt_required def get_status(): - return get_generic_model(models.Status, request.args) + return get_generic_model(models.Status) @bp.route('/status', methods=['POST']) diff --git a/app/api/network.py b/app/api/network.py index ef2c28c..b48e05d 100644 --- a/app/api/network.py +++ b/app/api/network.py @@ -9,7 +9,7 @@ This module implements the network API. :license: BSD 2-Clause, see LICENSE for more details. """ -from flask import Blueprint, request +from flask import Blueprint from flask_jwt_extended import jwt_required from .. import models from ..decorators import jwt_groups_accepted @@ -21,8 +21,7 @@ bp = Blueprint('network_api', __name__) @bp.route('/scopes') @jwt_required def get_scopes(): - # TODO: add pagination - return get_generic_model(models.NetworkScope, request.args, + return get_generic_model(models.NetworkScope, order_by=models.NetworkScope.name) @@ -38,8 +37,7 @@ def create_scope(): @bp.route('/networks') @jwt_required def get_networks(): - # TODO: add pagination - return get_generic_model(models.Network, request.args, + return get_generic_model(models.Network, order_by=models.Network.address) @@ -55,8 +53,7 @@ def create_network(): @bp.route('/interfaces') @jwt_required def get_interfaces(): - # TODO: add pagination - return get_generic_model(models.Interface, request.args, + return get_generic_model(models.Interface, order_by=models.Interface.ip) @@ -71,7 +68,7 @@ def create_interface(): @bp.route('/macs') @jwt_required def get_macs(): - return get_generic_model(models.Mac, request.args, + return get_generic_model(models.Mac, order_by=models.Mac.address) diff --git a/app/api/user.py b/app/api/user.py index 8868d30..ba22298 100644 --- a/app/api/user.py +++ b/app/api/user.py @@ -23,7 +23,7 @@ bp = Blueprint('user_api', __name__) @bp.route('/users') @jwt_required def get_users(): - return get_generic_model(models.User, request.args, + return get_generic_model(models.User, order_by=models.User.username) diff --git a/app/api/utils.py b/app/api/utils.py index 64471fd..168b266 100644 --- a/app/api/utils.py +++ b/app/api/utils.py @@ -9,6 +9,7 @@ This module implements useful functions for the API. :license: BSD 2-Clause, see LICENSE for more details. """ +import urllib.parse import sqlalchemy as sa from flask import current_app, jsonify, request from ..extensions import db @@ -23,20 +24,58 @@ def commit(): raise utils.CSEntryError(str(e), status_code=422) -def get_generic_model(model, args, order_by=None): +def build_pagination_header(pagination, base_url, **kwargs): + """Return the X-Total-Count and Link header information + + :param pagination: flask_sqlalchemy Pagination class instance + :param base_url: request base_url + :param kwargs: extra query string parameters (without page and per_page) + :returns: dict with X-Total-Count and Link keys + """ + header = {'X-Total-Count': pagination.total} + links = [] + if pagination.page > 1: + params = urllib.parse.urlencode({'per_page': pagination.per_page, + 'page': 1, + **kwargs}) + links.append(f'<{base_url}?{params}>; rel="first"') + if pagination.has_prev: + params = urllib.parse.urlencode({'per_page': pagination.per_page, + 'page': pagination.prev_num, + **kwargs}) + links.append(f'<{base_url}?{params}>; rel="prev"') + if pagination.has_next: + params = urllib.parse.urlencode({'per_page': pagination.per_page, + 'page': pagination.next_num, + **kwargs}) + links.append(f'<{base_url}?{params}>; rel="next"') + if pagination.pages > pagination.page: + params = urllib.parse.urlencode({'per_page': pagination.per_page, + 'page': pagination.pages, + **kwargs}) + links.append(f'<{base_url}?{params}>; rel="last"') + if links: + header['Link'] = ', '.join(links) + return header + + +def get_generic_model(model, order_by=None): """Return data from model as json :param model: model class - :param MultiDict args: args from the request :param order_by: column to order the result by :returns: data from model as json """ - query = utils.get_query(model.query, request.args) + kwargs = request.args.to_dict() + page = int(kwargs.pop('page', 1)) + per_page = int(kwargs.pop('per_page', 20)) + query = utils.get_query(model.query, **kwargs) if order_by is None: order_by = getattr(model, 'name') - instances = query.order_by(order_by) - data = [instance.to_dict() for instance in instances] - return jsonify(data) + pagination = query.order_by(order_by).paginate(page, per_page) + data = [item.to_dict() for item in pagination.items] + header = build_pagination_header(pagination, request.base_url, **kwargs) + return jsonify(data), 200, header def create_generic_model(model, mandatory_fields=('name',), **kwargs): diff --git a/app/utils.py b/app/utils.py index dd9fbec..ee68efe 100644 --- a/app/utils.py +++ b/app/utils.py @@ -135,15 +135,14 @@ def get_model_choices(model, allow_none=False, attr='name', query=None): return choices -def get_query(query, args): +def get_query(query, **kwargs): """Retrieve the query from the arguments :param query: sqlalchemy base query - :param MultiDict args: args from a request + :param kwargs: kwargs from a request :returns: query filtered by the arguments """ - if args: - kwargs = args.to_dict() + if kwargs: try: query = query.filter_by(**kwargs) except (sa.exc.InvalidRequestError, AttributeError) as e: -- GitLab