mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-11 03:52:59 +00:00
Merge pull request #3177 from minrk/user-state-filter
add ?state= filter for GET /users
This commit is contained in:
@@ -79,6 +79,21 @@ paths:
|
||||
/users:
|
||||
get:
|
||||
summary: List users
|
||||
parameters:
|
||||
- name: state
|
||||
in: query
|
||||
required: false
|
||||
type: string
|
||||
enum: ["inactive", "active", "ready"]
|
||||
description: |
|
||||
Return only users who have servers in the given state.
|
||||
If unspecified, return all users.
|
||||
|
||||
active: all users with any active servers (ready OR pending)
|
||||
ready: all users who have any ready servers (running, not pending)
|
||||
inactive: all users who have *no* active servers (complement of active)
|
||||
|
||||
Added in JupyterHub 1.3
|
||||
responses:
|
||||
'200':
|
||||
description: The Hub's user list
|
||||
|
@@ -9,6 +9,7 @@ from datetime import timezone
|
||||
|
||||
from async_generator import aclosing
|
||||
from dateutil.parser import parse as parse_date
|
||||
from sqlalchemy import func
|
||||
from tornado import web
|
||||
from tornado.iostream import StreamClosedError
|
||||
|
||||
@@ -39,11 +40,61 @@ class SelfAPIHandler(APIHandler):
|
||||
|
||||
|
||||
class UserListAPIHandler(APIHandler):
|
||||
def _user_has_ready_spawner(self, orm_user):
|
||||
"""Return True if a user has *any* ready spawners
|
||||
|
||||
Used for filtering from active -> ready
|
||||
"""
|
||||
user = self.users[orm_user]
|
||||
return any(spawner.ready for spawner in user.spawners.values())
|
||||
|
||||
@admin_only
|
||||
def get(self):
|
||||
state_filter = self.get_argument("state", None)
|
||||
|
||||
# post_filter
|
||||
post_filter = None
|
||||
|
||||
if state_filter in {"active", "ready"}:
|
||||
# only get users with active servers
|
||||
# an 'active' Spawner has a server record in the database
|
||||
# which means Spawner.server != None
|
||||
# it may still be in a pending start/stop state.
|
||||
# join filters out users with no Spawners
|
||||
query = (
|
||||
self.db.query(orm.User)
|
||||
# join filters out any Users with no Spawners
|
||||
.join(orm.Spawner)
|
||||
# this implicitly gets Users with *any* active server
|
||||
.filter(orm.Spawner.server != None)
|
||||
)
|
||||
if state_filter == "ready":
|
||||
# have to post-process query results because active vs ready
|
||||
# can only be distinguished with in-memory Spawner properties
|
||||
post_filter = self._user_has_ready_spawner
|
||||
|
||||
elif state_filter == "inactive":
|
||||
# only get users with *no* active servers
|
||||
# as opposed to users with *any inactive servers*
|
||||
# this is the complement to the above query.
|
||||
# how expensive is this with lots of servers?
|
||||
query = (
|
||||
self.db.query(orm.User)
|
||||
.outerjoin(orm.Spawner)
|
||||
.outerjoin(orm.Server)
|
||||
.group_by(orm.User.id)
|
||||
.having(func.count(orm.Server.id) == 0)
|
||||
)
|
||||
elif state_filter:
|
||||
raise web.HTTPError(400, "Unrecognized state filter: %r" % state_filter)
|
||||
else:
|
||||
# no filter, return all users
|
||||
query = self.db.query(orm.User)
|
||||
|
||||
data = [
|
||||
self.user_model(u, include_servers=True, include_state=True)
|
||||
for u in self.db.query(orm.User)
|
||||
for u in query
|
||||
if (post_filter is None or post_filter(u))
|
||||
]
|
||||
self.write(json.dumps(data))
|
||||
|
||||
|
@@ -14,6 +14,7 @@ from pytest import mark
|
||||
|
||||
import jupyterhub
|
||||
from .. import orm
|
||||
from ..objects import Server
|
||||
from ..utils import url_path_join as ujoin
|
||||
from ..utils import utcnow
|
||||
from .mocking import public_host
|
||||
@@ -179,6 +180,70 @@ async def test_get_users(app):
|
||||
assert r.status_code == 403
|
||||
|
||||
|
||||
@mark.user
|
||||
@mark.parametrize(
|
||||
"state", ("inactive", "active", "ready", "invalid"),
|
||||
)
|
||||
async def test_get_users_state_filter(app, state):
|
||||
db = app.db
|
||||
|
||||
# has_one_active: one active, one inactive, zero ready
|
||||
has_one_active = add_user(db, app=app, name='has_one_active')
|
||||
# has_two_active: two active, ready servers
|
||||
has_two_active = add_user(db, app=app, name='has_two_active')
|
||||
# has_two_inactive: two spawners, neither active
|
||||
has_two_inactive = add_user(db, app=app, name='has_two_inactive')
|
||||
# has_zero: no Spawners registered at all
|
||||
has_zero = add_user(db, app=app, name='has_zero')
|
||||
|
||||
test_usernames = set(
|
||||
("has_one_active", "has_two_active", "has_two_inactive", "has_zero")
|
||||
)
|
||||
|
||||
user_states = {
|
||||
"inactive": ["has_two_inactive", "has_zero"],
|
||||
"ready": ["has_two_active"],
|
||||
"active": ["has_one_active", "has_two_active"],
|
||||
"invalid": [],
|
||||
}
|
||||
expected = user_states[state]
|
||||
|
||||
def add_spawner(user, name='', active=True, ready=True):
|
||||
"""Add a spawner in a requested state
|
||||
|
||||
If active, should turn up in an active query
|
||||
If active and ready, should turn up in a ready query
|
||||
If not active, should turn up in an inactive query
|
||||
"""
|
||||
spawner = user.spawners[name]
|
||||
db.commit()
|
||||
if active:
|
||||
orm_server = orm.Server()
|
||||
db.add(orm_server)
|
||||
db.commit()
|
||||
spawner.server = Server(orm_server=orm_server)
|
||||
db.commit()
|
||||
if not ready:
|
||||
spawner._spawn_pending = True
|
||||
return spawner
|
||||
|
||||
for name in ("", "secondary"):
|
||||
add_spawner(has_two_active, name, active=True)
|
||||
add_spawner(has_two_inactive, name, active=False)
|
||||
|
||||
add_spawner(has_one_active, active=True, ready=False)
|
||||
add_spawner(has_one_active, "inactive", active=False)
|
||||
|
||||
r = await api_request(app, 'users?state={}'.format(state))
|
||||
if state == "invalid":
|
||||
assert r.status_code == 400
|
||||
return
|
||||
assert r.status_code == 200
|
||||
|
||||
usernames = sorted(u["name"] for u in r.json() if u["name"] in test_usernames)
|
||||
assert usernames == expected
|
||||
|
||||
|
||||
@mark.user
|
||||
async def test_get_self(app):
|
||||
db = app.db
|
||||
|
Reference in New Issue
Block a user