mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-18 15:33:02 +00:00
Merge pull request #3013 from twalcari/feature/spawn_query_arguments
This commit is contained in:
@@ -175,11 +175,41 @@ class SpawnHandler(BaseHandler):
|
|||||||
auth_state = await user.get_auth_state()
|
auth_state = await user.get_auth_state()
|
||||||
await spawner.run_auth_state_hook(auth_state)
|
await spawner.run_auth_state_hook(auth_state)
|
||||||
|
|
||||||
|
# Try to start server directly when query arguments are passed.
|
||||||
|
error_message = ''
|
||||||
|
query_options = {}
|
||||||
|
for key, byte_list in self.request.query_arguments.items():
|
||||||
|
query_options[key] = [bs.decode('utf8') for bs in byte_list]
|
||||||
|
|
||||||
|
# 'next' is reserved argument for redirect after spawn
|
||||||
|
query_options.pop('next', None)
|
||||||
|
|
||||||
|
if len(query_options) > 0:
|
||||||
|
try:
|
||||||
|
self.log.debug(
|
||||||
|
"Triggering spawn with supplied query arguments for %s",
|
||||||
|
spawner._log_name,
|
||||||
|
)
|
||||||
|
options = await maybe_future(spawner.options_from_query(query_options))
|
||||||
|
pending_url = self._get_pending_url(user, server_name)
|
||||||
|
return await self._wrap_spawn_single_user(
|
||||||
|
user, server_name, spawner, pending_url, options
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
self.log.error(
|
||||||
|
"Failed to spawn single-user server with query arguments",
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
error_message = str(e)
|
||||||
|
# fallback to behavior without failing query arguments
|
||||||
|
|
||||||
spawner_options_form = await spawner.get_options_form()
|
spawner_options_form = await spawner.get_options_form()
|
||||||
if spawner_options_form:
|
if spawner_options_form:
|
||||||
self.log.debug("Serving options form for %s", spawner._log_name)
|
self.log.debug("Serving options form for %s", spawner._log_name)
|
||||||
form = await self._render_form(
|
form = await self._render_form(
|
||||||
for_user=user, spawner_options_form=spawner_options_form
|
for_user=user,
|
||||||
|
spawner_options_form=spawner_options_form,
|
||||||
|
message=error_message,
|
||||||
)
|
)
|
||||||
self.finish(form)
|
self.finish(form)
|
||||||
else:
|
else:
|
||||||
|
@@ -384,6 +384,37 @@ class Spawner(LoggingConfigurable):
|
|||||||
"""
|
"""
|
||||||
return form_data
|
return form_data
|
||||||
|
|
||||||
|
def options_from_query(self, query_data):
|
||||||
|
"""Interpret query arguments passed to /spawn
|
||||||
|
|
||||||
|
Query arguments will always arrive as a dict of unicode strings.
|
||||||
|
Override this function to understand single-values, numbers, etc.
|
||||||
|
|
||||||
|
By default, options_from_form is called from this function. You can however override
|
||||||
|
this function if you need to process the query arguments differently.
|
||||||
|
|
||||||
|
This should coerce form data into the structure expected by self.user_options,
|
||||||
|
which must be a dict, and should be JSON-serializeable,
|
||||||
|
though it can contain bytes in addition to standard JSON data types.
|
||||||
|
|
||||||
|
This method should not have any side effects.
|
||||||
|
Any handling of `user_options` should be done in `.start()`
|
||||||
|
to ensure consistent behavior across servers
|
||||||
|
spawned via the API and form submission page.
|
||||||
|
|
||||||
|
Instances will receive this data on self.user_options, after passing through this function,
|
||||||
|
prior to `Spawner.start`.
|
||||||
|
|
||||||
|
.. versionadded:: 1.2
|
||||||
|
user_options are persisted in the JupyterHub database to be reused
|
||||||
|
on subsequent spawns if no options are given.
|
||||||
|
user_options is serialized to JSON as part of this persistence
|
||||||
|
(with additional support for bytes in case of uploaded file data),
|
||||||
|
and any non-bytes non-jsonable values will be replaced with None
|
||||||
|
if the user_options are re-used.
|
||||||
|
"""
|
||||||
|
return self.options_from_form(query_data)
|
||||||
|
|
||||||
user_options = Dict(
|
user_options = Dict(
|
||||||
help="""
|
help="""
|
||||||
Dict of user specified options for the user's spawned instance of a single-user server.
|
Dict of user specified options for the user's spawned instance of a single-user server.
|
||||||
|
@@ -173,6 +173,9 @@ class FormSpawner(MockSpawner):
|
|||||||
options['energy'] = form_data['energy'][0]
|
options['energy'] = form_data['energy'][0]
|
||||||
if 'hello_file' in form_data:
|
if 'hello_file' in form_data:
|
||||||
options['hello'] = form_data['hello_file'][0]
|
options['hello'] = form_data['hello_file'][0]
|
||||||
|
|
||||||
|
if 'illegal_argument' in form_data:
|
||||||
|
raise ValueError("You are not allowed to specify 'illegal_argument'")
|
||||||
return options
|
return options
|
||||||
|
|
||||||
|
|
||||||
|
@@ -255,6 +255,47 @@ async def test_spawn_page_admin(app, admin_access):
|
|||||||
assert "Spawning server for {}".format(u.name) in r.text
|
assert "Spawning server for {}".format(u.name) in r.text
|
||||||
|
|
||||||
|
|
||||||
|
async def test_spawn_with_query_arguments(app):
|
||||||
|
with mock.patch.dict(app.users.settings, {'spawner_class': FormSpawner}):
|
||||||
|
base_url = ujoin(public_host(app), app.hub.base_url)
|
||||||
|
cookies = await app.login_user('jones')
|
||||||
|
orm_u = orm.User.find(app.db, 'jones')
|
||||||
|
u = app.users[orm_u]
|
||||||
|
await u.stop()
|
||||||
|
next_url = ujoin(app.base_url, 'user/jones/tree')
|
||||||
|
r = await async_requests.get(
|
||||||
|
url_concat(
|
||||||
|
ujoin(base_url, 'spawn'), {'next': next_url, 'energy': '510keV'},
|
||||||
|
),
|
||||||
|
cookies=cookies,
|
||||||
|
)
|
||||||
|
r.raise_for_status()
|
||||||
|
assert r.history
|
||||||
|
assert u.spawner.user_options == {
|
||||||
|
'energy': '510keV',
|
||||||
|
'notspecified': 5,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_spawn_with_query_arguments_error(app):
|
||||||
|
with mock.patch.dict(app.users.settings, {'spawner_class': FormSpawner}):
|
||||||
|
base_url = ujoin(public_host(app), app.hub.base_url)
|
||||||
|
cookies = await app.login_user('jones')
|
||||||
|
orm_u = orm.User.find(app.db, 'jones')
|
||||||
|
u = app.users[orm_u]
|
||||||
|
await u.stop()
|
||||||
|
next_url = ujoin(app.base_url, 'user/jones/tree')
|
||||||
|
r = await async_requests.get(
|
||||||
|
url_concat(
|
||||||
|
ujoin(base_url, 'spawn'),
|
||||||
|
{'next': next_url, 'energy': '510keV', 'illegal_argument': '42'},
|
||||||
|
),
|
||||||
|
cookies=cookies,
|
||||||
|
)
|
||||||
|
r.raise_for_status()
|
||||||
|
assert "You are not allowed to specify " in r.text
|
||||||
|
|
||||||
|
|
||||||
async def test_spawn_form(app):
|
async def test_spawn_form(app):
|
||||||
with mock.patch.dict(app.users.settings, {'spawner_class': FormSpawner}):
|
with mock.patch.dict(app.users.settings, {'spawner_class': FormSpawner}):
|
||||||
base_url = ujoin(public_host(app), app.hub.base_url)
|
base_url = ujoin(public_host(app), app.hub.base_url)
|
||||||
|
Reference in New Issue
Block a user