diff --git a/jupyterhub/singleuser/mixins.py b/jupyterhub/singleuser/mixins.py index 05cb1679..a57a590b 100755 --- a/jupyterhub/singleuser/mixins.py +++ b/jupyterhub/singleuser/mixins.py @@ -182,6 +182,7 @@ page_template = """ Control Panel @@ -640,7 +641,8 @@ class SingleUserNotebookAppMixin(Configurable): if default_url: self.config[self.__class__.__name__].default_url = default_url self._log_app_versions() - return super().initialize(argv) + super().initialize(argv) + self.patch_templates() def start(self): self.log.info("Starting jupyterhub-singleuser server version %s", __version__) @@ -711,7 +713,6 @@ class SingleUserNotebookAppMixin(Configurable): # apply X-JupyterHub-Version to *all* request handlers (even redirects) self.patch_default_headers() - self.patch_templates() def page_config_hook(self, handler, page_config): """JupyterLab page config hook @@ -744,19 +745,30 @@ class SingleUserNotebookAppMixin(Configurable): ) self.jinja_template_vars['hub_host'] = self.hub_host self.jinja_template_vars['hub_prefix'] = self.hub_prefix - env = self.web_app.settings['jinja2_env'] + self.jinja_template_vars[ + 'hub_control_panel_url' + ] = self.hub_host + url_path_join(self.hub_prefix, 'home') - env.globals['hub_control_panel_url'] = self.hub_host + url_path_join( - self.hub_prefix, 'home' - ) + settings = self.web_app.settings + # patch classic notebook jinja env + jinja_envs = [] + if 'jinja2_env' in settings: + # default jinja env (should we do this on jupyter-server, or only notebook?) + jinja_envs.append(settings['jinja2_env']) + if 'notebook_jinja2_env' in settings: + # when running with jupyter-server, classic notebook (nbclassic server extension) + # gets its own jinja env, which needs the same patch + jinja_envs.append(settings['notebook_jinja2_env']) - # patch jinja env loading to modify page template + # patch jinja env loading to get modified template, only for base page.html def get_page(name): if name == 'page.html': return page_template - orig_loader = env.loader - env.loader = ChoiceLoader([FunctionLoader(get_page), orig_loader]) + for jinja_env in jinja_envs: + jinja_env.loader = ChoiceLoader( + [FunctionLoader(get_page), jinja_env.loader] + ) def load_server_extensions(self): # Loading LabApp sets $JUPYTERHUB_API_TOKEN on load, which is incorrect diff --git a/jupyterhub/tests/test_singleuser.py b/jupyterhub/tests/test_singleuser.py index 89dbb8f9..a606781b 100644 --- a/jupyterhub/tests/test_singleuser.py +++ b/jupyterhub/tests/test_singleuser.py @@ -5,9 +5,11 @@ from contextlib import contextmanager from subprocess import CalledProcessError from subprocess import check_output from unittest import mock +from urllib.parse import urlencode from urllib.parse import urlparse import pytest +from bs4 import BeautifulSoup import jupyterhub from .. import orm @@ -16,6 +18,7 @@ from .mocking import public_url from .mocking import StubSingleUserSpawner from .utils import async_requests from .utils import AsyncSession +from .utils import get_page @contextmanager @@ -225,3 +228,22 @@ def test_singleuser_app_class(JUPYTERHUB_SINGLEUSER_APP): else: assert '--ServerApp.' in out assert '--NotebookApp.' not in out + + +async def test_nbclassic_control_panel(app, user): + # use StubSingleUserSpawner to launch a single-user app in a thread + app.spawner_class = StubSingleUserSpawner + app.tornado_settings['spawner_class'] = StubSingleUserSpawner + + # login, start the server + await user.spawn() + cookies = await app.login_user(user.name) + next_url = url_path_join(user.url, "tree/") + url = '/?' + urlencode({'next': next_url}) + r = await get_page(url, app, cookies=cookies) + r.raise_for_status() + assert urlparse(r.url).path == urlparse(next_url).path + page = BeautifulSoup(r.text, "html.parser") + link = page.find("a", id="jupyterhub-control-panel-link") + assert link, f"Missing jupyterhub-control-panel-link in {page}" + assert link["href"] == url_path_join(app.base_url, "hub/home")