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:
Min RK
2015-12-16 15:52:53 +01:00
parent 675f19b5cb
commit ba634354dd
8 changed files with 112 additions and 8 deletions

View File

@@ -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)

View File

@@ -270,7 +270,7 @@ class BaseHandler(RequestHandler):
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,7 +458,13 @@ 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:
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) yield self.spawn_single_user(current_user)
else:
if self.spawner_class.options_form:
self.redirect(url_path_join(self.hub.server.base_url, 'spawn'))
else: else:
yield self.spawn_single_user(current_user) yield self.spawn_single_user(current_user)
# set login cookie anew # set login cookie anew

View File

@@ -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='')

View File

@@ -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),
] ]

View File

@@ -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',

View File

@@ -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()

View File

@@ -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 %}

View 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 %}