diff --git a/jupyterhub/apihandlers/base.py b/jupyterhub/apihandlers/base.py index d59cfb12..e33f572f 100644 --- a/jupyterhub/apihandlers/base.py +++ b/jupyterhub/apihandlers/base.py @@ -265,6 +265,22 @@ class APIHandler(BaseHandler): 400, ("group names must be str, not %r", type(groupname)) ) + def get_api_pagination(self): + default_limit = self.settings["app"].api_page_default_limit + max_limit = self.settings["app"].api_page_max_limit + offset = self.get_argument("offset", None) + limit = self.get_argument("limit", default_limit) + try: + offset = abs(int(offset)) if offset is not None else 0 + limit = abs(int(limit)) + if limit > max_limit: + limit = max_limit + except Exception as e: + raise web.HTTPError( + 400, "Invalid argument type, offset and limit must be integers" + ) + return offset, limit + def options(self, *args, **kwargs): self.finish() diff --git a/jupyterhub/apihandlers/groups.py b/jupyterhub/apihandlers/groups.py index 9cbaa997..ec770800 100644 --- a/jupyterhub/apihandlers/groups.py +++ b/jupyterhub/apihandlers/groups.py @@ -38,34 +38,9 @@ class GroupListAPIHandler(_GroupAPIHandler): @admin_only def get(self): """List groups""" - offset = self.get_argument("offset", None) - limit = self.get_argument("limit", self.settings['app'].api_page_default_limit) - max_limit = self.settings['app'].api_page_max_limit - query = self.db.query(orm.Group) - - if offset is not None: - try: - offset = int(offset) - except Exception as e: - raise web.HTTPError( - 400, "Invalid argument type, offset must be an integer" - ) - query = query.offset(offset) - - if limit != self.settings['app'].api_page_default_limit: - try: - limit = int(limit) - if limit > max_limit: - limit = max_limit - - except Exception as e: - raise web.HTTPError( - 400, "Invalid argument type, limit must be an integer" - ) - - query = query.limit(limit) - + offset, limit = self.get_api_pagination() + query = query.offset(offset).limit(limit) data = [self.group_model(group) for group in query] self.write(json.dumps(data)) @@ -104,36 +79,8 @@ class GroupAPIHandler(_GroupAPIHandler): @admin_only def get(self, name): - offset = self.get_argument("offset", None) - limit = self.get_argument("limit", self.settings['app'].api_page_default_limit) - max_limit = self.settings['app'].api_page_max_limit - group = self.find_group(name) - - if offset is not None: - try: - offset = int(offset) - except Exception as e: - raise web.HTTPError( - 400, "Invalid argument type, offset must be an integer" - ) - - group.users = group.users[offset:] - - if limit != self.settings['app'].api_page_default_limit: - try: - limit = int(limit) - if limit > max_limit: - limit = max_limit - except Exception as e: - raise web.HTTPError( - 400, "Invalid argument type, limit must be an integer" - ) - - group.users = group.users[:limit] - group_model = self.group_model(group) - self.write(json.dumps(group_model)) @admin_only diff --git a/jupyterhub/apihandlers/proxy.py b/jupyterhub/apihandlers/proxy.py index 8bbbafb3..2a1b7a4e 100644 --- a/jupyterhub/apihandlers/proxy.py +++ b/jupyterhub/apihandlers/proxy.py @@ -17,36 +17,14 @@ class ProxyAPIHandler(APIHandler): This is the same as fetching the routing table directly from the proxy, but without clients needing to maintain separate """ - offset = self.get_argument("offset", None) - limit = self.get_argument("limit", self.settings['app'].api_page_default_limit) - max_limit = self.settings['app'].api_page_max_limit + offset, limit = self.get_api_pagination() routes = await self.proxy.get_all_routes() - if offset is not None: - try: - offset = int(offset) - except Exception as e: - raise web.HTTPError( - 400, "Invalid argument type, offset must be an integer" - ) - routes = { - key: routes[key] - for key in list(routes.keys())[offset:] - if key in routes - } - - if limit != self.settings['app'].api_page_default_limit: - try: - limit = int(limit) - if limit > max_limit: - limit = max_limit - except Exception as e: - raise web.HTTPError( - 400, "Invalid argument type, limit must be an integer" - ) routes = { - key: routes[key] for key in list(routes.keys())[:limit] if key in routes + key: routes[key] + for key in list(routes.keys())[offset:limit] + if key in routes } self.write(json.dumps(routes)) diff --git a/jupyterhub/apihandlers/users.py b/jupyterhub/apihandlers/users.py index 2c01bf70..f5ff0561 100644 --- a/jupyterhub/apihandlers/users.py +++ b/jupyterhub/apihandlers/users.py @@ -55,9 +55,7 @@ class UserListAPIHandler(APIHandler): @admin_only def get(self): state_filter = self.get_argument("state", None) - offset = self.get_argument("offset", None) - limit = self.get_argument("limit", self.settings['app'].api_page_default_limit) - max_limit = self.settings['app'].api_page_max_limit + offset, limit = self.get_api_pagination() # post_filter post_filter = None @@ -98,27 +96,7 @@ class UserListAPIHandler(APIHandler): # no filter, return all users query = self.db.query(orm.User) - # Apply offset and limit for request pagination - if offset is not None: - try: - offset = int(offset) - except Exception as e: - raise web.HTTPError( - 400, "Invalid argument type, offset must be an integer" - ) - query = query.offset(offset) - - if limit != self.settings['app'].api_page_default_limit: - try: - limit = int(limit) - if limit > max_limit: - limit = max_limit - except Exception as e: - raise web.HTTPError( - 400, "Invalid argument type, limit must be an integer" - ) - - query = query.limit(limit) + query = query.offset(offset).limit(limit) data = [ self.user_model(u, include_servers=True, include_state=True) diff --git a/jupyterhub/tests/test_api.py b/jupyterhub/tests/test_api.py index 61950217..c8add86e 100644 --- a/jupyterhub/tests/test_api.py +++ b/jupyterhub/tests/test_api.py @@ -1504,36 +1504,6 @@ async def test_group_get(app): } -@mark.group -async def test_group_user_pagination(app): - group = orm.Group.find(app.db, name='betaflight') - user = add_user(app.db, app=app, name='bigfoot') - second_user = add_user(app.db, app=app, name='birdman') - group.users.append(user) - group.users.append(second_user) - app.db.commit() - - # Test offset for pagination - r = await api_request(app, 'groups/betaflight?offset=1') - r.raise_for_status() - reply = r.json() - assert reply == {'kind': 'group', 'name': 'betaflight', 'users': ['birdman']} - - r = await api_request(app, 'groups/betaflight?offset=10') - r.raise_for_status() - assert r.json() == {'kind': 'group', 'name': 'betaflight', 'users': []} - - # Test limit for pagination - r = await api_request(app, 'groups/betaflight?limit=1') - r.raise_for_status() - reply = r.json() - assert reply == {'kind': 'group', 'name': 'betaflight', 'users': ['bigfoot']} - - r = await api_request(app, 'groups/betaflight?limit=0') - r.raise_for_status() - assert r.json() == {'kind': 'group', 'name': 'betaflight', 'users': []} - - @mark.group async def test_group_create_delete(app): db = app.db