mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-13 13:03:01 +00:00

call poll_and_notify to ensure triggering of dead-server events in a few places: - `/hub/home` page view - user start and stop API endpoints This should avoid the failure to stop a server that's died uncleanly because the server hasn't noticed yet
174 lines
5.4 KiB
Python
174 lines
5.4 KiB
Python
"""Basic html-rendering handlers."""
|
|
|
|
# Copyright (c) Jupyter Development Team.
|
|
# Distributed under the terms of the Modified BSD License.
|
|
|
|
from tornado import web, gen
|
|
|
|
from .. import orm
|
|
from ..utils import admin_only, url_path_join
|
|
from .base import BaseHandler
|
|
from urllib.parse import quote
|
|
|
|
|
|
class RootHandler(BaseHandler):
|
|
"""Render the Hub root page.
|
|
|
|
If logged in, redirects to:
|
|
|
|
- single-user server if running
|
|
- hub home, otherwise
|
|
|
|
Otherwise, renders login page.
|
|
"""
|
|
def get(self):
|
|
user = self.get_current_user()
|
|
if user:
|
|
if user.running:
|
|
url = user.url
|
|
self.log.debug("User is running: %s", url)
|
|
self.set_login_cookie(user) # set cookie
|
|
else:
|
|
url = url_path_join(self.hub.server.base_url, 'home')
|
|
self.log.debug("User is not running: %s", url)
|
|
self.redirect(url)
|
|
return
|
|
url = url_path_join(self.hub.server.base_url, 'login')
|
|
self.redirect(url)
|
|
|
|
|
|
class HomeHandler(BaseHandler):
|
|
"""Render the user's home page."""
|
|
|
|
@web.authenticated
|
|
@gen.coroutine
|
|
def get(self):
|
|
user = self.get_current_user()
|
|
if user.running:
|
|
# trigger poll_and_notify event in case of a server that died
|
|
yield user.spawner.poll_and_notify()
|
|
html = self.render_template('home.html',
|
|
user=user,
|
|
)
|
|
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.
|
|
"""
|
|
def _render_form(self, message=''):
|
|
user = self.get_current_user()
|
|
return self.render_template('spawn.html',
|
|
user=user,
|
|
spawner_options_form=user.spawner.options_form,
|
|
error_message=message,
|
|
)
|
|
|
|
@web.authenticated
|
|
def get(self):
|
|
"""GET renders form for spawning with user-specified options"""
|
|
user = self.get_current_user()
|
|
if user.running:
|
|
url = user.url
|
|
self.log.debug("User is running: %s", url)
|
|
self.redirect(url)
|
|
return
|
|
if user.spawner.options_form:
|
|
self.finish(self._render_form())
|
|
else:
|
|
# not running, no form. Trigger spawn.
|
|
self.redirect(user.url)
|
|
|
|
@web.authenticated
|
|
@gen.coroutine
|
|
def post(self):
|
|
"""POST spawns with user-specified options"""
|
|
user = self.get_current_user()
|
|
if user.running:
|
|
url = user.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 ]
|
|
for key, byte_list in self.request.files.items():
|
|
form_options["%s_file"%key] = byte_list
|
|
try:
|
|
options = user.spawner.options_from_form(form_options)
|
|
yield self.spawn_single_user(user, options=options)
|
|
except Exception as e:
|
|
self.log.error("Failed to spawn single-user server with form", exc_info=True)
|
|
self.finish(self._render_form(str(e)))
|
|
return
|
|
self.set_login_cookie(user)
|
|
url = user.url
|
|
self.redirect(url)
|
|
|
|
class AdminHandler(BaseHandler):
|
|
"""Render the admin page."""
|
|
|
|
@admin_only
|
|
def get(self):
|
|
available = {'name', 'admin', 'running', 'last_activity'}
|
|
default_sort = ['admin', 'name']
|
|
mapping = {
|
|
'running': '_server_id'
|
|
}
|
|
default_order = {
|
|
'name': 'asc',
|
|
'last_activity': 'desc',
|
|
'admin': 'desc',
|
|
'running': 'desc',
|
|
}
|
|
sorts = self.get_arguments('sort') or default_sort
|
|
orders = self.get_arguments('order')
|
|
|
|
for bad in set(sorts).difference(available):
|
|
self.log.warning("ignoring invalid sort: %r", bad)
|
|
sorts.remove(bad)
|
|
for bad in set(orders).difference({'asc', 'desc'}):
|
|
self.log.warning("ignoring invalid order: %r", bad)
|
|
orders.remove(bad)
|
|
|
|
# add default sort as secondary
|
|
for s in default_sort:
|
|
if s not in sorts:
|
|
sorts.append(s)
|
|
if len(orders) < len(sorts):
|
|
for col in sorts[len(orders):]:
|
|
orders.append(default_order[col])
|
|
else:
|
|
orders = orders[:len(sorts)]
|
|
|
|
# this could be one incomprehensible nested list comprehension
|
|
# get User columns
|
|
cols = [ getattr(orm.User, mapping.get(c, c)) for c in sorts ]
|
|
# get User.col.desc() order objects
|
|
ordered = [ getattr(c, o)() for c, o in zip(cols, orders) ]
|
|
|
|
users = self.db.query(orm.User).order_by(*ordered)
|
|
users = [ self._user_from_orm(u) for u in users ]
|
|
running = [ u for u in users if u.running ]
|
|
|
|
html = self.render_template('admin.html',
|
|
user=self.get_current_user(),
|
|
admin_access=self.settings.get('admin_access', False),
|
|
users=users,
|
|
running=running,
|
|
sort={s:o for s,o in zip(sorts, orders)},
|
|
)
|
|
self.finish(html)
|
|
|
|
|
|
default_handlers = [
|
|
(r'/', RootHandler),
|
|
(r'/home', HomeHandler),
|
|
(r'/admin', AdminHandler),
|
|
(r'/spawn', SpawnHandler),
|
|
]
|