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:
|
||||
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
|
||||
self.set_status(status)
|
||||
|
||||
|
@@ -270,7 +270,7 @@ class BaseHandler(RequestHandler):
|
||||
return self.settings.get('spawner_class', LocalProcessSpawner)
|
||||
|
||||
@gen.coroutine
|
||||
def spawn_single_user(self, user):
|
||||
def spawn_single_user(self, user, options=None):
|
||||
if user.spawn_pending:
|
||||
raise RuntimeError("Spawn already pending for: %s" % user.name)
|
||||
tic = IOLoop.current().time()
|
||||
@@ -281,6 +281,7 @@ class BaseHandler(RequestHandler):
|
||||
hub=self.hub,
|
||||
config=self.config,
|
||||
authenticator=self.authenticator,
|
||||
options=options,
|
||||
)
|
||||
@gen.coroutine
|
||||
def finish_user_spawn(f=None):
|
||||
@@ -457,7 +458,13 @@ class UserSpawnHandler(BaseHandler):
|
||||
# spawn has supposedly finished, check on the status
|
||||
status = yield current_user.spawner.poll()
|
||||
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)
|
||||
else:
|
||||
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
|
||||
|
@@ -68,7 +68,7 @@ class LoginHandler(BaseHandler):
|
||||
if user.spawner:
|
||||
status = yield user.spawner.poll()
|
||||
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)
|
||||
self.set_login_cookie(user)
|
||||
next_url = self.get_argument('next', default='')
|
||||
|
@@ -3,7 +3,7 @@
|
||||
# Copyright (c) Jupyter Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
from tornado import web
|
||||
from tornado import web, gen
|
||||
|
||||
from .. import orm
|
||||
from ..utils import admin_only, url_path_join
|
||||
@@ -47,6 +47,51 @@ class HomeHandler(BaseHandler):
|
||||
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):
|
||||
"""Render the admin page."""
|
||||
|
||||
@@ -107,4 +152,5 @@ default_handlers = [
|
||||
(r'/', RootHandler),
|
||||
(r'/home', HomeHandler),
|
||||
(r'/admin', AdminHandler),
|
||||
(r'/spawn', SpawnHandler),
|
||||
]
|
||||
|
@@ -73,6 +73,29 @@ class Spawner(LoggingConfigurable):
|
||||
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([
|
||||
'PATH',
|
||||
'PYTHONPATH',
|
||||
|
@@ -107,7 +107,8 @@ class User(HasTraits):
|
||||
return quote(self.name, safe='@')
|
||||
|
||||
@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"""
|
||||
db = self.db
|
||||
if hub is None:
|
||||
@@ -129,6 +130,7 @@ class User(HasTraits):
|
||||
hub=hub,
|
||||
db=db,
|
||||
authenticator=authenticator,
|
||||
user_options=options or {},
|
||||
)
|
||||
# we are starting a new server, make sure it doesn't restore state
|
||||
spawner.clear_state()
|
||||
|
@@ -9,8 +9,15 @@
|
||||
<a id="stop" class="btn btn-lg btn-danger">Stop My Server</a>
|
||||
{% endif %}
|
||||
<a id="start" class="btn btn-lg btn-success"
|
||||
{% if user.running %}
|
||||
href="{{base_url}}user/{{user.name}}/"
|
||||
{% else %}
|
||||
href="{{base_url}}spawn"
|
||||
{% endif %}
|
||||
>
|
||||
{% if not user.running %}
|
||||
Start
|
||||
{% endif %}
|
||||
My Server
|
||||
</a>
|
||||
{% 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