Repository: jupyterhub/jupyterhub

Issue:
make pagination configurable
add some unittests for pagination

reorganize pagination a bit to make it easier to configure

still need to do a bit more testing

closes #3228


Discussion:
Awesome, @minrk!!!!
@minrk I added the enhancement label because it make github-activity automatically categorize the PR for the changelog.
Hmm, this seems to not work. 

I've the following in my extra_config:

```yaml
      07-no-pagination: |
        c.Pagination.default_per_page = 10000
        c.Pagination.max_per_page = 10000
```

But pagination still seems to be at 250.

Resolving patch:
diff --git a/jupyterhub/handlers/pages.py b/jupyterhub/handlers/pages.py
--- a/jupyterhub/handlers/pages.py
+++ b/jupyterhub/handlers/pages.py
@@ -457,7 +457,8 @@ class AdminHandler(BaseHandler):
     @web.authenticated
     @admin_only
     async def get(self):
-        page, per_page, offset = Pagination(config=self.config).get_page_args(self)
+        pagination = Pagination(url=self.request.uri, config=self.config)
+        page, per_page, offset = pagination.get_page_args(self)
 
         available = {'name', 'admin', 'running', 'last_activity'}
         default_sort = ['admin', 'name']
@@ -513,14 +514,7 @@ async def get(self):
         for u in users:
             running.extend(s for s in u.spawners.values() if s.active)
 
-        total = self.db.query(orm.User.id).count()
-        pagination = Pagination(
-            url=self.request.uri,
-            total=total,
-            page=page,
-            per_page=per_page,
-            config=self.config,
-        )
+        pagination.total = self.db.query(orm.User.id).count()
 
         auth_state = await self.current_user.get_auth_state()
         html = await self.render_template(
diff --git a/jupyterhub/pagination.py b/jupyterhub/pagination.py
--- a/jupyterhub/pagination.py
+++ b/jupyterhub/pagination.py
@@ -1,7 +1,6 @@
 """Basic class to manage pagination utils."""
 # Copyright (c) Jupyter Development Team.
 # Distributed under the terms of the Modified BSD License.
-from traitlets import Bool
 from traitlets import default
 from traitlets import Integer
 from traitlets import observe
@@ -81,13 +80,13 @@ def get_page_args(self, handler):
         try:
             self.per_page = int(per_page)
         except Exception:
-            self.per_page = self._default_per_page
+            self.per_page = self.default_per_page
 
         try:
             self.page = int(page)
             if self.page < 1:
                 self.page = 1
-        except:
+        except Exception:
             self.page = 1
 
         return self.page, self.per_page, self.per_page * (self.page - 1)


Test patch:
diff --git a/jupyterhub/tests/test_pagination.py b/jupyterhub/tests/test_pagination.py
--- a/jupyterhub/tests/test_pagination.py
+++ b/jupyterhub/tests/test_pagination.py
@@ -6,11 +6,20 @@
 from jupyterhub.pagination import Pagination
 
 
-def test_per_page_bounds():
+@mark.parametrize(
+    "per_page, max_per_page, expected",
+    [
+        (20, 10, 10),
+        (1000, 1000, 1000),
+    ],
+)
+def test_per_page_bounds(per_page, max_per_page, expected):
     cfg = Config()
-    cfg.Pagination.max_per_page = 10
-    p = Pagination(config=cfg, per_page=20, total=100)
-    assert p.per_page == 10
+    cfg.Pagination.max_per_page = max_per_page
+    p = Pagination(config=cfg)
+    p.per_page = per_page
+    p.total = 99999
+    assert p.per_page == expected
     with raises(Exception):
         p.per_page = 0
 
@@ -39,7 +48,5 @@ def test_per_page_bounds():
     ],
 )
 def test_window(page, per_page, total, expected):
-    cfg = Config()
-    cfg.Pagination
     pagination = Pagination(page=page, per_page=per_page, total=total)
     assert pagination.calculate_pages_window() == expected