mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-15 22:13:00 +00:00
Add Spawner form
If Spawner.options_form is specified, a form providing input controls is shown to the user prior to launch. Spawners access the result via the `self.user_options` dict. The default spawners offer no form.
This commit is contained in:
@@ -160,7 +160,8 @@ class UserServerAPIHandler(APIHandler):
|
|||||||
if state is None:
|
if state is None:
|
||||||
raise web.HTTPError(400, "%s's server is already running" % name)
|
raise web.HTTPError(400, "%s's server is already running" % name)
|
||||||
|
|
||||||
yield self.spawn_single_user(user)
|
options = self.get_json_body()
|
||||||
|
yield self.spawn_single_user(user, options=options)
|
||||||
status = 202 if user.spawn_pending else 201
|
status = 202 if user.spawn_pending else 201
|
||||||
self.set_status(status)
|
self.set_status(status)
|
||||||
|
|
||||||
|
@@ -268,9 +268,9 @@ class BaseHandler(RequestHandler):
|
|||||||
@property
|
@property
|
||||||
def spawner_class(self):
|
def spawner_class(self):
|
||||||
return self.settings.get('spawner_class', LocalProcessSpawner)
|
return self.settings.get('spawner_class', LocalProcessSpawner)
|
||||||
|
|
||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
def spawn_single_user(self, user):
|
def spawn_single_user(self, user, options=None):
|
||||||
if user.spawn_pending:
|
if user.spawn_pending:
|
||||||
raise RuntimeError("Spawn already pending for: %s" % user.name)
|
raise RuntimeError("Spawn already pending for: %s" % user.name)
|
||||||
tic = IOLoop.current().time()
|
tic = IOLoop.current().time()
|
||||||
@@ -281,6 +281,7 @@ class BaseHandler(RequestHandler):
|
|||||||
hub=self.hub,
|
hub=self.hub,
|
||||||
config=self.config,
|
config=self.config,
|
||||||
authenticator=self.authenticator,
|
authenticator=self.authenticator,
|
||||||
|
options=options,
|
||||||
)
|
)
|
||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
def finish_user_spawn(f=None):
|
def finish_user_spawn(f=None):
|
||||||
@@ -457,9 +458,15 @@ class UserSpawnHandler(BaseHandler):
|
|||||||
# spawn has supposedly finished, check on the status
|
# spawn has supposedly finished, check on the status
|
||||||
status = yield current_user.spawner.poll()
|
status = yield current_user.spawner.poll()
|
||||||
if status is not None:
|
if status is not None:
|
||||||
yield self.spawn_single_user(current_user)
|
if self.spawner_class.options_form:
|
||||||
|
self.redirect(url_path_join(self.hub.server.base_url, 'spawn'))
|
||||||
|
else:
|
||||||
|
yield self.spawn_single_user(current_user)
|
||||||
else:
|
else:
|
||||||
yield self.spawn_single_user(current_user)
|
if self.spawner_class.options_form:
|
||||||
|
self.redirect(url_path_join(self.hub.server.base_url, 'spawn'))
|
||||||
|
else:
|
||||||
|
yield self.spawn_single_user(current_user)
|
||||||
# set login cookie anew
|
# set login cookie anew
|
||||||
self.set_login_cookie(current_user)
|
self.set_login_cookie(current_user)
|
||||||
without_prefix = self.request.uri[len(self.hub.server.base_url):]
|
without_prefix = self.request.uri[len(self.hub.server.base_url):]
|
||||||
|
@@ -68,7 +68,7 @@ class LoginHandler(BaseHandler):
|
|||||||
if user.spawner:
|
if user.spawner:
|
||||||
status = yield user.spawner.poll()
|
status = yield user.spawner.poll()
|
||||||
already_running = (status == None)
|
already_running = (status == None)
|
||||||
if not already_running:
|
if not already_running and not self.spawner_class.options_form:
|
||||||
yield self.spawn_single_user(user)
|
yield self.spawn_single_user(user)
|
||||||
self.set_login_cookie(user)
|
self.set_login_cookie(user)
|
||||||
next_url = self.get_argument('next', default='')
|
next_url = self.get_argument('next', default='')
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
# Copyright (c) Jupyter Development Team.
|
# Copyright (c) Jupyter Development Team.
|
||||||
# Distributed under the terms of the Modified BSD License.
|
# Distributed under the terms of the Modified BSD License.
|
||||||
|
|
||||||
from tornado import web
|
from tornado import web, gen
|
||||||
|
|
||||||
from .. import orm
|
from .. import orm
|
||||||
from ..utils import admin_only, url_path_join
|
from ..utils import admin_only, url_path_join
|
||||||
@@ -47,6 +47,51 @@ class HomeHandler(BaseHandler):
|
|||||||
self.finish(html)
|
self.finish(html)
|
||||||
|
|
||||||
|
|
||||||
|
class SpawnHandler(BaseHandler):
|
||||||
|
"""Handle spawning of single-user servers via form.
|
||||||
|
|
||||||
|
GET renders the form, POST handles form submission.
|
||||||
|
|
||||||
|
Only enabled when Spawner.options_form is defined.
|
||||||
|
"""
|
||||||
|
@web.authenticated
|
||||||
|
def get(self):
|
||||||
|
"""GET renders form for spawning with user-specified options"""
|
||||||
|
user = self.get_current_user()
|
||||||
|
if user.running:
|
||||||
|
url = user.server.base_url
|
||||||
|
self.log.debug("User is running: %s", url)
|
||||||
|
self.redirect(url)
|
||||||
|
return
|
||||||
|
if self.spawner_class.options_form:
|
||||||
|
html = self.render_template('spawn.html',
|
||||||
|
user=self.get_current_user(),
|
||||||
|
spawner_options_form=self.spawner_class.options_form,
|
||||||
|
)
|
||||||
|
self.finish(html)
|
||||||
|
else:
|
||||||
|
# not running, no form. Trigger spawn.
|
||||||
|
url = url_path_join(self.base_url, 'users', user.name)
|
||||||
|
self.redirect(url)
|
||||||
|
|
||||||
|
@web.authenticated
|
||||||
|
@gen.coroutine
|
||||||
|
def post(self):
|
||||||
|
"""POST spawns with user-specified options"""
|
||||||
|
user = self.get_current_user()
|
||||||
|
if user.running:
|
||||||
|
url = user.server.base_url
|
||||||
|
self.log.warning("User is already running: %s", url)
|
||||||
|
self.redirect(url)
|
||||||
|
return
|
||||||
|
form_options = {}
|
||||||
|
for key, byte_list in self.request.body_arguments.items():
|
||||||
|
form_options[key] = [ bs.decode('utf8') for bs in byte_list ]
|
||||||
|
options = self.spawner_class.options_from_form(form_options)
|
||||||
|
yield self.spawn_single_user(user, options=options)
|
||||||
|
url = user.server.base_url
|
||||||
|
self.redirect(url)
|
||||||
|
|
||||||
class AdminHandler(BaseHandler):
|
class AdminHandler(BaseHandler):
|
||||||
"""Render the admin page."""
|
"""Render the admin page."""
|
||||||
|
|
||||||
@@ -107,4 +152,5 @@ default_handlers = [
|
|||||||
(r'/', RootHandler),
|
(r'/', RootHandler),
|
||||||
(r'/home', HomeHandler),
|
(r'/home', HomeHandler),
|
||||||
(r'/admin', AdminHandler),
|
(r'/admin', AdminHandler),
|
||||||
|
(r'/spawn', SpawnHandler),
|
||||||
]
|
]
|
||||||
|
@@ -73,6 +73,29 @@ class Spawner(LoggingConfigurable):
|
|||||||
help="Enable debug-logging of the single-user server"
|
help="Enable debug-logging of the single-user server"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# options_form is a class attribute, defining an HTML form snippet,
|
||||||
|
# which can be used to specify whether
|
||||||
|
# (i.e. just the <input> elements, not submit button or the <form> tag).
|
||||||
|
# This is **not** a configurable,
|
||||||
|
options_form = ""
|
||||||
|
@classmethod
|
||||||
|
def options_from_form(cls, form_data):
|
||||||
|
"""Interpret HTTP form data
|
||||||
|
|
||||||
|
Form data will always arrive as a dict of lists of strings.
|
||||||
|
Override this function to understand single-values, numbers, etc.
|
||||||
|
|
||||||
|
This should coerce form data into the structure expected by self.options,
|
||||||
|
which must be a dict.
|
||||||
|
|
||||||
|
Instances will receive this data on self.user_options, after passing through this function.
|
||||||
|
|
||||||
|
This must be a @classmethod.
|
||||||
|
"""
|
||||||
|
return form_data
|
||||||
|
|
||||||
|
user_options = Dict(help="This is where form-specified options ultimately end up.")
|
||||||
|
|
||||||
env_keep = List([
|
env_keep = List([
|
||||||
'PATH',
|
'PATH',
|
||||||
'PYTHONPATH',
|
'PYTHONPATH',
|
||||||
|
@@ -107,7 +107,8 @@ class User(HasTraits):
|
|||||||
return quote(self.name, safe='@')
|
return quote(self.name, safe='@')
|
||||||
|
|
||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
def spawn(self, spawner_class, base_url='/', hub=None, config=None, authenticator=None):
|
def spawn(self, spawner_class, base_url='/', hub=None, config=None,
|
||||||
|
authenticator=None, options=None):
|
||||||
"""Start the user's spawner"""
|
"""Start the user's spawner"""
|
||||||
db = self.db
|
db = self.db
|
||||||
if hub is None:
|
if hub is None:
|
||||||
@@ -129,6 +130,7 @@ class User(HasTraits):
|
|||||||
hub=hub,
|
hub=hub,
|
||||||
db=db,
|
db=db,
|
||||||
authenticator=authenticator,
|
authenticator=authenticator,
|
||||||
|
user_options=options or {},
|
||||||
)
|
)
|
||||||
# we are starting a new server, make sure it doesn't restore state
|
# we are starting a new server, make sure it doesn't restore state
|
||||||
spawner.clear_state()
|
spawner.clear_state()
|
||||||
|
@@ -9,8 +9,15 @@
|
|||||||
<a id="stop" class="btn btn-lg btn-danger">Stop My Server</a>
|
<a id="stop" class="btn btn-lg btn-danger">Stop My Server</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a id="start" class="btn btn-lg btn-success"
|
<a id="start" class="btn btn-lg btn-success"
|
||||||
|
{% if user.running %}
|
||||||
href="{{base_url}}user/{{user.name}}/"
|
href="{{base_url}}user/{{user.name}}/"
|
||||||
|
{% else %}
|
||||||
|
href="{{base_url}}spawn"
|
||||||
|
{% endif %}
|
||||||
>
|
>
|
||||||
|
{% if not user.running %}
|
||||||
|
Start
|
||||||
|
{% endif %}
|
||||||
My Server
|
My Server
|
||||||
</a>
|
</a>
|
||||||
{% if user.admin %}
|
{% if user.admin %}
|
||||||
|
18
share/jupyter/hub/templates/spawn.html
Normal file
18
share/jupyter/hub/templates/spawn.html
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{% extends "page.html" %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="row text-center">
|
||||||
|
<h1>Spawner options</h1>
|
||||||
|
</div>
|
||||||
|
<div class="row col-sm-offset-2 col-sm-8">
|
||||||
|
<form id="spawn_form" action="{{base_url}}spawn" method="post">
|
||||||
|
{{spawner_options_form}}
|
||||||
|
<br>
|
||||||
|
<input type="submit" value="Spawn">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
Reference in New Issue
Block a user