mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-07 18:14:10 +00:00
Compare commits
23 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
220eb87bce | ||
![]() |
f9e9150abc | ||
![]() |
8074469ad7 | ||
![]() |
46d2455aff | ||
![]() |
72e4119e1a | ||
![]() |
faa1754645 | ||
![]() |
318f739ba9 | ||
![]() |
20b3229249 | ||
![]() |
f0862f1d10 | ||
![]() |
3c5f9b255e | ||
![]() |
b6d9d5c120 | ||
![]() |
bccd0e2ff1 | ||
![]() |
a2d39c693d | ||
![]() |
76e65da9ff | ||
![]() |
eb9bb71655 | ||
![]() |
a39ef8f163 | ||
![]() |
f4727cba47 | ||
![]() |
14dfa65c75 | ||
![]() |
9f23bc2959 | ||
![]() |
24e8362401 | ||
![]() |
c4c662843c | ||
![]() |
6d5b13962c | ||
![]() |
fe64595d75 |
@@ -9,6 +9,10 @@ cryptography
|
||||
html5lib # needed for beautifulsoup
|
||||
jupyterlab >=3
|
||||
mock
|
||||
# nbclassic provides the '/tree/' handler, which we use in tests
|
||||
# it is a transitive dependency via jupyterlab,
|
||||
# but depend on it directly
|
||||
nbclassic
|
||||
pre-commit
|
||||
pytest>=3.3
|
||||
pytest-asyncio; python_version < "3.7"
|
||||
|
@@ -6,7 +6,7 @@ info:
|
||||
description: The REST API for JupyterHub
|
||||
license:
|
||||
name: BSD-3-Clause
|
||||
version: 2.3.0
|
||||
version: 2.3.2.dev
|
||||
servers:
|
||||
- url: /hub/api
|
||||
security:
|
||||
|
File diff suppressed because one or more lines are too long
@@ -60,7 +60,10 @@ const AddUser = (props) => {
|
||||
placeholder="usernames separated by line"
|
||||
data-testid="user-textarea"
|
||||
onBlur={(e) => {
|
||||
let split_users = e.target.value.split("\n");
|
||||
let split_users = e.target.value
|
||||
.split("\n")
|
||||
.map((u) => u.trim())
|
||||
.filter((u) => u.length > 0);
|
||||
setUsers(split_users);
|
||||
}}
|
||||
></textarea>
|
||||
@@ -88,17 +91,7 @@ const AddUser = (props) => {
|
||||
data-testid="submit"
|
||||
className="btn btn-primary"
|
||||
onClick={() => {
|
||||
let filtered_users = users.filter(
|
||||
(e) =>
|
||||
e.length > 2 &&
|
||||
/[!@#$%^&*(),.?":{}|<>]/g.test(e) == false
|
||||
);
|
||||
if (filtered_users.length < users.length) {
|
||||
setUsers(filtered_users);
|
||||
failRegexEvent();
|
||||
}
|
||||
|
||||
addUsers(filtered_users, admin)
|
||||
addUsers(users, admin)
|
||||
.then((data) =>
|
||||
data.status < 300
|
||||
? updateUsers(0, limit)
|
||||
|
@@ -70,12 +70,12 @@ test("Removes users when they fail Regex", async () => {
|
||||
let textarea = screen.getByTestId("user-textarea");
|
||||
let submit = screen.getByTestId("submit");
|
||||
|
||||
fireEvent.blur(textarea, { target: { value: "foo\nbar\n!!*&*" } });
|
||||
fireEvent.blur(textarea, { target: { value: "foo \n bar\na@b.co\n \n\n" } });
|
||||
await act(async () => {
|
||||
fireEvent.click(submit);
|
||||
});
|
||||
|
||||
expect(callbackSpy).toHaveBeenCalledWith(["foo", "bar"], false);
|
||||
expect(callbackSpy).toHaveBeenCalledWith(["foo", "bar", "a@b.co"], false);
|
||||
});
|
||||
|
||||
test("Correctly submits admin", async () => {
|
||||
|
@@ -59,7 +59,7 @@ const CreateGroup = (props) => {
|
||||
value={groupName}
|
||||
placeholder="group name..."
|
||||
onChange={(e) => {
|
||||
setGroupName(e.target.value);
|
||||
setGroupName(e.target.value.trim());
|
||||
}}
|
||||
></input>
|
||||
</div>
|
||||
|
@@ -200,6 +200,25 @@ const ServerDashboard = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
const ServerRowTable = ({ data }) => {
|
||||
return (
|
||||
<ReactObjectTableViewer
|
||||
className="table-striped table-bordered"
|
||||
style={{
|
||||
padding: "3px 6px",
|
||||
margin: "auto",
|
||||
}}
|
||||
keyStyle={{
|
||||
padding: "4px",
|
||||
}}
|
||||
valueStyle={{
|
||||
padding: "4px",
|
||||
}}
|
||||
data={data}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const serverRow = (user, server) => {
|
||||
const { servers, ...userNoServers } = user;
|
||||
const serverNameDash = server.name ? `-${server.name}` : "";
|
||||
@@ -286,37 +305,11 @@ const ServerDashboard = (props) => {
|
||||
>
|
||||
<Card style={{ width: "100%", padding: 3, margin: "0 auto" }}>
|
||||
<Card.Title>User</Card.Title>
|
||||
<ReactObjectTableViewer
|
||||
className="table-striped table-bordered admin-table-head"
|
||||
style={{
|
||||
padding: "3px 6px",
|
||||
margin: "auto",
|
||||
}}
|
||||
keyStyle={{
|
||||
padding: "4px",
|
||||
}}
|
||||
valueStyle={{
|
||||
padding: "4px",
|
||||
}}
|
||||
data={userNoServers}
|
||||
/>
|
||||
<ServerRowTable data={userNoServers} />
|
||||
</Card>
|
||||
<Card style={{ width: "100%", padding: 3, margin: "0 auto" }}>
|
||||
<Card.Title>Server</Card.Title>
|
||||
<ReactObjectTableViewer
|
||||
className="table-striped table-bordered admin-table-head"
|
||||
style={{
|
||||
padding: "3px 6px",
|
||||
margin: "auto",
|
||||
}}
|
||||
keyStyle={{
|
||||
padding: "4px",
|
||||
}}
|
||||
valueStyle={{
|
||||
padding: "4px",
|
||||
}}
|
||||
data={server}
|
||||
/>
|
||||
<ServerRowTable data={server} />
|
||||
</Card>
|
||||
</CardGroup>
|
||||
</Collapse>
|
||||
|
@@ -2,7 +2,7 @@
|
||||
# Copyright (c) Jupyter Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
# version_info updated by running `tbump`
|
||||
version_info = (2, 3, 0, "", "")
|
||||
version_info = (2, 3, 2, "", "dev")
|
||||
|
||||
# pep 440 version: no dot before beta/rc, but before .dev
|
||||
# 0.1.0rc1
|
||||
|
@@ -1689,7 +1689,9 @@ class JupyterHub(Application):
|
||||
for authority, files in self.internal_ssl_authorities.items():
|
||||
if files:
|
||||
self.log.info("Adding CA for %s", authority)
|
||||
certipy.store.add_record(authority, is_ca=True, files=files)
|
||||
certipy.store.add_record(
|
||||
authority, is_ca=True, files=files, overwrite=True
|
||||
)
|
||||
|
||||
self.internal_trust_bundles = certipy.trust_from_graph(
|
||||
self.internal_ssl_components_trust
|
||||
|
@@ -536,9 +536,7 @@ class Hashed(Expiring):
|
||||
prefix = token[: cls.prefix_length]
|
||||
# since we can't filter on hashed values, filter on prefix
|
||||
# so we aren't comparing with all tokens
|
||||
prefix_match = db.query(cls).filter(
|
||||
bindparam('prefix', prefix).startswith(cls.prefix)
|
||||
)
|
||||
prefix_match = db.query(cls).filter_by(prefix=prefix)
|
||||
prefix_match = prefix_match.filter(
|
||||
or_(cls.expires_at == None, cls.expires_at >= cls.now())
|
||||
)
|
||||
|
@@ -29,9 +29,9 @@ else:
|
||||
try:
|
||||
App = import_item(JUPYTERHUB_SINGLEUSER_APP)
|
||||
except ImportError as e:
|
||||
continue
|
||||
if _import_error is None:
|
||||
_import_error = e
|
||||
continue
|
||||
else:
|
||||
break
|
||||
if App is None:
|
||||
|
@@ -182,6 +182,7 @@ page_template = """
|
||||
|
||||
<span>
|
||||
<a href='{{hub_control_panel_url}}'
|
||||
id='jupyterhub-control-panel-link'
|
||||
class='btn btn-default btn-sm navbar-btn pull-right'
|
||||
style='margin-right: 4px; margin-left: 2px;'>
|
||||
Control Panel
|
||||
@@ -633,8 +634,15 @@ class SingleUserNotebookAppMixin(Configurable):
|
||||
# disable trash by default
|
||||
# this can be re-enabled by config
|
||||
self.config.FileContentsManager.delete_to_trash = False
|
||||
# load default-url env at higher priority than `@default`,
|
||||
# which may have their own _defaults_ which should not override explicit default_url config
|
||||
# via e.g. c.Spawner.default_url. Seen in jupyterlab's SingleUserLabApp.
|
||||
default_url = os.environ.get("JUPYTERHUB_DEFAULT_URL")
|
||||
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__)
|
||||
@@ -705,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
|
||||
@@ -738,19 +745,32 @@ 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'])
|
||||
for ext_name in ("notebook", "nbclassic"):
|
||||
env_name = f"{ext_name}_jinja2_env"
|
||||
if env_name in settings:
|
||||
# when running with jupyter-server, classic notebook (nbclassic server extension or notebook v7)
|
||||
# gets its own jinja env, which needs the same patch
|
||||
jinja_envs.append(settings[env_name])
|
||||
|
||||
# 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
|
||||
|
@@ -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
|
||||
@@ -196,10 +199,22 @@ def test_singleuser_app_class(JUPYTERHUB_SINGLEUSER_APP):
|
||||
import jupyter_server # noqa
|
||||
except ImportError:
|
||||
have_server = False
|
||||
expect_error = "jupyter_server" in JUPYTERHUB_SINGLEUSER_APP
|
||||
else:
|
||||
have_server = True
|
||||
expect_error = False
|
||||
try:
|
||||
import notebook.notebookapp # noqa
|
||||
except ImportError:
|
||||
have_notebook = False
|
||||
else:
|
||||
have_notebook = True
|
||||
|
||||
if JUPYTERHUB_SINGLEUSER_APP.startswith("notebook."):
|
||||
expect_error = not have_notebook
|
||||
elif JUPYTERHUB_SINGLEUSER_APP.startswith("jupyter_server."):
|
||||
expect_error = not have_server
|
||||
else:
|
||||
# not specified, will try both
|
||||
expect_error = not (have_server or have_notebook)
|
||||
|
||||
if expect_error:
|
||||
ctx = pytest.raises(CalledProcessError)
|
||||
@@ -225,3 +240,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")
|
||||
|
@@ -15,7 +15,7 @@ target_version = [
|
||||
github_url = "https://github.com/jupyterhub/jupyterhub"
|
||||
|
||||
[tool.tbump.version]
|
||||
current = "2.3.0"
|
||||
current = "2.3.2.dev"
|
||||
|
||||
# Example of a semver regexp.
|
||||
# Make sure this matches current_version before
|
||||
|
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user