diff --git a/jupyterhub/handlers/pages.py b/jupyterhub/handlers/pages.py index 6217ccf6..56ea2314 100644 --- a/jupyterhub/handlers/pages.py +++ b/jupyterhub/handlers/pages.py @@ -131,21 +131,24 @@ class SpawnHandler(BaseHandler): # must not be /user/server for named servers, # which may get handled by the default server if they aren't ready yet - next_url = self.get_next_url( - user, - default=url_path_join( - self.hub.base_url, "spawn-pending", user.name, server_name - ), + pending_url = url_path_join( + self.hub.base_url, "spawn-pending", user.name, server_name ) + if self.get_argument('next', None): + # preserve `?next=...` through spawn-pending + pending_url = url_concat(pending_url, {'next': self.get_argument('next')}) + # spawner is active, redirect back to get progress, etc. if spawner.ready: self.log.info("Server %s is already running", spawner._log_name) next_url = self.get_next_url(user, default=user.server_url(server_name)) + self.redirect(next_url) + return elif spawner.active: self.log.info("Server %s is already active", spawner._log_name) - self.redirect(next_url) + self.redirect(pending_url) return # Add handler to spawner here so you can access query params in form rendering. @@ -169,7 +172,7 @@ class SpawnHandler(BaseHandler): # not running, no form. Trigger spawn and redirect back to /user/:name f = asyncio.ensure_future(self.spawn_single_user(user, server_name)) await asyncio.wait([f], timeout=1) - self.redirect(next_url) + self.redirect(pending_url) @web.authenticated async def post(self, for_user=None, server_name=''): @@ -317,10 +320,7 @@ class SpawnPendingHandler(BaseHandler): # further, set status to 404 because this is not # serving the expected page if status is not None: - spawn_url = url_concat( - url_path_join(self.hub.base_url, "spawn", user.escaped_name), - {"next": self.request.uri}, - ) + spawn_url = url_path_join(self.hub.base_url, "spawn", user.escaped_name) html = self.render_template( "not_running.html", user=user, diff --git a/jupyterhub/tests/test_pages.py b/jupyterhub/tests/test_pages.py index 5fde8930..6be9b723 100644 --- a/jupyterhub/tests/test_pages.py +++ b/jupyterhub/tests/test_pages.py @@ -2,6 +2,7 @@ import asyncio import sys from unittest import mock +from urllib.parse import parse_qs from urllib.parse import urlencode from urllib.parse import urlparse @@ -16,6 +17,7 @@ from ..handlers import BaseHandler from ..utils import url_path_join as ujoin from .mocking import FalsyCallableFormSpawner from .mocking import FormSpawner +from .test_api import next_event from .utils import add_user from .utils import api_request from .utils import async_requests @@ -310,6 +312,61 @@ async def test_spawn_form_with_file(app): } +async def test_spawn_pending(app, username, slow_spawn): + cookies = await app.login_user(username) + # first request, no spawn is pending + # spawn-pending shows button linking to spawn + r = await get_page('/spawn-pending/' + username, app, cookies=cookies) + r.raise_for_status() + page = BeautifulSoup(r.text, "html.parser") + assert "is not running" in page.body.text + link = page.find("a", id="start") + assert link + assert link['href'] == ujoin(app.base_url, '/hub/spawn/', username) + + # request spawn + next_url = ujoin(app.base_url, 'user', username, 'tree/foo') + spawn_url = url_concat('/spawn/' + username, dict(next=next_url)) + r = await get_page(spawn_url, app, cookies=cookies) + r.raise_for_status() + url = urlparse(r.url) + # spawn redirects to spawn-pending + assert url.path == ujoin(app.base_url, 'hub/spawn-pending', username) + # ?next query arg is preserved + assert parse_qs(url.query).get('next') == [next_url] + + # check spawn-pending html + page = BeautifulSoup(r.text, "html.parser") + assert page.find('div', {'class': 'progress'}) + + # validate event source url by consuming it + script = page.body.find('script').text + assert 'EventSource' in script + # find EventSource url in javascript + # maybe not the most robust way to check this? + eventsource = script.find('new EventSource') + start = script.find('(', eventsource) + end = script.find(')', start) + # strip quotes + progress_url = script[start + 2 : end - 1] + # verify that it works by watching progress events + # (double-duty because we also want to wait for the spawn to finish) + progress = await api_request(app, 'users', username, 'server/progress', stream=True) + ex = async_requests.executor + line_iter = iter(progress.iter_lines(decode_unicode=True)) + evt = True + while evt is not None: + evt = await ex.submit(next_event, line_iter) + if evt: + print(evt) + + # refresh page after progress is complete + r = await async_requests.get(r.url, cookies=cookies) + r.raise_for_status() + # should have redirected to the now-running server + assert urlparse(r.url).path == urlparse(next_url).path + + async def test_user_redirect(app, username): name = username cookies = await app.login_user(name) @@ -650,7 +707,7 @@ async def test_token_page(app): assert urlparse(r.url).path.endswith('/hub/token') def extract_body(r): - soup = BeautifulSoup(r.text, "html5lib") + soup = BeautifulSoup(r.text, "html.parser") import re # trim empty lines diff --git a/share/jupyterhub/templates/spawn_pending.html b/share/jupyterhub/templates/spawn_pending.html index ad6a6e41..b1aedb3a 100644 --- a/share/jupyterhub/templates/spawn_pending.html +++ b/share/jupyterhub/templates/spawn_pending.html @@ -35,7 +35,7 @@ require(["jquery"], function ($) { $("#refresh").click(function () { window.location.reload(); - }) + }); // hook up event-stream for progress var evtSource = new EventSource("{{ progress_url }}"); @@ -85,7 +85,7 @@ require(["jquery"], function ($) { // open event log for debugging $('#progress-details').prop('open', true); } - } + }; });