diff --git a/app/api/utils.py b/app/api/utils.py index b9e69644f3a1fa06d7a063868e9b2a20de14f376..9a2269c2a797cb5edefe31ee5231a1cee73c13ee 100644 --- a/app/api/utils.py +++ b/app/api/utils.py @@ -79,7 +79,9 @@ def get_generic_model(model, order_by=None, query=None): if order_by is None: order_by = getattr(model, "name") query = query.order_by(order_by) - pagination = query.paginate(page, per_page) + pagination = query.paginate( + page, per_page, max_per_page=current_app.config["MAX_PER_PAGE"] + ) data = [item.to_dict(recursive=recursive) for item in pagination.items] # Re-add recursive to kwargs so that it's part of the pagination url kwargs["recursive"] = recursive @@ -96,6 +98,7 @@ def search_generic_model(model): kwargs = request.args.to_dict() page = int(kwargs.pop("page", 1)) per_page = int(kwargs.pop("per_page", 20)) + per_page = min(per_page, current_app.config["MAX_PER_PAGE"]) search = kwargs.get("q", "*") instances, nb_filtered = model.search(search, page=page, per_page=per_page) current_app.logger.debug( diff --git a/app/settings.py b/app/settings.py index 37e804c5518743829aef79dd5a39e7f9b9fab86b..5c1ab337d3bd5af79723e5e3f4c40063e40d6af9 100644 --- a/app/settings.py +++ b/app/settings.py @@ -89,6 +89,9 @@ DOCUMENTATION_URL = "http://ics-infrastructure.pages.esss.lu.se/csentry/index.ht # Shall be set to staging|production|development CSENTRY_ENVIRONMENT = "staging" +# Maximum number of elements returned per page by an API call +MAX_PER_PAGE = 100 + AWX_URL = "https://torn.tn.esss.lu.se" # AWX dynamic inventory source to update # Use the id because resource.update requires a number diff --git a/docs/api.rst b/docs/api.rst index 0b0a7bc88a5b475e84b09a170dd4b97579b5f5c0..300d0829ecbfcf9b686e17049c75ba576f3331f5 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -51,7 +51,8 @@ Pagination ---------- When returning an array, the result is limited to 20 elements per page by default. -You can pass the `page` (default to 1) and `per_page` (default to 20) parameters to access other elements. +You can pass the `page` (default: 1) and `per_page` (default:20, max: 100) parameters to access other elements. +Note that you can't request more than 100 elements per page. You should follow the link headers to get more results. To list the 30 elements of the second page:: diff --git a/tests/functional/conftest.py b/tests/functional/conftest.py index 7d7cc0006b3e49e0fcf7952b8cb68bee4b8150b0..9976b9913d1634c82745a6139792fa6307c98d81 100644 --- a/tests/functional/conftest.py +++ b/tests/functional/conftest.py @@ -60,6 +60,7 @@ def app(request): }, "AWX_URL": "https://awx.example.org", "ALLOWED_VM_CREATION_DOMAINS": ["lab.example.org"], + "MAX_PER_PAGE": 25, } app = create_app(config=config) ctx = app.app_context() diff --git a/tests/functional/test_api.py b/tests/functional/test_api.py index 23ca0ed300b9c4cdaa75375e0a41422f67483291..2b0da7190ff36a2dda0c04394f898db1fab590a7 100644 --- a/tests/functional/test_api.py +++ b/tests/functional/test_api.py @@ -1700,3 +1700,70 @@ def test_search_hosts(client, host_factory, readonly_token): assert HOST_KEYS == set(r[0].keys()) assert r[0]["name"] == host1.name assert r[0]["description"] == host1.description + + +@pytest.mark.parametrize("endpoint", ["network/hosts", "network/hosts/search"]) +def test_pagination(endpoint, client, host_factory, readonly_token): + # MAX_PER_PAGE set to 25 for testing + # Create 30 hosts + for i in range(30): + host_factory() + if endpoint == "network/hosts": + extra_args = "&recursive=False" + else: + extra_args = "" + # By default 20 hosts per page shall be returned + response = get(client, f"{API_URL}/{endpoint}", token=readonly_token) + assert response.status_code == 200 + assert len(response.get_json()) == 20 + assert response.headers["x-total-count"] == "30" + assert ( + f'{API_URL}/{endpoint}?per_page=20&page=2{extra_args}>; rel="next",' + in response.headers["link"] + ) + assert 'rel="prev"' not in response.headers["link"] + assert 'rel="first"' not in response.headers["link"] + # Get second page (which is last) + response = get( + client, + f"{API_URL}/{endpoint}?per_page=20&page=2{extra_args}", + token=readonly_token, + ) + assert response.status_code == 200 + assert len(response.get_json()) == 10 + assert ( + f'{API_URL}/{endpoint}?per_page=20&page=1{extra_args}>; rel="first",' + in response.headers["link"] + ) + assert ( + f'{API_URL}/{endpoint}?per_page=20&page=1{extra_args}>; rel="prev"' + in response.headers["link"] + ) + assert 'rel="next"' not in response.headers["link"] + assert 'rel="last"' not in response.headers["link"] + # Request 10 elements per_page + response = get(client, f"{API_URL}/{endpoint}?per_page=10", token=readonly_token) + assert response.status_code == 200 + assert len(response.get_json()) == 10 + assert response.headers["x-total-count"] == "30" + assert ( + f'{API_URL}/{endpoint}?per_page=10&page=2{extra_args}>; rel="next",' + in response.headers["link"] + ) + assert ( + f'{API_URL}/{endpoint}?per_page=10&page=3{extra_args}>; rel="last"' + in response.headers["link"] + ) + # You can't request more than MAX_PER_PAGE elements + response = get(client, f"{API_URL}/{endpoint}?per_page=50", token=readonly_token) + assert response.status_code == 200 + assert len(response.get_json()) == 25 + assert response.headers["x-total-count"] == "30" + assert ( + f'{API_URL}/{endpoint}?per_page=25&page=2{extra_args}>; rel="next",' + in response.headers["link"] + ) + assert ( + f'{API_URL}/{endpoint}?per_page=25&page=2{extra_args}>; rel="last"' + in response.headers["link"] + )