Merge branch 'master' into delete-to-trash

Thomas Kluyver 9 years ago committed by GitHub
commit 1849b80287

3
.gitignore vendored

@ -33,3 +33,6 @@ src
Read the Docs
config.rst
/.project
/.pydevproject

@ -8,15 +8,15 @@ python:
- 3.5.1 # Set to 3.5.1 since travis has not yet included as default for 3.5
- 2.7
sudo: false
sudo: required
env:
global:
- PATH=$TRAVIS_BUILD_DIR/pandoc:$PATH
matrix:
- GROUP=js/notebook
- GROUP=python
- GROUP=js/base
- GROUP=js/notebook
- GROUP=js/services
- GROUP=js/tree
@ -29,15 +29,19 @@ before_install:
- npm --version
- npm upgrade -g npm
- npm install
- 'if [[ $GROUP == js* ]]; then npm install -g casperjs@1.1.0-beta5; fi'
- |
if [[ $GROUP == js* ]]; then
npm install -g casperjs@1.1.3 phantomjs-prebuilt@2.1.7
fi
- git clone --quiet --depth 1 https://github.com/minrk/travis-wheels travis-wheels
install:
- pip install -f travis-wheels/wheelhouse file://$PWD#egg=notebook[test]
- wget https://github.com/jgm/pandoc/releases/download/1.19.1/pandoc-1.19.1-1-amd64.deb && sudo dpkg -i pandoc-1.19.1-1-amd64.deb
script:
- 'if [[ $GROUP == js* ]]; then travis_retry python -m notebook.jstest ${GROUP:3}; fi'
- 'if [[ $GROUP == "js/notebook" ]]; then npm run lint; fi'
- 'if [[ $GROUP == python ]]; then nosetests -v --with-coverage --cover-package=notebook notebook; fi'
matrix:

@ -24,22 +24,20 @@ Installing Node.js and npm
^^^^^^^^^^^^^^^^^^^^^^^^^^
Building the Notebook from its GitHub source code requires some tools to
create and minify JavaScript components and the CSS.
Namely, that's Node.js and Node's package manager, ``npm``.
create and minify JavaScript components and the CSS,
specifically Node.js and Node's package manager, ``npm``.
It should be node version ≥ 6.0.
If you use ``conda``, you can get them with::
conda install -c javascript nodejs
conda install -c conda-forge nodejs
If you use `Homebrew <http://brew.sh/>`_ on Mac OS X::
brew install node
For Debian/Ubuntu systems, you should use the ``nodejs-legacy`` package instead
of the ``node`` package::
sudo apt-get update
sudo apt-get install nodejs-legacy npm
Installation on Linux may vary, but be aware that the `nodejs` or `npm` packages
included in the system package repository may be too old to work properly.
You can also use the installer from the `Node.js website <https://nodejs.org>`_.
@ -50,14 +48,13 @@ Installing the Jupyter Notebook
Once you have installed the dependencies mentioned above, use the following
steps::
pip install setuptools pip --upgrade --user
pip install --upgrade setuptools pip
git clone https://github.com/jupyter/notebook
cd notebook
pip install -e . --user
pip install -e .
If you want the development environment to be available for all users of your
system (assuming you have the necessary rights) or if you are installing in a
virtual environment, just drop the ``--user`` option.
If you are using a system-wide Python installation and you only want to install the notebook for you,
you can add ``--user`` to the install commands.
Once you have done this, you can launch the master branch of Jupyter notebook
from any directory in your system with::
@ -72,25 +69,28 @@ There is a build step for the JavaScript and CSS in the notebook.
To make sure that you are working with up-to-date code, you will need to run
this command whenever there are changes to JavaScript or LESS sources::
python setup.py js css
npm run build
**IMPORTANT:** Don't forget to run ``npm run build`` after switching branches.
When switching between branches of different versions (e.g. ``4.x`` and
``master``), run ``pip install -e .``. If you have tried the above and still
find that the notebook is not reflecting the current source code, try cleaning
the repo with ``git clean -xfd`` and reinstalling with ``pip install -e .``.
Prototyping Tip
Development Tip
"""""""""""""""
When doing prototyping which needs quick iteration of the Notebook's
JavaScript, run this in the root of the repository::
When doing development, you can use this command to automatically rebuild
JavaScript and LESS sources as they are modified::
npm run build:watch
This will cause WebPack to monitor the files you edit and recompile them on the
fly.
Git Hooks
"""""""""
If you want to automatically update dependencies, recompile the JavaScript, and
recompile the CSS after checking out a new commit, you can install
post-checkout and post-merge hooks which will do it for you::
If you want to automatically update dependencies and recompile JavaScript and
CSS after checking out a new commit, you can install post-checkout and
post-merge hooks which will do it for you::
git-hooks/install-hooks.sh
@ -105,7 +105,7 @@ Python Tests
Install dependencies::
pip install -e .[test] --user
pip install -e .[test]
To run the Python tests, use::
@ -121,7 +121,7 @@ JavaScript Tests
To run the JavaScript tests, you will need to have PhantomJS and CasperJS
installed::
npm install -g casperjs phantomjs@1.9.18
npm install -g casperjs phantomjs-prebuilt
Then, to run the JavaScript tests::
@ -154,10 +154,9 @@ containing all the necessary packages (except pandoc), use::
.. _conda environment:
http://conda.pydata.org/docs/using/envs.html#use-environment-from-file
If you want to install the necessary packages with ``pip`` instead, use
(omitting --user if working in a virtual environment)::
If you want to install the necessary packages with ``pip`` instead::
pip install -r docs/doc-requirements.txt --user
pip install -r docs/doc-requirements.txt
Once you have installed the required packages, you can build the docs with::

@ -1,5 +1,5 @@
include COPYING.md
include CONTRIBUTING.md
include CONTRIBUTING.rst
include README.md
include package.json
include bower.json

@ -3,10 +3,11 @@ matrix:
fast_finish: true
environment:
CONDA_INSTALL_LOCN: "C:\\conda"
matrix:
- CONDA_PY: 35
CONDA_INSTALL_LOCN: "C:\\Miniconda35-x64"
- CONDA_PY: 27
CONDA_INSTALL_LOCN: "C:\\Miniconda-x64"
platform:
- x64
@ -14,19 +15,8 @@ platform:
build: off
install:
- appveyor DownloadFile "https://raw.githubusercontent.com/conda-forge/conda-smithy/master/bootstrap-obvious-ci-and-miniconda.py"
- cmd: python bootstrap-obvious-ci-and-miniconda.py %CONDA_INSTALL_LOCN% %platform% %CONDA_PY:~0,1% --without-obvci
# Add a hack to switch to `conda` version `4.1.12` before activating.
# This is required to handle a long path activation issue.
# Verbatim from https://github.com/conda-forge/conda-smithy/pull/329
- cmd: set "OLDPATH=%PATH%"
- cmd: set "PATH=%CONDA_INSTALL_LOCN%\\Scripts;%CONDA_INSTALL_LOCN%\\Library\\bin;%PATH%"
- cmd: conda install --yes --quiet conda=4.1.12
- cmd: set "PATH=%OLDPATH%"
- cmd: set "OLDPATH="
- cmd: call %CONDA_INSTALL_LOCN%\Scripts\activate.bat
- cmd: conda update --yes --quiet conda
- cmd: conda config --set show_channel_urls true
- cmd: conda config --add channels conda-forge
- cmd: conda install -y pyzmq tornado jupyter_client nbformat nbconvert ipykernel pip nodejs nose

@ -5,16 +5,25 @@
"backbone": "components/backbone#~1.2",
"bootstrap": "components/bootstrap#~3.3",
"bootstrap-tour": "0.9.0",
"codemirror": "components/codemirror#~5.21",
"font-awesome": "components/font-awesome#~4.2.0",
"codemirror": "components/codemirror#~5.27",
"es6-promise": "~1.0",
"font-awesome": "components/font-awesome#~4.7.0",
"google-caja": "5669",
"jed": "~1.1.1",
"jquery": "components/jquery#~2.0",
"jquery-typeahead": "~2.0.0",
"jquery-ui": "components/jqueryui#~1.10",
"marked": "~0.3",
"MathJax": "components/MathJax#~2.6",
"moment": "~2.8.4",
"preact": "https://unpkg.com/preact@^7.2.0/dist/preact.min.js",
"preact-compat": "https://unpkg.com/preact-compat@^3.14.3/dist/preact-compat.min.js",
"proptypes": "https://unpkg.com/proptypes@^0.14.4/index.js",
"requirejs": "~2.1",
"requirejs-text": "~2.0.15",
"requirejs-plugins": "~1.0.3",
"text-encoding": "~0.1",
"underscore": "components/underscore#~1.8.3"
"underscore": "components/underscore#~1.8.3",
"xterm.js": "sourcelair/xterm.js#~2.8.1"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

@ -11,18 +11,218 @@ For more detailed information, see
Use ``pip install notebook --upgrade`` or ``conda upgrade notebook`` to
upgrade to the latest release.
.. we push for pip 9+ or it will break for Python 2 users when IPython 6 is out.
We strongly recommend that you upgrade to version 9+ of pip before upgrading ``notebook``.
.. tip::
Use ``pip install pip --upgrade`` to upgrade pip. Check pip version with
``pip --version``.
.. _release-5.0.0:
Notebook version 5.0
--------------------
This is the first major release of the Jupyter Notebook since version 4.0 was
created by the "Big Split" of IPython and Jupyter.
We encourage users to start trying JupyterLab in preparation for a future
transition.
We have merged more than 300 pull requests since 4.0. Some of the
major user-facing changes are described here.
File sorting in the dashboard
*****************************
Files in the dashboard may now be sorted by last modified date or name (:ghpull:`943`):
.. image:: /_static/images/dashboard-sort.png
:align: center
Cell tags
*********
There is a new cell toolbar for adding *cell tags* (:ghpull:`2048`):
.. image:: /_static/images/cell-tags-toolbar.png
:align: center
Cell tags are a lightweight way to customise the behaviour of tools working with
notebooks; we're working on building support for them into tools like `nbconvert
<http://nbconvert.readthedocs.io/en/latest/>`__ and `nbval
<https://github.com/computationalmodelling/nbval>`__. To start using tags,
select ``Tags`` in the ``View > Cell Toolbar`` menu in a notebook.
The UI for editing cell tags is basic for now; we hope to improve it in future
releases.
Table style
***********
The default styling for tables in the notebook has been updated (:ghpull:`1776`).
Before:
.. image:: /_static/images/table-style-before.png
:align: center
After:
.. image:: /_static/images/table-style-after.png
:align: center
Customise keyboard shortcuts
****************************
You can now edit keyboard shortcuts for *Command Mode* within the UI
(:ghpull:`1347`):
.. image:: /_static/images/shortcut-editor.png
:align: center
See the ``Help > Edit Keyboard Shortcuts`` menu item and follow the instructions.
Other additions
***************
- You can copy and paste cells between notebooks, using :kbd:`Ctrl-C` and
:kbd:`Ctrl-V` (:kbd:`Cmd-C` and :kbd:`Cmd-V` on Mac).
- It's easier to configure a password for the notebook with the new
``jupyter notebook password`` command (:ghpull:`2007`).
- The file list can now be ordered by *last modified* or by *name*
(:ghpull:`943`).
- Markdown cells now support attachments. Simply drag and drop an image from
your desktop to a markdown cell to add it. Unlike relative links that you
enter manually, attachments are embedded in the notebook itself. An
unreferenced attachment will be automatically scrubbed from the notebook on
save (:ghpull:`621`).
- Undoing cell deletion now supports undeleting multiple cells. Cells may not be
in the same order as before their deletion, depending on the actions you did
on the meantime, but this should should help reduce the impact of
accidentally deleting code.
- The file browser now has *Edit* and *View* buttons.
- The file browser now supports moving multiple files at once
(:ghpull:`1088`).
- The Notebook will refuse to run as root unless the ``--allow-root`` flag is
given (:ghpull:`1115`).
- Keyboard shortcuts are now declarative (:ghpull:`1234`).
- Toggling line numbers can now affect all cells (:ghpull:`1312`).
- Add more visible *Trusted* and *Untrusted* notifications (:ghpull:`1658`).
- The favicon (browser shortcut icon) now changes to indicate when the kernel is busy
(:ghpull:`1837`).
- Header and toolbar visibility is now persisted in nbconfig and across sessions
(:ghpull:`1769`).
- Load server extensions with ConfigManager so that merge happens recursively,
unlike normal config values, to make it load more consistently with frontend
extensions(:ghpull:`2108`).
- The notebook server now supports the `bundler API
<http://jupyter-notebook.readthedocs.io/en/latest/extending/bundler_extensions.html>`__
from the `jupyter_cms incubator project
<https://github.com/jupyter-incubator/contentmanagement>`__ (:ghpull:`1579`).
- The notebook server now provides information about kernel activity in
its kernel resource API (:ghpull:`1827`).
Remember that upgrading ``notebook`` only affects the user
interface. Upgrading kernels and libraries may also provide new features,
better stability and integration with the notebook interface.
.. _release-4.4.0:
4.4.0
-----
- Allow override of output callbacks to redirect output messages. This is used to implement the ipywidgets Output widget, for example.
- Fix an async bug in message handling by allowing comm message handlers to return a promise which halts message processing until the promise resolves.
See the 4.4 milestone on GitHub for a complete list of
`issues <https://github.com/jupyter/notebook/issues?utf8=%E2%9C%93&q=is%3Aissue%20milestone%3A4.4>`__
and `pull requests <https://github.com/jupyter/notebook/pulls?utf8=%E2%9C%93&q=is%3Apr%20milestone%3A4.4>`__ involved in this release.
.. _release-4.3.2:
4.3.2
-----
4.3.2 is a patch release with a bug fix for CodeMirror and improved handling of the "editable" cell metadata field.
- Monkey-patch for CodeMirror that resolves `#2037 <https://github.com/jupyter/notebook/issues/2037>`__ without breaking `#1967 <https://github.com/jupyter/notebook/issues/1967>`__
- Read-only (``"editable": false``) cells can be executed but cannot be split, merged, or deleted
See the 4.3.2 milestone on GitHub for a complete list of
`issues <https://github.com/jupyter/notebook/issues?utf8=%E2%9C%93&q=is%3Aissue%20milestone%3A4.3.2>`__
and `pull requests <https://github.com/jupyter/notebook/pulls?utf8=%E2%9C%93&q=is%3Apr%20milestone%3A4.3.2>`__ involved in this release.
.. _release-4.3.1:
4.3.1
-----
4.3.1 is a patch release with a security patch, a couple bug fixes, and improvements to the newly-released token authentication.
**Security fix**:
- CVE-2016-9971. Fix CSRF vulnerability,
where malicious forms could create untitled files and start kernels
(no remote execution or modification of existing files)
for users of certain browsers (Firefox, Internet Explorer / Edge).
All previous notebook releases are affected.
Bug fixes:
- Fix carriage return handling
- Make the font size more robust against fickle browsers
- Ignore resize events that bubbled up and didn't come from window
- Add Authorization to allowed CORS headers
- Downgrade CodeMirror to 5.16 while we figure out issues in Safari
Other improvements:
- Better docs for token-based authentication
- Further highlight token info in log output when autogenerated
See the 4.3.1 milestone on GitHub for a complete list of
`issues <https://github.com/jupyter/notebook/issues?utf8=%E2%9C%93&q=is%3Aissue%20milestone%3A4.3.1>`__
and `pull requests <https://github.com/jupyter/notebook/pulls?utf8=%E2%9C%93&q=is%3Apr%20milestone%3A4.3.1>`__ involved in this release.
.. _release-4.3:
4.3
4.3.0
-----
4.3 is a minor release with many bug fixes and improvements.
The biggest user-facing change is the addition of token authentication,
which is enabled by default.
A token is generated and used when your browser is opened automatically,
so you shouldn't have to enter anything in the default circumstances.
If you see a login page
(e.g. by switching browsers, or launching on a new port with ``--no-browser``),
you get a login URL with the token from the command ``jupyter notebook list``,
which you can paste into your browser.
Highlights:
- API for creating mime-type based renderer extensions using :code:`OutputArea.register_mime_type` and :code:`Notebook.render_cell_output` methods. See `mimerender-cookiecutter <https://github.com/jupyterlab/mimerender-cookiecutter>`__ for reference implementations and cookiecutter.
- Enable token authentication by default
- Enable token authentication by default. See :ref:`server_security` for more details.
- Update security docs to reflect new signature system
- Switched from term.js to xterm.js
@ -31,7 +231,7 @@ Bug fixes:
- Ensure variable is set if exc_info is falsey
- Catch and log handler exceptions in :code:`events.trigger`
- Add debug log for static file paths
- Don't check origin on token-authenticated requests
- Don't check origin on token-authenticated requests
- Remove leftover print statement
- Fix highlighting of Python code blocks
- :code:`json_errors` should be outermost decorator on API handlers
@ -45,7 +245,7 @@ Bug fixes:
Other improvements:
- Allow JSON output data with mime type "application/*+json"
- Allow JSON output data with mime type ``application/*+json``
- Allow kernelspecs to have spaces in them for backward compat
- Allow websocket connections from scripts
- Allow :code:`None` for post_save_hook
@ -65,6 +265,7 @@ See the 4.3 milestone on GitHub for a complete list of
`issues <https://github.com/jupyter/notebook/issues?utf8=%E2%9C%93&q=is%3Aissue%20milestone%3A4.3%20>`__
and `pull requests <https://github.com/jupyter/notebook/pulls?utf8=%E2%9C%93&q=is%3Apr%20milestone%3A4.3%20>`__ involved in this release.
.. _release-4.2.3:
4.2.3

@ -30,6 +30,8 @@ for item in sys.path:
# add repo root to sys.path
# here = root/docs/source
here = os.path.abspath(os.path.dirname(__file__))
sphinxext = os.path.join(os.path.dirname(here), 'sphinxext')
sys.path.insert(0, sphinxext)
repo_root = os.path.dirname(os.path.dirname(here))
sys.path.insert(0, repo_root)
@ -70,6 +72,7 @@ extensions = [
'sphinx.ext.mathjax',
'IPython.sphinxext.ipython_console_highlighting',
'nbsphinx',
'github',
]
# Add any paths that contain templates here, relative to this directory.
@ -91,6 +94,9 @@ project = 'Jupyter Notebook'
copyright = '2015, Jupyter Team, https://jupyter.org'
author = 'The Jupyter Team'
# ghissue config
github_project_url = "https://github.com/jupyter/notebook"
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
@ -331,7 +337,7 @@ texinfo_documents = [
#texinfo_no_detailmenu = False
intersphinx_mapping = {
'ipython': ('http://ipython.org/ipython-doc/dev/', None),
'ipython': ('https://ipython.readthedocs.io/en/stable/', None),
'nbconvert': ('https://nbconvert.readthedocs.io/en/latest/', None),
'nbformat': ('https://nbformat.readthedocs.io/en/latest/', None),
'jupyter': ('https://jupyter.readthedocs.io/en/latest/', None),

@ -27,15 +27,16 @@
"\n",
"Jupyter currently has 3 frontends:\n",
"\n",
"* Terminal Console (`ipython console`)\n",
"* Qt Console (`ipython qtconsole`)\n",
"* Notebook (`ipython notebook`)\n",
"* Terminal Console (`jupyter console`)\n",
"* Qt Console (`jupyter qtconsole`)\n",
"* Notebook (`jupyter notebook`)\n",
"\n",
"The Kernel and Frontend communicate over a ZeroMQ/JSON based messaging protocol, which allows multiple Frontends (even of different types) to communicate with a single Kernel. This opens the door for all sorts of interesting things, such as connecting a Console or Qt Console to a Notebook's Kernel. For example, you may want to connect a Qt console to your Notebook's Kernel and use it as a help\n",
"browser, calling `??` on objects in the Qt console (whose pager is more flexible than the\n",
"one in the notebook). \n",
"\n",
"This Notebook describes how you would connect another Frontend to a Kernel that is associated with a Notebook."
"This Notebook describes how you would connect another Frontend to an IPython Kernel that is associated with a Notebook.\n",
"The commands currently given here are specific to the IPython kernel."
]
},
{
@ -55,9 +56,7 @@
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"metadata": {},
"outputs": [],
"source": [
"%connect_info"
@ -88,9 +87,7 @@
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"metadata": {},
"outputs": [],
"source": [
"a = 10"
@ -99,9 +96,7 @@
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"metadata": {},
"outputs": [],
"source": [
"%qtconsole"
@ -124,9 +119,9 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.4.0"
"version": "3.5.2"
}
},
"nbformat": 4,
"nbformat_minor": 0
"nbformat_minor": 1
}

@ -7,6 +7,26 @@
"# Keyboard Shortcut Customization"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Starting with Jupyter Notebook 5.0, you can customize the `command` mode shortcuts from within the Notebook Application itself. \n",
"\n",
"Head to the **`Help`** menu and select the **`Edit keyboard Shortcuts`** item.\n",
"A dialog will guide you through the process of adding custom keyboard shortcuts.\n",
"\n",
"Keyboard shortcut set from within the Notebook Application will be persisted to your configuration file. \n",
"A single action may have several shortcuts attached to it."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Keyboard Shortcut Customization (Pre Notebook 5.0)"
]
},
{
"cell_type": "markdown",
"metadata": {},
@ -17,9 +37,7 @@
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"metadata": {},
"outputs": [],
"source": [
"%%javascript\n",
@ -55,9 +73,7 @@
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"metadata": {},
"outputs": [],
"source": [
"%%javascript\n",
@ -78,9 +94,7 @@
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"metadata": {},
"outputs": [],
"source": [
"%%javascript\n",
@ -107,9 +121,7 @@
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"metadata": {},
"outputs": [],
"source": [
"%%javascript\n",
@ -134,9 +146,9 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.4.3"
"version": "3.5.2"
}
},
"nbformat": 4,
"nbformat_minor": 0
"nbformat_minor": 1
}

@ -344,7 +344,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"So my notebook has a heading cell and some code cells,\n",
"So my notebook has some code cells,\n",
"one of which contains some IPython syntax.\n",
"\n",
"Let's see what happens when we import it"

@ -67,7 +67,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"The above tools can be tedious for editing edit long JavaScript files. Therefore we provide the `%%javascript` magic. This allows you to quickly inject JavaScript into the notebook. Still the javascript injected this way will not survive reloading. Hence, it is a good tool for testing an refining a script.\n",
"The above tools can be tedious for editing edit long JavaScript files. Therefore we provide the `%%javascript` magic. This allows you to quickly inject JavaScript into the notebook. Still the javascript injected this way will not survive reloading. Hence, it is a good tool for testing and refining a script.\n",
"\n",
"You might see here and there people modifying css and injecting js into the notebook by reading file(s) and publishing them into the notebook.\n",
"Not only does this often break the flow of the notebook and make the re-execution of the notebook broken, but it also means that you need to execute those cells in the entire notebook every time you need to update the code.\n",
@ -222,7 +222,7 @@
"source": [
"Create a dropdown element in the toolbar (DOM `Jupyter.toolbar.element`), you will need \n",
"\n",
"- `Jupyter.notebook.set_autosave_interval(miliseconds)`\n",
"- `Jupyter.notebook.set_autosave_interval(milliseconds)`\n",
"- know that 1 min = 60 sec, and 1 sec = 1000 ms"
]
},
@ -343,7 +343,7 @@
"Jupyter.toolbar.add_buttons_group([\n",
" {\n",
" 'label' : 'run qtconsole',\n",
" 'icon' : 'icon-terminal', // select your icon from \n",
" 'icon' : 'fa-terminal', // select your icon from \n",
" // http://fortawesome.github.io/Font-Awesome/icons/\n",
" 'callback': function(){Jupyter.notebook.kernel.execute('%qtconsole')}\n",
" }\n",

@ -20,7 +20,7 @@
"source": [
"When you first start the notebook server, your browser will open to the notebook dashboard. The dashboard serves as a home page for the notebook. Its main purpose is to display the notebooks and files in the current directory. For example, here is a screenshot of the dashboard page for the `examples` directory in the Jupyter repository:\n",
"\n",
"<img src=\"images/dashboard_files_tab.png\" width=\"791px\"/>"
"![Jupyter dashboard showing files tab](images/dashboard_files_tab.png)"
]
},
{
@ -31,7 +31,7 @@
"\n",
"To create a new notebook, click on the \"New\" button at the top of the list and select a kernel from the dropdown (as seen below). Which kernels are listed depend on what's installed on the server. Some of the kernels in the screenshot below may not exist as an option to you.\n",
"\n",
"<img src=\"images/dashboard_files_tab_new.png\" width=\"202px\" />"
"![Jupyter \"New\" menu](images/dashboard_files_tab_new.png)"
]
},
{
@ -42,7 +42,8 @@
"\n",
"The notebook list shows green \"Running\" text and a green notebook icon next to running notebooks (as seen below). Notebooks remain running until you explicitly shut them down; closing the notebook's page is not sufficient.\n",
"\n",
"<img src=\"images/dashboard_files_tab_run.png\" width=\"777px\"/>"
"\n",
"![Jupyter dashboard showing one notebook with a running kernel](images/dashboard_files_tab_run.png)"
]
},
{
@ -51,7 +52,7 @@
"source": [
"To shutdown, delete, duplicate, or rename a notebook check the checkbox next to it and an array of controls will appear at the top of the notebook list (as seen below). You can also use the same operations on directories and files when applicable.\n",
"\n",
"<img src=\"images/dashboard_files_tab_btns.png\" width=\"301px\" />"
"![Buttons: Duplicate, rename, shutdown, delete, new, refresh](images/dashboard_files_tab_btns.png)"
]
},
{
@ -60,7 +61,7 @@
"source": [
"To see all of your running notebooks along with their directories, click on the \"Running\" tab:\n",
"\n",
"<img src=\"images/dashboard_running_tab.png\" width=\"786px\" />\n",
"![Jupyter dashboard running tab](images/dashboard_running_tab.png)\n",
"\n",
"This view provides a convenient way to track notebooks that you start as you navigate the file system in a long running notebook server."
]
@ -112,7 +113,7 @@
"source": [
"Edit mode is indicated by a green cell border and a prompt showing in the editor area:\n",
"\n",
"<img src=\"images/edit_mode.png\">\n",
"![Jupyter cell with green border](images/edit_mode.png)\n",
"\n",
"When a cell is in edit mode, you can type into the cell, like a normal text editor."
]
@ -139,7 +140,7 @@
"source": [
"Command mode is indicated by a grey cell border with a blue left margin:\n",
"\n",
"<img src=\"images/command_mode.png\">\n",
"![Jupyter cell with blue & grey border](images/command_mode.png)\n",
"\n",
"When you are in command mode, you are able to edit the notebook as a whole, but not type into individual cells. Most importantly, in command mode, the keyboard is mapped to a set of shortcuts that let you perform notebook and cell actions efficiently. For example, if you are in command mode and you press `c`, you will copy the current cell - no modifier is needed."
]
@ -175,7 +176,7 @@
"source": [
"All navigation and actions in the Notebook are available using the mouse through the menubar and toolbar, which are both above the main Notebook area:\n",
"\n",
"<img src=\"images/menubar_toolbar.png\" width=\"786px\" />"
"![Jupyter notebook menus and toolbar](images/menubar_toolbar.png)"
]
},
{
@ -193,7 +194,7 @@
"source": [
"The second idea of mouse based navigation is that **cell actions usually apply to the currently selected cell**. Thus if you want to run the code in a cell, you would select it and click the <button class='btn btn-default btn-xs'><i class=\"fa fa-step-forward icon-step-forward\"></i></button> button in the toolbar or the \"Cell:Run\" menu item. Similarly, to copy a cell you would select it and click the <button class='btn btn-default btn-xs'><i class=\"fa fa-copy icon-copy\"></i></button> button in the toolbar or the \"Edit:Copy\" menu item. With this simple pattern, you should be able to do most everything you need with the mouse.\n",
"\n",
"Markdown and heading cells have one other state that can be modified with the mouse. These cells can either be rendered or unrendered. When they are rendered, you will see a nice formatted representation of the cell's contents. When they are unrendered, you will see the raw text source of the cell. To render the selected cell with the mouse, click the <button class='btn btn-default btn-xs'><i class=\"fa fa-step-forward icon-step-forward\"></i></button> button in the toolbar or the \"Cell:Run\" menu item. To unrender the selected cell, double click on the cell."
"Markdown cells have one other state that can be modified with the mouse. These cells can either be rendered or unrendered. When they are rendered, you will see a nice formatted representation of the cell's contents. When they are unrendered, you will see the raw text source of the cell. To render the selected cell with the mouse, click the <button class='btn btn-default btn-xs'><i class=\"fa fa-step-forward icon-step-forward\"></i></button> button in the toolbar or the \"Cell:Run\" menu item. To unrender the selected cell, double click on the cell."
]
},
{
@ -245,9 +246,9 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.5.1"
"version": "3.5.2"
}
},
"nbformat": 4,
"nbformat_minor": 0
"nbformat_minor": 1
}

@ -79,7 +79,6 @@
"* See the results of computations with **rich media representations**, such as HTML, LaTeX, PNG, SVG, PDF, etc.\n",
"* Create and use **interactive JavaScript widgets**, which bind interactive user interface controls and visualizations to reactive kernel side computations.\n",
"* Author **narrative text** using the [Markdown](https://daringfireball.net/projects/markdown/) markup language.\n",
"* Build **hierarchical documents** that are organized into sections with different levels of headings.\n",
"* Include mathematical equations using **LaTeX syntax in Markdown**, which are rendered in-browser by [MathJax](http://www.mathjax.org/)."
]
},
@ -111,7 +110,7 @@
"\n",
"The default kernel runs Python code. The notebook provides a simple way for users to pick which of these kernels is used for a given notebook. \n",
"\n",
"Each of these kernels communicate with the notebook web application and web browser using a JSON over ZeroMQ/WebSockets message protocol that is described [here](http://ipython.org/ipython-doc/dev/development/messaging.html). Most users don't need to know about these details, but it helps to understand that \"kernels run code.\""
"Each of these kernels communicate with the notebook web application and web browser using a JSON over ZeroMQ/WebSockets message protocol that is described [here](https://jupyter-client.readthedocs.io/en/latest/messaging.html#messaging). Most users don't need to know about these details, but it helps to understand that \"kernels run code.\""
]
},
{

@ -189,17 +189,13 @@
"source": [
"The Notebook webapp supports Github flavored markdown meaning that you can use triple backticks for code blocks:\n",
"\n",
" <pre>\n",
" ```python\n",
" print \"Hello World\"\n",
" ```\n",
" </pre>\n",
"\n",
" <pre>\n",
" ```javascript\n",
" console.log(\"Hello World\")\n",
" ```\n",
" </pre>\n",
"\n",
"Gives:\n",
"\n",
@ -213,16 +209,10 @@
"\n",
"And a table like this: \n",
"\n",
" <pre>\n",
" ```\n",
"\n",
" | This | is |\n",
" |------|------|\n",
" | a | table| \n",
"\n",
" ```\n",
" </pre>\n",
"\n",
"A nice HTML Table:\n",
"\n",
"| This | is |\n",
@ -278,7 +268,7 @@
"\n",
" <img src=\"../images/python_logo.svg\" />\n",
"\n",
"<img src=\"images/python_logo.svg\" />\n",
"<img src=\"../images/python_logo.svg\" />\n",
"\n",
"and a video with the HTML5 video tag:\n",
"\n",
@ -328,7 +318,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.5.2"
"version": "3.6.0"
}
},
"nbformat": 4,

@ -167,7 +167,7 @@ alert, and adds a toolabr button using the full action name:
var prefix = 'my_extension';
var action_name = 'show-alert';
var full_action_name = Jupyter.actions.register(action, name, prefix); // returns 'my_extension:show-alert'
var full_action_name = Jupyter.actions.register(action, action_name, prefix); // returns 'my_extension:show-alert'
Jupyter.toolbar.add_buttons_group([full_action_name]);
}
@ -263,7 +263,7 @@ effect to changing a kernel in the notebook. As it is impossible to "unload"
JavaScript, any attempt to change the kernelspec again will save the current
notebook and reload the page without confirmations.
Here is an example of ``kernel.js``::
Here is an example of ``kernel.js``:
.. code:: javascript

@ -31,33 +31,33 @@ when the extension is loaded.
To get the notebook server to load your custom extension, you'll need to
add it to the list of extensions to be loaded. You can do this using the
config system. ``NotebookApp.server_extensions`` is a config variable
which is an array of strings, each a Python module to be imported.
config system. ``NotebookApp.nbserver_extensions`` is a config variable
which is a dictionary of strings, each a Python module to be imported, mapping
to ``True`` to enable or ``False`` to disable each extension.
Because this variable is notebook config, you can set it two different
ways, using config files or via the command line.
For example, to get your extension to load via the command line add a
double dash before the variable name, and put the Python array in
double dash before the variable name, and put the Python dictionary in
double quotes. If your package is "mypackage" and module is
"mymodule", this would look like
``jupyter notebook --NotebookApp.server_extensions="['mypackage.mymodule']"``
``jupyter notebook --NotebookApp.nbserver_extensions="{'mypackage.mymodule':True}"``
.
Basically the string should be Python importable.
Alternatively, you can have your extension loaded regardless of the
command line args by setting the variable in the Jupyter config file.
The default location of the Jupyter config file is
``~/.jupyter/profile_default/jupyter_notebook_config.py``. Then, inside
``~/.jupyter/jupyter_notebook_config.py`` (see :doc:`/config_overview`). Inside
the config file, you can use Python to set the variable. For example,
the following config does the same as the previous command line example
[1].
the following config does the same as the previous command line example.
.. code:: python
c = get_config()
c.NotebookApp.server_extensions = [
'mypackage.mymodule'
]
c.NotebookApp.nbserver_extensions = {
'mypackage.mymodule': True,
}
Before continuing, it's a good idea to verify that your extension is
being loaded. Use a print statement to print something unique. Launch

@ -61,7 +61,7 @@ four spaces. Enter the following code snippet in your JavaScript console::
var config = cell.config;
var patch = {
CodeCell:{
cm_config:{indentUnit: null} # only change here.
cm_config:{indentUnit: null} // only change here.
}
}
config.update(patch)

@ -19,10 +19,10 @@
.. Main Jupyter notebook links
.. _Notebook Basics: notebook_p2_
.. _notebook_p2: https://nbviewer.jupyter.org/urls/raw.github.com/ipython/ipython/3.x/examples/Notebook/Notebook%20Basics.ipynb
.. _notebook_p2: https://nbviewer.jupyter.org/github/jupyter/notebook/blob/master/docs/source/examples/Notebook/Notebook%20Basics.ipynb
.. _Running Code in the Jupyter Notebook: notebook_p1_
.. _notebook_p1: https://nbviewer.jupyter.org/urls/raw.github.com/ipython/ipython/3.x/examples/Notebook/Running%20Code.ipynb
.. _notebook_p1: https://nbviewer.jupyter.org/github/jupyter/notebook/blob/master/docs/source/examples/Notebook/Running%20Code.ipynb
.. Other python projects
.. _matplotlib: http://matplotlib.org
@ -33,7 +33,9 @@
.. _Markdown: http://daringfireball.net/projects/markdown/syntax
.. _Rich Output: notebook_p5_
.. _notebook_p5: https://nbviewer.jupyter.org/urls/raw.github.com/ipython/ipython/3.x/examples/IPython%20Kernel/Rich%20Output.ipynb
.. _notebook_p5: https://nbviewer.jupyter.org/github/ipython/ipython/blob/master/examples/IPython%20Kernel/Rich%20Output.ipynb
.. _Plotting with Matplotlib: notebook_p3_
.. _notebook_p3: https://nbviewer.jupyter.org/urls/raw.github.com/ipython/ipython/3.x/examples/IPython%20Kernel/Plotting%20in%20the%20Notebook.ipynb
.. _notebook_p3: https://nbviewer.jupyter.org/github/ipython/ipython/blob/master/examples/IPython%20Kernel/Plotting%20in%20the%20Notebook.ipynb
.. _Working with Markdown Cells: https://nbviewer.jupyter.org/github/jupyter/notebook/blob/master/docs/source/examples/Notebook/Working%20With%20Markdown%20Cells.ipynb

@ -136,34 +136,38 @@ the dashboard.
Opening notebooks
~~~~~~~~~~~~~~~~~
An open notebook has **exactly one** interactive session connected to an
:ref:`IPython kernel <ipythonzmq>`, which will execute code sent by the user
An open notebook has **exactly one** interactive session connected to a
kernel, which will execute code sent by the user
and communicate back results. This kernel remains active if the web browser
window is closed, and reopening the same notebook from the dashboard will
reconnect the web application to the same kernel. In the dashboard, notebooks
with an active kernel have a ``Shutdown`` button next to them, whereas
notebooks without an active kernel have a ``Delete`` button in its place.
Other clients may connect to the same underlying IPython kernel.
The notebook server always prints to the terminal the full details of
how to connect to each kernel, with messages such as the following::
Other clients may connect to the same kernel.
When each kernel is started, the notebook server prints to the terminal a
message like this::
[NotebookApp] Kernel started: 87f7d2c0-13e3-43df-8bb8-1bd37aaf3373
This long string is the kernel's ID which is sufficient for getting the
information necessary to connect to the kernel. You can also request this
information necessary to connect to the kernel. If the notebook uses the IPython
kernel, you can also see this
connection data by running the ``%connect_info`` :ref:`magic
<magics_explained>`. This will print the same ID information as well as the
content of the JSON data structure it contains.
<magics_explained>`, which will print the same ID information along with other
details.
You can then, for example, manually start a Qt console connected to the *same*
kernel from the command line, by passing a portion of the ID::
$ ipython qtconsole --existing 87f7d2c0
$ jupyter qtconsole --existing 87f7d2c0
Without an ID, ``--existing`` will connect to the most recently
started kernel. This can also be done by running the ``%qtconsole``
:ref:`magic <magics_explained>` in the notebook.
started kernel.
With the IPython kernel, you can also run the ``%qtconsole``
:ref:`magic <magics_explained>` in the notebook to open a Qt console connected
to the same kernel.
.. seealso::
@ -204,15 +208,14 @@ operations within the notebook, by clicking on an icon.
Structure of a notebook document
--------------------------------
The notebook consists of a sequence of cells. A cell is a multiline
text input field, and its contents can be executed by using
:kbd:`Shift-Enter`, or by clicking either the "Play" button the toolbar, or
`Cell | Run` in the menu bar. The execution behavior of a cell is determined
the cell's type. There are four types of cells: **code cells**, **markdown
cells**, **raw cells** and **heading cells**. Every cell starts off
being a **code cell**, but its type can be changed by using a drop-down on the
toolbar (which will be "Code", initially), or via :ref:`keyboard shortcuts
<keyboard-shortcuts>`.
The notebook consists of a sequence of cells. A cell is a multiline text input
field, and its contents can be executed by using :kbd:`Shift-Enter`, or by
clicking either the "Play" button the toolbar, or `Cell | Run` in the menu bar.
The execution behavior of a cell is determined the cell's type. There are four
types of cells: **code cells**, **markdown cells**, and **raw cells**. Every
cell starts off being a **code cell**, but its type can be changed by using a
drop-down on the toolbar (which will be "Code", initially), or via
:ref:`keyboard shortcuts <keyboard-shortcuts>`.
For more information on the different things you can do in a notebook,
see the `collection of examples
@ -246,6 +249,11 @@ called *Markdown cells*. The Markdown language provides a simple way to
perform this text markup, that is, to specify which parts of the text should
be emphasized (italics), bold, form lists, etc.
If you want to provide structure for your document, you can use markdown
headings. Markdown headings consist of 1 to 6 hash # signs ``#`` followed by a
space and the title of your section. The markdown heading will be converted
to a clickable link for a section of the notebook. It is also used as a hint
when exporting to other document formats, like PDF.
When a Markdown cell is executed, the Markdown code is converted into
the corresponding formatted rich text. Markdown allows arbitrary HTML code for
@ -270,7 +278,7 @@ the IPython session.
.. seealso::
`Markdown Cells`_ example notebook
`Working with Markdown Cells`_ example notebook
Raw cells
~~~~~~~~~
@ -282,17 +290,6 @@ destination format unmodified. For example, this allows you to type full LaTeX
into a raw cell, which will only be rendered by LaTeX after conversion by
nbconvert.
Heading cells
~~~~~~~~~~~~~
If you want to provide structure for your document, you can use markdown
headings. Markdown headings consist of 1 to 6 hash # signs ``#`` followed by a
space and the title of your section. The markdown heading will be converted
to a clickable link for a section of the notebook. It is also used as a hint
when exporting to other document formats, like PDF.
We recommend using only one markdown header in a cell and limit the cell's
content to the header text. For flexibility of text format conversion, we
suggest placing additional text in the next notebook cell.
Basic workflow
--------------
@ -327,8 +324,6 @@ content of markdown cells have been inserted as comments.
`Notebook Basics`_ example notebook
:ref:`a warning about doing "roundtrip" conversions <note_about_roundtrip>`.
.. _keyboard-shortcuts:
Keyboard shortcuts

@ -53,6 +53,7 @@ configuring the :attr:`NotebookApp.password` setting in
Prerequisite: A notebook configuration file
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Check to see if you have a notebook configuration file,
:file:`jupyter_notebook_config.py`. The default location for this file
is your Jupyter folder in your home directory, ``~/.jupyter``.
@ -66,7 +67,20 @@ using the following command::
Preparing a hashed password
~~~~~~~~~~~~~~~~~~~~~~~~~~~
You can prepare a hashed password using the function
As of notebook version 5.0, you can enter and store a password for your
notebook server with a single command.
:command:`jupyter notebook password` will prompt you for your password
and record the hashed password in your :file:`jupyter_notebook_config.json`.
.. code-block:: bash
$ jupyter notebook password
Enter password: ****
Verify password: ****
[NotebookPasswordApp] Wrote hashed password to /Users/you/.jupyter/jupyter_notebook_config.json
You can prepare a hashed password manually, using the function
:func:`notebook.auth.security.passwd`:
.. code-block:: ipython
@ -275,7 +289,7 @@ with the following configuration setting in
c.NotebookApp.tornado_settings = {
'headers': {
'Content-Security-Policy': "frame-ancestors 'https://mywebsite.example.com' 'self' "
'Content-Security-Policy': "frame-ancestors https://mywebsite.example.com 'self' "
}
}

@ -1,7 +1,90 @@
.. _server_security:
Security in the Jupyter notebook server
=======================================
Since access to the Jupyter notebook server means access to running arbitrary code,
it is important to restrict access to the notebook server.
For this reason, notebook 4.3 introduces token-based authentication that is **on by default**.
.. note::
If you enable a password for your notebook server,
token authentication is not enabled by default,
and the behavior of the notebook server is unchanged from versions earlier than 4.3.
When token authentication is enabled, the notebook uses a token to authenticate requests.
This token can be provided to login to the notebook server in three ways:
- in the ``Authorization`` header, e.g.::
Authorization: token abcdef...
- In a URL parameter, e.g.::
https://my-notebook/tree/?token=abcdef...
- In the password field of the login form that will be shown to you if you are not logged in.
When you start a notebook server with token authentication enabled (default),
a token is generated to use for authentication.
This token is logged to the terminal, so that you can copy/paste the URL into your browser::
[I 11:59:16.597 NotebookApp] The Jupyter Notebook is running at:
http://localhost:8888/?token=c8de56fa4deed24899803e93c227592aef6538f93025fe01
If the notebook server is going to open your browser automatically
(the default, unless ``--no-browser`` has been passed),
an *additional* token is generated for launching the browser.
This additional token can be used only once,
and is used to set a cookie for your browser once it connects.
After your browser has made its first request with this one-time-token,
the token is discarded and a cookie is set in your browser.
At any later time, you can see the tokens and URLs for all of your running servers with :command:`jupyter notebook list`::
$ jupyter notebook list
Currently running servers:
http://localhost:8888/?token=abc... :: /home/you/notebooks
https://0.0.0.0:9999/?token=123... :: /tmp/public
http://localhost:8889/ :: /tmp/has-password
For servers with token-authentication enabled, the URL in the above listing will include the token,
so you can copy and paste that URL into your browser to login.
If a server has no token (e.g. it has a password or has authentication disabled),
the URL will not include the token argument.
Once you have visited this URL,
a cookie will be set in your browser and you won't need to use the token again,
unless you switch browsers, clear your cookies, or start a notebook server on a new port.
Alternatives to token authentication
------------------------------------
If a generated token doesn't work well for you,
you can set a password for your notebook.
:command:`jupyter notebook password` will prompt you for a password,
and store the hashed password in your :file:`jupyter_notebook_config.json`.
.. versionadded:: 5.0
:command:`jupyter notebook password` command is added.
It is possible disable authentication altogether by setting the token and password to empty strings,
but this is **NOT RECOMMENDED**, unless authentication or access restrictions are handled at a different layer in your web application:
.. sourcecode:: python
c.NotebookApp.token = ''
c.NotebookApp.password = ''
.. _notebook_security:
Security in Jupyter notebooks
=============================
Security in notebook documents
==============================
As Jupyter notebooks become more popular for sharing and collaboration,
the potential for malicious people to attempt to exploit the notebook

@ -0,0 +1,157 @@
"""Define text roles for GitHub
* ghissue - Issue
* ghpull - Pull Request
* ghuser - User
Adapted from bitbucket example here:
https://bitbucket.org/birkenfeld/sphinx-contrib/src/tip/bitbucket/sphinxcontrib/bitbucket.py
Authors
-------
* Doug Hellmann
* Min RK
"""
#
# Original Copyright (c) 2010 Doug Hellmann. All rights reserved.
#
from docutils import nodes, utils
from docutils.parsers.rst.roles import set_classes
def make_link_node(rawtext, app, type, slug, options):
"""Create a link to a github resource.
:param rawtext: Text being replaced with link node.
:param app: Sphinx application context
:param type: Link type (issues, changeset, etc.)
:param slug: ID of the thing to link to
:param options: Options dictionary passed to role func.
"""
try:
base = app.config.github_project_url
if not base:
raise AttributeError
if not base.endswith('/'):
base += '/'
except AttributeError as err:
raise ValueError('github_project_url configuration value is not set (%s)' % str(err))
ref = base + type + '/' + slug + '/'
set_classes(options)
prefix = "#"
if type == 'pull':
prefix = "PR " + prefix
node = nodes.reference(rawtext, prefix + utils.unescape(slug), refuri=ref,
**options)
return node
def ghissue_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
"""Link to a GitHub issue.
Returns 2 part tuple containing list of nodes to insert into the
document and a list of system messages. Both are allowed to be
empty.
:param name: The role name used in the document.
:param rawtext: The entire markup snippet, with role.
:param text: The text marked with the role.
:param lineno: The line number where rawtext appears in the input.
:param inliner: The inliner instance that called us.
:param options: Directive options for customization.
:param content: The directive content for customization.
"""
try:
issue_num = int(text)
if issue_num <= 0:
raise ValueError
except ValueError:
msg = inliner.reporter.error(
'GitHub issue number must be a number greater than or equal to 1; '
'"%s" is invalid.' % text, line=lineno)
prb = inliner.problematic(rawtext, rawtext, msg)
return [prb], [msg]
app = inliner.document.settings.env.app
#app.info('issue %r' % text)
if 'pull' in name.lower():
category = 'pull'
elif 'issue' in name.lower():
category = 'issues'
else:
msg = inliner.reporter.error(
'GitHub roles include "ghpull" and "ghissue", '
'"%s" is invalid.' % name, line=lineno)
prb = inliner.problematic(rawtext, rawtext, msg)
return [prb], [msg]
node = make_link_node(rawtext, app, category, str(issue_num), options)
return [node], []
def ghuser_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
"""Link to a GitHub user.
Returns 2 part tuple containing list of nodes to insert into the
document and a list of system messages. Both are allowed to be
empty.
:param name: The role name used in the document.
:param rawtext: The entire markup snippet, with role.
:param text: The text marked with the role.
:param lineno: The line number where rawtext appears in the input.
:param inliner: The inliner instance that called us.
:param options: Directive options for customization.
:param content: The directive content for customization.
"""
app = inliner.document.settings.env.app
#app.info('user link %r' % text)
ref = 'https://www.github.com/' + text
node = nodes.reference(rawtext, text, refuri=ref, **options)
return [node], []
def ghcommit_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
"""Link to a GitHub commit.
Returns 2 part tuple containing list of nodes to insert into the
document and a list of system messages. Both are allowed to be
empty.
:param name: The role name used in the document.
:param rawtext: The entire markup snippet, with role.
:param text: The text marked with the role.
:param lineno: The line number where rawtext appears in the input.
:param inliner: The inliner instance that called us.
:param options: Directive options for customization.
:param content: The directive content for customization.
"""
app = inliner.document.settings.env.app
#app.info('user link %r' % text)
try:
base = app.config.github_project_url
if not base:
raise AttributeError
if not base.endswith('/'):
base += '/'
except AttributeError as err:
raise ValueError('github_project_url configuration value is not set (%s)' % str(err))
ref = base + text
node = nodes.reference(rawtext, text[:6], refuri=ref, **options)
return [node], []
def setup(app):
"""Install the plugin.
:param app: Sphinx application context.
"""
app.info('Initializing GitHub plugin')
app.add_role('ghissue', ghissue_role)
app.add_role('ghpull', ghissue_role)
app.add_role('ghuser', ghuser_role)
app.add_role('ghcommit', ghcommit_role)
app.add_config_value('github_project_url', None, 'env')
metadata = {'parallel_read_safe': True, 'parallel_write_safe': True}
return metadata

@ -46,11 +46,15 @@ def pkg_commit_hash(pkg_path):
while cur_path != par_path:
cur_path = par_path
if p.exists(p.join(cur_path, '.git')):
proc = subprocess.Popen('git rev-parse --short HEAD',
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
cwd=pkg_path, shell=True)
repo_commit, _ = proc.communicate()
try:
proc = subprocess.Popen(['git', 'rev-parse', '--short', 'HEAD'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
cwd=pkg_path)
repo_commit, _ = proc.communicate()
except OSError:
repo_commit = None
if repo_commit:
return 'repository', repo_commit.strip().decode('ascii')
else:

@ -0,0 +1,42 @@
# encoding: utf-8
"""
Timezone utilities
Just UTC-awareness right now
"""
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
from datetime import tzinfo, timedelta, datetime
# constant for zero offset
ZERO = timedelta(0)
class tzUTC(tzinfo):
"""tzinfo object for UTC (zero offset)"""
def utcoffset(self, d):
return ZERO
def dst(self, d):
return ZERO
UTC = tzUTC()
def utc_aware(unaware):
"""decorator for adding UTC tzinfo to datetime's utcfoo methods"""
def utc_method(*args, **kwargs):
dt = unaware(*args, **kwargs)
return dt.replace(tzinfo=UTC)
return utc_method
utcfromtimestamp = utc_aware(datetime.utcfromtimestamp)
utcnow = utc_aware(datetime.utcnow)
def isoformat(dt):
"""Return iso-formatted timestamp
Like .isoformat(), but uses Z for UTC instead of +00:00
"""
return dt.isoformat().replace('+00:00', 'Z')

@ -9,5 +9,5 @@ store the current version info of the notebook.
# Next beta/alpha/rc release: The version number for beta is X.Y.ZbN **without dots**.
version_info = (5, 0, 0, '.dev')
version_info = (5, 1, 0, '.dev')
__version__ = '.'.join(map(str, version_info[:3])) + ''.join(version_info[3:])

@ -1,311 +0,0 @@
"""WebsocketProtocol76 from tornado 3.2.2 for tornado >= 4.0
The contents of this file are Copyright (c) Tornado
Used under the Apache 2.0 license
"""
from __future__ import absolute_import, division, print_function, with_statement
# Author: Jacob Kristhammar, 2010
import functools
import hashlib
import struct
import time
import tornado.escape
import tornado.web
from tornado.log import gen_log, app_log
from tornado.util import bytes_type, unicode_type
from tornado.websocket import WebSocketHandler, WebSocketProtocol13
class AllowDraftWebSocketHandler(WebSocketHandler):
"""Restore Draft76 support for tornado 4
Remove when we can run tests without phantomjs + qt4
"""
# get is unmodified except between the BEGIN/END PATCH lines
@tornado.web.asynchronous
def get(self, *args, **kwargs):
self.open_args = args
self.open_kwargs = kwargs
# Upgrade header should be present and should be equal to WebSocket
if self.request.headers.get("Upgrade", "").lower() != 'websocket':
self.set_status(400)
self.finish("Can \"Upgrade\" only to \"WebSocket\".")
return
# Connection header should be upgrade. Some proxy servers/load balancers
# might mess with it.
headers = self.request.headers
connection = map(lambda s: s.strip().lower(), headers.get("Connection", "").split(","))
if 'upgrade' not in connection:
self.set_status(400)
self.finish("\"Connection\" must be \"Upgrade\".")
return
# Handle WebSocket Origin naming convention differences
# The difference between version 8 and 13 is that in 8 the
# client sends a "Sec-Websocket-Origin" header and in 13 it's
# simply "Origin".
if "Origin" in self.request.headers:
origin = self.request.headers.get("Origin")
else:
origin = self.request.headers.get("Sec-Websocket-Origin", None)
# If there was an origin header, check to make sure it matches
# according to check_origin. When the origin is None, we assume it
# did not come from a browser and that it can be passed on.
if origin is not None and not self.check_origin(origin):
self.set_status(403)
self.finish("Cross origin websockets not allowed")
return
self.stream = self.request.connection.detach()
self.stream.set_close_callback(self.on_connection_close)
if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
self.ws_connection = WebSocketProtocol13(self)
self.ws_connection.accept_connection()
#--------------- BEGIN PATCH ----------------
elif (self.allow_draft76() and
"Sec-WebSocket-Version" not in self.request.headers):
self.ws_connection = WebSocketProtocol76(self)
self.ws_connection.accept_connection()
#--------------- END PATCH ----------------
else:
if not self.stream.closed():
self.stream.write(tornado.escape.utf8(
"HTTP/1.1 426 Upgrade Required\r\n"
"Sec-WebSocket-Version: 8\r\n\r\n"))
self.stream.close()
# 3.2 methods removed in 4.0:
def allow_draft76(self):
"""Using this class allows draft76 connections by default"""
return True
def get_websocket_scheme(self):
"""Return the url scheme used for this request, either "ws" or "wss".
This is normally decided by HTTPServer, but applications
may wish to override this if they are using an SSL proxy
that does not provide the X-Scheme header as understood
by HTTPServer.
Note that this is only used by the draft76 protocol.
"""
return "wss" if self.request.protocol == "https" else "ws"
# No modifications from tornado-3.2.2 below this line
class WebSocketProtocol(object):
"""Base class for WebSocket protocol versions.
"""
def __init__(self, handler):
self.handler = handler
self.request = handler.request
self.stream = handler.stream
self.client_terminated = False
self.server_terminated = False
def async_callback(self, callback, *args, **kwargs):
"""Wrap callbacks with this if they are used on asynchronous requests.
Catches exceptions properly and closes this WebSocket if an exception
is uncaught.
"""
if args or kwargs:
callback = functools.partial(callback, *args, **kwargs)
def wrapper(*args, **kwargs):
try:
return callback(*args, **kwargs)
except Exception:
app_log.error("Uncaught exception in %s",
self.request.path, exc_info=True)
self._abort()
return wrapper
def on_connection_close(self):
self._abort()
def _abort(self):
"""Instantly aborts the WebSocket connection by closing the socket"""
self.client_terminated = True
self.server_terminated = True
self.stream.close() # forcibly tear down the connection
self.close() # let the subclass cleanup
class WebSocketProtocol76(WebSocketProtocol):
"""Implementation of the WebSockets protocol, version hixie-76.
This class provides basic functionality to process WebSockets requests as
specified in
http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
"""
def __init__(self, handler):
WebSocketProtocol.__init__(self, handler)
self.challenge = None
self._waiting = None
def accept_connection(self):
try:
self._handle_websocket_headers()
except ValueError:
gen_log.debug("Malformed WebSocket request received")
self._abort()
return
scheme = self.handler.get_websocket_scheme()
# draft76 only allows a single subprotocol
subprotocol_header = ''
subprotocol = self.request.headers.get("Sec-WebSocket-Protocol", None)
if subprotocol:
selected = self.handler.select_subprotocol([subprotocol])
if selected:
assert selected == subprotocol
subprotocol_header = "Sec-WebSocket-Protocol: %s\r\n" % selected
# Write the initial headers before attempting to read the challenge.
# This is necessary when using proxies (such as HAProxy), which
# need to see the Upgrade headers before passing through the
# non-HTTP traffic that follows.
self.stream.write(tornado.escape.utf8(
"HTTP/1.1 101 WebSocket Protocol Handshake\r\n"
"Upgrade: WebSocket\r\n"
"Connection: Upgrade\r\n"
"Server: TornadoServer/%(version)s\r\n"
"Sec-WebSocket-Origin: %(origin)s\r\n"
"Sec-WebSocket-Location: %(scheme)s://%(host)s%(uri)s\r\n"
"%(subprotocol)s"
"\r\n" % (dict(
version=tornado.version,
origin=self.request.headers["Origin"],
scheme=scheme,
host=self.request.host,
uri=self.request.uri,
subprotocol=subprotocol_header))))
self.stream.read_bytes(8, self._handle_challenge)
def challenge_response(self, challenge):
"""Generates the challenge response that's needed in the handshake
The challenge parameter should be the raw bytes as sent from the
client.
"""
key_1 = self.request.headers.get("Sec-Websocket-Key1")
key_2 = self.request.headers.get("Sec-Websocket-Key2")
try:
part_1 = self._calculate_part(key_1)
part_2 = self._calculate_part(key_2)
except ValueError:
raise ValueError("Invalid Keys/Challenge")
return self._generate_challenge_response(part_1, part_2, challenge)
def _handle_challenge(self, challenge):
try:
challenge_response = self.challenge_response(challenge)
except ValueError:
gen_log.debug("Malformed key data in WebSocket request")
self._abort()
return
self._write_response(challenge_response)
def _write_response(self, challenge):
self.stream.write(challenge)
self.async_callback(self.handler.open)(*self.handler.open_args, **self.handler.open_kwargs)
self._receive_message()
def _handle_websocket_headers(self):
"""Verifies all invariant- and required headers
If a header is missing or have an incorrect value ValueError will be
raised
"""
fields = ("Origin", "Host", "Sec-Websocket-Key1",
"Sec-Websocket-Key2")
if not all(map(lambda f: self.request.headers.get(f), fields)):
raise ValueError("Missing/Invalid WebSocket headers")
def _calculate_part(self, key):
"""Processes the key headers and calculates their key value.
Raises ValueError when feed invalid key."""
# pyflakes complains about variable reuse if both of these lines use 'c'
number = int(''.join(c for c in key if c.isdigit()))
spaces = len([c2 for c2 in key if c2.isspace()])
try:
key_number = number // spaces
except (ValueError, ZeroDivisionError):
raise ValueError
return struct.pack(">I", key_number)
def _generate_challenge_response(self, part_1, part_2, part_3):
m = hashlib.md5()
m.update(part_1)
m.update(part_2)
m.update(part_3)
return m.digest()
def _receive_message(self):
self.stream.read_bytes(1, self._on_frame_type)
def _on_frame_type(self, byte):
frame_type = ord(byte)
if frame_type == 0x00:
self.stream.read_until(b"\xff", self._on_end_delimiter)
elif frame_type == 0xff:
self.stream.read_bytes(1, self._on_length_indicator)
else:
self._abort()
def _on_end_delimiter(self, frame):
if not self.client_terminated:
self.async_callback(self.handler.on_message)(
frame[:-1].decode("utf-8", "replace"))
if not self.client_terminated:
self._receive_message()
def _on_length_indicator(self, byte):
if ord(byte) != 0x00:
self._abort()
return
self.client_terminated = True
self.close()
def write_message(self, message, binary=False):
"""Sends the given message to the client of this Web Socket."""
if binary:
raise ValueError(
"Binary messages not supported by this version of websockets")
if isinstance(message, unicode_type):
message = message.encode("utf-8")
assert isinstance(message, bytes_type)
self.stream.write(b"\x00" + message + b"\xff")
def write_ping(self, data):
"""Send ping frame."""
raise ValueError("Ping messages not supported by this version of websockets")
def close(self):
"""Closes the WebSocket connection."""
if not self.server_terminated:
if not self.stream.closed():
self.stream.write("\xff\x00")
self.server_terminated = True
if self.client_terminated:
if self._waiting is not None:
self.stream.io_loop.remove_timeout(self._waiting)
self._waiting = None
self.stream.close()
elif self._waiting is None:
self._waiting = self.stream.io_loop.add_timeout(
time.time() + 5, self._abort)

@ -52,7 +52,7 @@ class LoginHandler(IPythonHandler):
allow = bool(self.allow_origin_pat.match(origin))
if not allow:
# not allowed, use default
self.log.warn("Not allowing login redirect to %r" % url)
self.log.warning("Not allowing login redirect to %r" % url)
url = default
self.redirect(url)
@ -67,10 +67,13 @@ class LoginHandler(IPythonHandler):
def hashed_password(self):
return self.password_from_settings(self.settings)
def passwd_check(self, a, b):
return passwd_check(a, b)
def post(self):
typed_password = self.get_argument('password', default=u'')
if self.get_login_available(self.settings):
if passwd_check(self.hashed_password, typed_password):
if self.passwd_check(self.hashed_password, typed_password):
self.set_login_cookie(self, uuid.uuid4().hex)
elif self.token and self.token == typed_password:
self.set_login_cookie(self, uuid.uuid4().hex)
@ -97,7 +100,7 @@ class LoginHandler(IPythonHandler):
auth_header_pat = re.compile('token\s+(.+)', re.IGNORECASE)
@classmethod
def get_user_token(cls, handler):
def get_token(cls, handler):
"""Get the user token from a request
Default:
@ -117,14 +120,29 @@ class LoginHandler(IPythonHandler):
@classmethod
def should_check_origin(cls, handler):
"""Should the Handler check for CORS origin validation?
Origin check should be skipped for token-authenticated requests.
Returns:
- True, if Handler must check for valid CORS origin.
- False, if Handler should skip origin check since requests are token-authenticated.
"""
return not cls.is_token_authenticated(handler)
@classmethod
def is_token_authenticated(cls, handler):
"""Returns True if handler has been token authenticated. Otherwise, False.
Login with a token is used to signal certain things, such as:
- permit access to REST API
- xsrf protection
- skip origin-checks for scripts
"""
if getattr(handler, '_user_id', None) is None:
# ensure get_user has been called, so we know if we're token-authenticated
handler.get_current_user()
token_authenticated = getattr(handler, '_token_authenticated', False)
return not token_authenticated
return getattr(handler, '_token_authenticated', False)
@classmethod
def get_user(cls, handler):
@ -136,40 +154,56 @@ class LoginHandler(IPythonHandler):
# called on LoginHandler itself.
if getattr(handler, '_user_id', None):
return handler._user_id
user_id = handler.get_secure_cookie(handler.cookie_name)
if not user_id:
user_id = cls.get_user_token(handler)
if user_id is None:
user_id = handler.get_secure_cookie(handler.cookie_name)
else:
cls.set_login_cookie(handler, user_id)
# Record that the current request has been authenticated with a token.
# Used in is_token_authenticated above.
handler._token_authenticated = True
if user_id is None:
# prevent extra Invalid cookie sig warnings:
handler.clear_login_cookie()
token = handler.token
if not token and not handler.login_available:
if not handler.login_available:
# Completely insecure! No authentication at all.
# No need to warn here, though; validate_security will have already done that.
return 'anonymous'
if token:
# check login token from URL argument or Authorization header
user_token = cls.get_user_token(handler)
one_time_token = handler.one_time_token
authenticated = False
if user_token == token:
# token-authenticated, set the login cookie
handler.log.info("Accepting token-authenticated connection from %s", handler.request.remote_ip)
authenticated = True
elif one_time_token and user_token == one_time_token:
# one-time-token-authenticated, only allow this token once
handler.settings.pop('one_time_token', None)
handler.log.info("Accepting one-time-token-authenticated connection from %s", handler.request.remote_ip)
authenticated = True
if authenticated:
user_id = uuid.uuid4().hex
cls.set_login_cookie(handler, user_id)
# Record that we've been authenticated with a token.
# Used in should_check_origin above.
handler._token_authenticated = True
user_id = 'anonymous'
# cache value for future retrievals on the same request
handler._user_id = user_id
return user_id
@classmethod
def get_user_token(cls, handler):
"""Identify the user based on a token in the URL or Authorization header
Returns:
- uuid if authenticated
- None if not
"""
token = handler.token
if not token:
return
# check login token from URL argument or Authorization header
user_token = cls.get_token(handler)
one_time_token = handler.one_time_token
authenticated = False
if user_token == token:
# token-authenticated, set the login cookie
handler.log.debug("Accepting token-authenticated connection from %s", handler.request.remote_ip)
authenticated = True
elif one_time_token and user_token == one_time_token:
# one-time-token-authenticated, only allow this token once
handler.settings.pop('one_time_token', None)
handler.log.info("Accepting one-time-token-authenticated connection from %s", handler.request.remote_ip)
authenticated = True
if authenticated:
return uuid.uuid4().hex
else:
return None
@classmethod
def validate_security(cls, app, ssl_options=None):

@ -1,11 +1,20 @@
"""
Password generation for the Notebook.
"""
from contextlib import contextmanager
import getpass
import hashlib
import io
import json
import os
import random
import traceback
import warnings
from ipython_genutils.py3compat import cast_bytes, str_to_bytes
from ipython_genutils.py3compat import cast_bytes, str_to_bytes, cast_unicode
from traitlets.config import Config, ConfigFileNotFound, JSONFileConfigLoader
from jupyter_core.paths import jupyter_config_dir
# Length of the salt in nr of hex chars, which implies salt_len * 4
# bits of randomness.
@ -99,3 +108,41 @@ def passwd_check(hashed_passphrase, passphrase):
h.update(cast_bytes(passphrase, 'utf-8') + cast_bytes(salt, 'ascii'))
return h.hexdigest() == pw_digest
@contextmanager
def persist_config(config_file=None, mode=0o600):
"""Context manager that can be used to modify a config object
On exit of the context manager, the config will be written back to disk,
by default with user-only (600) permissions.
"""
if config_file is None:
config_file = os.path.join(jupyter_config_dir(), 'jupyter_notebook_config.json')
loader = JSONFileConfigLoader(os.path.basename(config_file), os.path.dirname(config_file))
try:
config = loader.load_config()
except ConfigFileNotFound:
config = Config()
yield config
with io.open(config_file, 'w', encoding='utf8') as f:
f.write(cast_unicode(json.dumps(config, indent=2)))
try:
os.chmod(config_file, mode)
except Exception as e:
tb = traceback.format_exc()
warnings.warn("Failed to set permissions on %s:\n%s" % (config_file, tb),
RuntimeWarning)
def set_password(password=None, config_file=None):
"""Ask user for password, store it in notebook json configuration file"""
hashed_password = passwd(password)
with persist_config(config_file) as config:
config.NotebookApp.password = hashed_password

@ -5,6 +5,7 @@
import functools
import json
import mimetypes
import os
import re
import sys
@ -30,6 +31,7 @@ from ipython_genutils.path import filefind
from ipython_genutils.py3compat import string_types
import notebook
from notebook._tz import utcnow
from notebook.utils import is_hidden, url_path_join, url_is_absolute, url_escape
from notebook.services.security import csp_report_uri
@ -38,7 +40,12 @@ from notebook.services.security import csp_report_uri
#-----------------------------------------------------------------------------
non_alphanum = re.compile(r'[^A-Za-z0-9]')
sys_info = json.dumps(get_sys_info())
_sys_info_cache = None
def json_sys_info():
global _sys_info_cache
if _sys_info_cache is None:
_sys_info_cache = json.dumps(get_sys_info())
return _sys_info_cache
def log():
if Application.initialized():
@ -48,27 +55,31 @@ def log():
class AuthenticatedHandler(web.RequestHandler):
"""A RequestHandler with an authenticated user."""
@property
def content_security_policy(self):
"""The default Content-Security-Policy header
Can be overridden by defining Content-Security-Policy in settings['headers']
"""
if 'Content-Security-Policy' in self.settings.get('headers', {}):
# user-specified, don't override
return self.settings['headers']['Content-Security-Policy']
return '; '.join([
"frame-ancestors 'self'",
# Make sure the report-uri is relative to the base_url
"report-uri " + url_path_join(self.base_url, csp_report_uri),
"report-uri " + self.settings.get('csp_report_uri', url_path_join(self.base_url, csp_report_uri)),
])
def set_default_headers(self):
headers = self.settings.get('headers', {})
headers = {}
headers.update(self.settings.get('headers', {}))
if "Content-Security-Policy" not in headers:
headers["Content-Security-Policy"] = self.content_security_policy
headers["Content-Security-Policy"] = self.content_security_policy
# Allow for overriding headers
for header_name,value in headers.items() :
for header_name, value in headers.items():
try:
self.set_header(header_name, value)
except Exception as e:
@ -95,6 +106,13 @@ class AuthenticatedHandler(web.RequestHandler):
return False
return not self.login_handler.should_check_origin(self)
@property
def token_authenticated(self):
"""Have I been authenticated with a token?"""
if self.login_handler is None or not hasattr(self.login_handler, 'is_token_authenticated'):
return False
return self.login_handler.is_token_authenticated(self)
@property
def cookie_name(self):
default_cookie_name = non_alphanum.sub('-', 'username-{}'.format(
@ -199,8 +217,8 @@ class IPythonHandler(AuthenticatedHandler):
@property
def contents_js_source(self):
self.log.debug("Using contents: %s", self.settings.get('contents_js_source',
'services/built/contents'))
return self.settings.get('contents_js_source', 'services/built/contents')
'services/contents'))
return self.settings.get('contents_js_source', 'services/contents')
#---------------------------------------------------------------
# Manager objects
@ -288,8 +306,12 @@ class IPythonHandler(AuthenticatedHandler):
host = self.request.headers.get("Host")
origin = self.request.headers.get("Origin")
# If no header is provided, assume it comes from a script/curl.
# We are only concerned with cross-site browser stuff here.
# If no header is provided, let the request through.
# Origin can be None for:
# - same-origin (IE, Firefox)
# - Cross-site POST form (IE, Firefox)
# - Scripts
# The cross-site POST (XSRF) case is handled by tornado's xsrf_token
if origin is None or host is None:
return True
@ -313,7 +335,15 @@ class IPythonHandler(AuthenticatedHandler):
self.request.path, origin, host,
)
return allow
def check_xsrf_cookie(self):
"""Bypass xsrf cookie checks when token-authenticated"""
if self.token_authenticated or self.settings.get('disable_check_xsrf', False):
# Token-authenticated requests do not need additional XSRF-check
# Servers without authentication are vulnerable to XSRF
return
return super(IPythonHandler, self).check_xsrf_cookie()
#---------------------------------------------------------------
# template rendering
#---------------------------------------------------------------
@ -337,9 +367,13 @@ class IPythonHandler(AuthenticatedHandler):
login_available=self.login_available,
token_available=bool(self.token or self.one_time_token),
static_url=self.static_url,
sys_info=sys_info,
sys_info=json_sys_info(),
contents_js_source=self.contents_js_source,
version_hash=self.version_hash,
ignore_minified_js=self.ignore_minified_js,
xsrf_form_html=self.xsrf_form_html,
token=self.token,
xsrf_token=self.xsrf_token.decode('utf8'),
**self.jinja_template_vars
)
@ -383,15 +417,14 @@ class IPythonHandler(AuthenticatedHandler):
message=message,
exception=exception,
)
self.set_header('Content-Type', 'text/html')
# render the template
try:
html = self.render_template('%s.html' % status_code, **ns)
except TemplateNotFound:
self.log.debug("No template for %d", status_code)
html = self.render_template('error.html', **ns)
self.write(html)
@ -410,13 +443,23 @@ class APIHandler(IPythonHandler):
"default-src 'none'",
])
return csp
# set _track_activity = False on API handlers that shouldn't track activity
_track_activity = True
def update_api_activity(self):
"""Update last_activity of API requests"""
# record activity of authenticated requests
if self._track_activity and self.get_current_user():
self.settings['api_last_activity'] = utcnow()
def finish(self, *args, **kwargs):
self.update_api_activity()
self.set_header('Content-Type', 'application/json')
return super(APIHandler, self).finish(*args, **kwargs)
def options(self, *args, **kwargs):
self.set_header('Access-Control-Allow-Headers', 'accept, content-type')
self.set_header('Access-Control-Allow-Headers', 'accept, content-type, authorization')
self.set_header('Access-Control-Allow-Methods',
'GET, PUT, POST, PATCH, DELETE, OPTIONS')
self.finish()
@ -433,13 +476,27 @@ class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
@web.authenticated
def get(self, path):
if os.path.splitext(path)[1] == '.ipynb':
if os.path.splitext(path)[1] == '.ipynb' or self.get_argument("download", False):
name = path.rsplit('/', 1)[-1]
self.set_header('Content-Type', 'application/json')
self.set_header('Content-Disposition','attachment; filename="%s"' % escape.url_escape(name))
return web.StaticFileHandler.get(self, path)
def get_content_type(self):
path = self.absolute_path.strip('/')
if '/' in path:
_, name = path.rsplit('/', 1)
else:
name = path
if name.endswith('.ipynb'):
return 'application/x-ipynb+json'
else:
cur_mime = mimetypes.guess_type(name)[0]
if cur_mime == 'text/plain':
return 'text/plain; charset=UTF-8'
else:
return super(AuthenticatedFileHandler, self).get_content_type()
def set_headers(self):
super(AuthenticatedFileHandler, self).set_headers()
# disable browser caching, rely on 304 replies for savings
@ -651,5 +708,6 @@ path_regex = r"(?P<path>(?:(?:/[^/]+)+|/?))"
default_handlers = [
(r".*/", TrailingSlashHandler),
(r"api", APIVersionHandler)
(r"api", APIVersionHandler),
(r'/(robots\.txt|favicon\.ico)', web.StaticFileHandler),
]

@ -87,14 +87,6 @@ def deserialize_binary_message(bmsg):
# ping interval for keeping websockets alive (30 seconds)
WS_PING_INTERVAL = 30000
if os.environ.get('IPYTHON_ALLOW_DRAFT_WEBSOCKETS_FOR_PHANTOMJS', False):
warnings.warn("""Allowing draft76 websocket connections!
This should only be done for testing with phantomjs!""")
from notebook import allow76
WebSocketHandler = allow76.AllowDraftWebSocketHandler
# draft 76 doesn't support ping
WS_PING_INTERVAL = 0
class WebSocketMixin(object):
"""Mixin for common websocket options"""
@ -296,5 +288,4 @@ class AuthenticatedZMQStreamHandler(ZMQStreamHandler, IPythonHandler):
self.session = Session(config=self.config)
def get_compression_options(self):
# use deflate compress websocket
return {}
return self.settings.get('websocket_compression_options', None)

@ -4,10 +4,8 @@
# Distributed under the terms of the Modified BSD License.
import io
import requests
from os.path import join as pjoin
from notebook.utils import url_path_join
from notebook.tests.launchnotebook import NotebookTestBase
from nbformat import write
from nbformat.v4 import (
@ -30,7 +28,7 @@ class BundleAPITest(NotebookTestBase):
"""Make a test notebook. Borrowed from nbconvert test. Assumes the class
teardown will clean it up in the end."""
super(BundleAPITest, cls).setup_class()
nbdir = cls.notebook_dir.name
nbdir = cls.notebook_dir
nb = new_notebook()
@ -45,20 +43,20 @@ class BundleAPITest(NotebookTestBase):
def test_missing_bundler_arg(self):
"""Should respond with 400 error about missing bundler arg"""
resp = requests.get(url_path_join(self.base_url(), 'bundle', 'fake.ipynb'))
resp = self.request('GET', 'bundle/fake.ipynb')
self.assertEqual(resp.status_code, 400)
self.assertIn('Missing argument bundler', resp.text)
def test_notebook_not_found(self):
"""Shoudl respond with 404 error about missing notebook"""
resp = requests.get(url_path_join(self.base_url(), 'bundle', 'fake.ipynb'),
resp = self.request('GET', 'bundle/fake.ipynb',
params={'bundler': 'fake_bundler'})
self.assertEqual(resp.status_code, 404)
self.assertIn('Not Found', resp.text)
def test_bundler_not_enabled(self):
"""Should respond with 400 error about disabled bundler"""
resp = requests.get(url_path_join(self.base_url(), 'bundle', 'testnb.ipynb'),
resp = self.request('GET', 'bundle/testnb.ipynb',
params={'bundler': 'fake_bundler'})
self.assertEqual(resp.status_code, 400)
self.assertIn('Bundler fake_bundler not enabled', resp.text)
@ -67,7 +65,7 @@ class BundleAPITest(NotebookTestBase):
"""Should respond with 500 error about failure to load bundler module"""
with patch('notebook.bundler.handlers.BundlerHandler.get_bundler') as mock:
mock.return_value = {'module_name': 'fake_module'}
resp = requests.get(url_path_join(self.base_url(), 'bundle', 'testnb.ipynb'),
resp = self.request('GET', 'bundle/testnb.ipynb',
params={'bundler': 'fake_bundler'})
mock.assert_called_with('fake_bundler')
self.assertEqual(resp.status_code, 500)
@ -77,7 +75,7 @@ class BundleAPITest(NotebookTestBase):
"""Should respond with 200 and output from test bundler stub"""
with patch('notebook.bundler.handlers.BundlerHandler.get_bundler') as mock:
mock.return_value = {'module_name': 'notebook.bundler.tests.test_bundler_api'}
resp = requests.get(url_path_join(self.base_url(), 'bundle', 'testnb.ipynb'),
resp = self.request('GET', 'bundle/testnb.ipynb',
params={'bundler': 'stub_bundler'})
mock.assert_called_with('stub_bundler')
self.assertEqual(resp.status_code, 200)

@ -26,6 +26,13 @@ class FilesHandler(IPythonHandler):
@web.authenticated
def get(self, path, include_body=True):
cm = self.contents_manager
if cm.files_handler_class:
return cm.files_handler_class(self.application, self.request, path=cm.root_dir)._execute(
[t(self.request) for t in self.application.transforms],
path
)
if cm.is_hidden(path):
self.log.info("Refusing to serve hidden file, via 404 Error")
raise web.HTTPError(404)
@ -46,13 +53,15 @@ class FilesHandler(IPythonHandler):
self.set_header('Content-Type', 'application/x-ipynb+json')
else:
cur_mime = mimetypes.guess_type(name)[0]
if cur_mime is not None:
if cur_mime == 'text/plain':
self.set_header('Content-Type', 'text/plain; charset=UTF-8')
elif cur_mime is not None:
self.set_header('Content-Type', cur_mime)
else:
if model['format'] == 'base64':
self.set_header('Content-Type', 'application/octet-stream')
else:
self.set_header('Content-Type', 'text/plain')
self.set_header('Content-Type', 'text/plain; charset=UTF-8')
if include_body:
if model['format'] == 'base64':

@ -0,0 +1,122 @@
# Implementation Notes for Internationalization of Jupyter Notebook
This is a prototype implementation of i18n features for Jupyter notebook, and should not
yet be considered ready for production use. I have tried to focus on the public user
interfaces in the notebook for the first cut, while leaving much of the console messages
behind, as their usefulness in a translated environment is questionable at best.
### Using a prototype translated version
In order to use this preliminary version, you need to do things after installing the
notebook as normal:
1. Set the LANG environment variable in your shell to "xx_XX" or just "xx".
where "xx" is the language code you're wanting to run in. If you're
running on Windows, I've found the easiest way to do this is to use Windows PowerShell,
and run the command:
`${Env:LANG} = "xx_XX"`
2. Set the preferred language for web pages in your browser to YourLanguage (xx). At the moment,
it has to be first in the list.
3. Run the `jupyter notebook` command to start the notebook.
### Message extraction:
I have split out the translatable material for the notebook into 3 POT, as follows:
notebook/i18n/notebook.pot - Console and startup messages, basically anything that is
produced by Python code.
notebook/i18n/nbui.pot - User interface strings, as extracted from the Jinja2 templates
in notebook/templates/*.html
noteook/i18n/nbjs.pot - JavaScript strings and dialogs, which contain much of the visible
user interface for Jupyter notebook.
To extract the messages from the source code whenever new material is added, use the
`pybabel` command to extract messages from the source code as follows:
( assuming you are in the base directory for Jupyter notebook )
`pybabel extract -F notebook/i18n/babel_notebook.cfg -o notebook/i18n/notebook.pot --no-wrap --project Jupyter .`
`pybabel extract -F notebook/i18n/babel_nbui.cfg -o notebook/i18n/nbui.pot --no-wrap --project Jupyter .`
`pybabel extract -F notebook/i18n/babel_nbjs.cfg -o notebook/i18n/nbjs.pot --no-wrap --project Jupyter .`
(Note: there is a '.' at the end of these commands, and it has to be there...)
After this is complete you have 3 POT files that you can give to a translator for your favorite language.
Babel's documentation has instructions on how to integrate this into your setup.py so that eventually
we can just do:
`setup.py extract_messages`
I hope to get this working at some point in the near future.
### Post translation procedures
After the source material has been translated, you should have 3 PO files with the same base names
as the POT files above. Put them in `notebook/i18n/${LANG}/LC_MESSAGES`, where ${LANG} is the language
code for your desired language ( i.e. German = "de", Japanese = "ja", etc. ). The first 2 files then
need to be converted from PO to MO format for use at runtime. There are many different ways to do
this, but pybabel has an option to do this as follows:
`pybabel compile -D notebook -f -l ${LANG} -i notebook/i18n/${LANG}/LC_MESSAGES/notebook.po -o notebook/i18n/${LANG}/notebook.mo`
`pybabel compile -D nbui -f -l ${LANG} -i notebook/i18n/${LANG}/LC_MESSAGES/nbui.po -o notebook/i18n/${LANG}/nbui.mo`
The nbjs.po needs to be converted to JSON for use within the JavaScript code. I'm using po2json for this, as follows:
`po2json -p -F -f jed1.x -d nbjs notebook/i18n/${LANG}/LC_MESSAGES/nbjs.po notebook/i18n/${LANG}/LC_MESSAGES/nbjs.json`
The conversions from PO to MO probably can and should be done during setup.py.
When new languages get added, their language codes should be added to notebook/i18n/nbjs.json
under the "supported_languages" element.
### Tips for Jupyter developers
The biggest "mistake" I found while doing i18n enablement was the habit of constructing UI messages
from English "piece parts". For example, code like:
`var msg = "Enter a new " + type + "name:"`
where "type" is either "file", "directory", or "notebook"....
is problematic when doing translations, because the surrounding text may need to vary
depending on the inserted word. In this case, you need to switch it and use complete phrases,
as follows:
```javascript
var rename_msg = function (type) {
switch(type) {
case 'file': return _("Enter a new file name:");
case 'directory': return _("Enter a new directory name:");
case 'notebook': return _("Enter a new notebook name:");
default: return _("Enter a new name:");
}
}
```
Also you need to remember that adding an "s" or "es" to an English word to
create the plural form doesn't translate well. Some languages have as many as 5 or 6 different
plural forms for differing numbers, so using an API such as ngettext() is necessary in order
to handle these cases properly.
### Known issues
1. Right now there are two different places where the desired language is set. At startup time, the Jupyter console's messages pay attention to the setting of the ${LANG} environment variable
as set in the shell at startup time. Unfortunately, this is also the time where the Jinja2
environment is set up, which means that the template stuff will always come from this setting.
We really want to be paying attention to the browser's settings for the stuff that happens in the
browser, so we need to be able to retrieve this information after the browser is started and somehow
communicate this back to Jinja2. So far, I haven't yet figured out how to do this, which means that if the ${LANG} at startup doesn't match the browser's settings, you could potentially get a mix
of languages in the UI ( never a good thing ).
2. We will need to decide if console messages should be translatable, and enable them if desired.
3. The keyboard shorcut editor was implemented after the i18n work was completed, so that portion
does not have translation support at this time.
Any questions or comments please let me know @JCEmmons on github (emmo@us.ibm.com)

@ -0,0 +1,11 @@
[javascript: notebook/static/base/js/*.js]
extract_messages = $._, i18n.msg._
[javascript: notebook/static/notebook/js/*.js]
extract_messages = $._, i18n.msg._
[javascript: notebook/static/notebook/js/celltoolbarpresets/*.js]
extract_messages = $._, i18n.msg._
[javascript: notebook/static/tree/js/*.js]
extract_messages = $._, i18n.msg._

@ -0,0 +1,4 @@
[jinja2: notebook/templates/**.html]
encoding = utf-8
[extractors]
jinja2 = jinja2.ext:babel_extract

@ -0,0 +1,2 @@
[python: notebook/*.py]
[python: notebook/services/contents/*.py]

@ -0,0 +1,12 @@
{
"domain": "nbjs",
"supported_languages": [
],
"locale_data": {
"nbjs": {
"": {
"domain": "nbjs"
}
}
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,731 @@
# Translations template for Jupyter.
# Copyright (C) 2017 ORGANIZATION
# This file is distributed under the same license as the Jupyter project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2017.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: Jupyter VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2017-07-07 12:48-0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.3.4\n"
#: notebook/templates/404.html:3
msgid "You are requesting a page that does not exist!"
msgstr ""
#: notebook/templates/edit.html:37
msgid "current mode"
msgstr ""
#: notebook/templates/edit.html:48 notebook/templates/notebook.html:78
msgid "File"
msgstr ""
#: notebook/templates/edit.html:50 notebook/templates/tree.html:57
msgid "New"
msgstr ""
#: notebook/templates/edit.html:51
msgid "Save"
msgstr ""
#: notebook/templates/edit.html:52 notebook/templates/tree.html:36
msgid "Rename"
msgstr ""
#: notebook/templates/edit.html:53 notebook/templates/tree.html:38
msgid "Download"
msgstr ""
#: notebook/templates/edit.html:56 notebook/templates/notebook.html:131
#: notebook/templates/tree.html:41
msgid "Edit"
msgstr ""
#: notebook/templates/edit.html:58
msgid "Find"
msgstr ""
#: notebook/templates/edit.html:59
msgid "Find &amp; Replace"
msgstr ""
#: notebook/templates/edit.html:61
msgid "Key Map"
msgstr ""
#: notebook/templates/edit.html:62
msgid "Default"
msgstr ""
#: notebook/templates/edit.html:63
msgid "Sublime Text"
msgstr ""
#: notebook/templates/edit.html:68 notebook/templates/notebook.html:159
#: notebook/templates/tree.html:40
msgid "View"
msgstr ""
#: notebook/templates/edit.html:70 notebook/templates/notebook.html:162
msgid "Show/Hide the logo and notebook title (above menu bar)"
msgstr ""
#: notebook/templates/edit.html:71 notebook/templates/notebook.html:163
msgid "Toggle Header"
msgstr ""
#: notebook/templates/edit.html:72 notebook/templates/notebook.html:171
msgid "Toggle Line Numbers"
msgstr ""
#: notebook/templates/edit.html:75
msgid "Language"
msgstr ""
#: notebook/templates/error.html:23
msgid "The error was:"
msgstr ""
#: notebook/templates/login.html:24
msgid "Password or token:"
msgstr ""
#: notebook/templates/login.html:26
msgid "Password:"
msgstr ""
#: notebook/templates/login.html:31
msgid "Log in"
msgstr ""
#: notebook/templates/login.html:39
msgid "No login available, you shouldn't be seeing this page."
msgstr ""
#: notebook/templates/logout.html:24
#, python-format
msgid "Proceed to the <a href=\"%(base_url)s\">dashboard"
msgstr ""
#: notebook/templates/logout.html:26
#, python-format
msgid "Proceed to the <a href=\"%(base_url)slogin\">login page"
msgstr ""
#: notebook/templates/notebook.html:62
msgid "Menu"
msgstr ""
#: notebook/templates/notebook.html:65 notebook/templates/notebook.html:254
msgid "Kernel"
msgstr ""
#: notebook/templates/notebook.html:68
msgid "This notebook is read-only"
msgstr ""
#: notebook/templates/notebook.html:81
msgid "New Notebook"
msgstr ""
#: notebook/templates/notebook.html:85
msgid "Opens a new window with the Dashboard view"
msgstr ""
#: notebook/templates/notebook.html:86
msgid "Open..."
msgstr ""
#: notebook/templates/notebook.html:90
msgid "Open a copy of this notebook's contents and start a new kernel"
msgstr ""
#: notebook/templates/notebook.html:91
msgid "Make a Copy..."
msgstr ""
#: notebook/templates/notebook.html:92
msgid "Rename..."
msgstr ""
#: notebook/templates/notebook.html:93
msgid "Save and Checkpoint"
msgstr ""
#: notebook/templates/notebook.html:96
msgid "Revert to Checkpoint"
msgstr ""
#: notebook/templates/notebook.html:106
msgid "Print Preview"
msgstr ""
#: notebook/templates/notebook.html:107
msgid "Download as"
msgstr ""
#: notebook/templates/notebook.html:109
msgid "Notebook (.ipynb)"
msgstr ""
#: notebook/templates/notebook.html:110
msgid "Script"
msgstr ""
#: notebook/templates/notebook.html:111
msgid "HTML (.html)"
msgstr ""
#: notebook/templates/notebook.html:112
msgid "Markdown (.md)"
msgstr ""
#: notebook/templates/notebook.html:113
msgid "reST (.rst)"
msgstr ""
#: notebook/templates/notebook.html:114
msgid "LaTeX (.tex)"
msgstr ""
#: notebook/templates/notebook.html:115
msgid "PDF via LaTeX (.pdf)"
msgstr ""
#: notebook/templates/notebook.html:118
msgid "Deploy as"
msgstr ""
#: notebook/templates/notebook.html:123
msgid "Trust the output of this notebook"
msgstr ""
#: notebook/templates/notebook.html:124
msgid "Trust Notebook"
msgstr ""
#: notebook/templates/notebook.html:127
msgid "Shutdown this notebook's kernel, and close this window"
msgstr ""
#: notebook/templates/notebook.html:128
msgid "Close and Halt"
msgstr ""
#: notebook/templates/notebook.html:133
msgid "Cut Cells"
msgstr ""
#: notebook/templates/notebook.html:134
msgid "Copy Cells"
msgstr ""
#: notebook/templates/notebook.html:135
msgid "Paste Cells Above"
msgstr ""
#: notebook/templates/notebook.html:136
msgid "Paste Cells Below"
msgstr ""
#: notebook/templates/notebook.html:137
msgid "Paste Cells &amp; Replace"
msgstr ""
#: notebook/templates/notebook.html:138
msgid "Delete Cells"
msgstr ""
#: notebook/templates/notebook.html:139
msgid "Undo Delete Cells"
msgstr ""
#: notebook/templates/notebook.html:141
msgid "Split Cell"
msgstr ""
#: notebook/templates/notebook.html:142
msgid "Merge Cell Above"
msgstr ""
#: notebook/templates/notebook.html:143
msgid "Merge Cell Below"
msgstr ""
#: notebook/templates/notebook.html:145
msgid "Move Cell Up"
msgstr ""
#: notebook/templates/notebook.html:146
msgid "Move Cell Down"
msgstr ""
#: notebook/templates/notebook.html:148
msgid "Edit Notebook Metadata"
msgstr ""
#: notebook/templates/notebook.html:150
msgid "Find and Replace"
msgstr ""
#: notebook/templates/notebook.html:152
msgid "Cut Cell Attachments"
msgstr ""
#: notebook/templates/notebook.html:153
msgid "Copy Cell Attachments"
msgstr ""
#: notebook/templates/notebook.html:154
msgid "Paste Cell Attachments"
msgstr ""
#: notebook/templates/notebook.html:156
msgid "Insert Image"
msgstr ""
#: notebook/templates/notebook.html:166
msgid "Show/Hide the action icons (below menu bar)"
msgstr ""
#: notebook/templates/notebook.html:167
msgid "Toggle Toolbar"
msgstr ""
#: notebook/templates/notebook.html:170
msgid "Show/Hide line numbers in cells"
msgstr ""
#: notebook/templates/notebook.html:174
msgid "Cell Toolbar"
msgstr ""
#: notebook/templates/notebook.html:179
msgid "Insert"
msgstr ""
#: notebook/templates/notebook.html:182
msgid "Insert an empty Code cell above the currently active cell"
msgstr ""
#: notebook/templates/notebook.html:183
msgid "Insert Cell Above"
msgstr ""
#: notebook/templates/notebook.html:185
msgid "Insert an empty Code cell below the currently active cell"
msgstr ""
#: notebook/templates/notebook.html:186
msgid "Insert Cell Below"
msgstr ""
#: notebook/templates/notebook.html:189
msgid "Cell"
msgstr ""
#: notebook/templates/notebook.html:191
msgid "Run this cell, and move cursor to the next one"
msgstr ""
#: notebook/templates/notebook.html:192
msgid "Run Cells"
msgstr ""
#: notebook/templates/notebook.html:193
msgid "Run this cell, select below"
msgstr ""
#: notebook/templates/notebook.html:194
msgid "Run Cells and Select Below"
msgstr ""
#: notebook/templates/notebook.html:195
msgid "Run this cell, insert below"
msgstr ""
#: notebook/templates/notebook.html:196
msgid "Run Cells and Insert Below"
msgstr ""
#: notebook/templates/notebook.html:197
msgid "Run all cells in the notebook"
msgstr ""
#: notebook/templates/notebook.html:198
msgid "Run All"
msgstr ""
#: notebook/templates/notebook.html:199
msgid "Run all cells above (but not including) this cell"
msgstr ""
#: notebook/templates/notebook.html:200
msgid "Run All Above"
msgstr ""
#: notebook/templates/notebook.html:201
msgid "Run this cell and all cells below it"
msgstr ""
#: notebook/templates/notebook.html:202
msgid "Run All Below"
msgstr ""
#: notebook/templates/notebook.html:205
msgid "All cells in the notebook have a cell type. By default, new cells are created as 'Code' cells"
msgstr ""
#: notebook/templates/notebook.html:206
msgid "Cell Type"
msgstr ""
#: notebook/templates/notebook.html:209
msgid "Contents will be sent to the kernel for execution, and output will display in the footer of cell"
msgstr ""
#: notebook/templates/notebook.html:212
msgid "Contents will be rendered as HTML and serve as explanatory text"
msgstr ""
#: notebook/templates/notebook.html:213 notebook/templates/notebook.html:298
msgid "Markdown"
msgstr ""
#: notebook/templates/notebook.html:215
msgid "Contents will pass through nbconvert unmodified"
msgstr ""
#: notebook/templates/notebook.html:216
msgid "Raw NBConvert"
msgstr ""
#: notebook/templates/notebook.html:220
msgid "Current Outputs"
msgstr ""
#: notebook/templates/notebook.html:223
msgid "Hide/Show the output of the current cell"
msgstr ""
#: notebook/templates/notebook.html:224 notebook/templates/notebook.html:240
msgid "Toggle"
msgstr ""
#: notebook/templates/notebook.html:227
msgid "Scroll the output of the current cell"
msgstr ""
#: notebook/templates/notebook.html:228 notebook/templates/notebook.html:244
msgid "Toggle Scrolling"
msgstr ""
#: notebook/templates/notebook.html:231
msgid "Clear the output of the current cell"
msgstr ""
#: notebook/templates/notebook.html:232 notebook/templates/notebook.html:248
msgid "Clear"
msgstr ""
#: notebook/templates/notebook.html:236
msgid "All Output"
msgstr ""
#: notebook/templates/notebook.html:239
msgid "Hide/Show the output of all cells"
msgstr ""
#: notebook/templates/notebook.html:243
msgid "Scroll the output of all cells"
msgstr ""
#: notebook/templates/notebook.html:247
msgid "Clear the output of all cells"
msgstr ""
#: notebook/templates/notebook.html:257
msgid "Send Keyboard Interrupt (CTRL-C) to the Kernel"
msgstr ""
#: notebook/templates/notebook.html:258
msgid "Interrupt"
msgstr ""
#: notebook/templates/notebook.html:261
msgid "Restart the Kernel"
msgstr ""
#: notebook/templates/notebook.html:262
msgid "Restart"
msgstr ""
#: notebook/templates/notebook.html:265
msgid "Restart the Kernel and clear all output"
msgstr ""
#: notebook/templates/notebook.html:266
msgid "Restart &amp; Clear Output"
msgstr ""
#: notebook/templates/notebook.html:269
msgid "Restart the Kernel and re-run the notebook"
msgstr ""
#: notebook/templates/notebook.html:270
msgid "Restart &amp; Run All"
msgstr ""
#: notebook/templates/notebook.html:273
msgid "Reconnect to the Kernel"
msgstr ""
#: notebook/templates/notebook.html:274
msgid "Reconnect"
msgstr ""
#: notebook/templates/notebook.html:282
msgid "Change kernel"
msgstr ""
#: notebook/templates/notebook.html:287
msgid "Help"
msgstr ""
#: notebook/templates/notebook.html:290
msgid "A quick tour of the notebook user interface"
msgstr ""
#: notebook/templates/notebook.html:290
msgid "User Interface Tour"
msgstr ""
#: notebook/templates/notebook.html:291
msgid "Opens a tooltip with all keyboard shortcuts"
msgstr ""
#: notebook/templates/notebook.html:291
msgid "Keyboard Shortcuts"
msgstr ""
#: notebook/templates/notebook.html:292
msgid "Opens a dialog allowing you to edit Keyboard shortcuts"
msgstr ""
#: notebook/templates/notebook.html:292
msgid "Edit Keyboard Shortcuts"
msgstr ""
#: notebook/templates/notebook.html:297
msgid "Notebook Help"
msgstr ""
#: notebook/templates/notebook.html:303
msgid "Opens in a new window"
msgstr ""
#: notebook/templates/notebook.html:319
msgid "About Jupyter Notebook"
msgstr ""
#: notebook/templates/notebook.html:319
msgid "About"
msgstr ""
#: notebook/templates/page.html:114
msgid "Jupyter Notebook requires JavaScript."
msgstr ""
#: notebook/templates/page.html:115
msgid "Please enable it to proceed. "
msgstr ""
#: notebook/templates/page.html:121
msgid "dashboard"
msgstr ""
#: notebook/templates/page.html:132
msgid "Logout"
msgstr ""
#: notebook/templates/page.html:134
msgid "Login"
msgstr ""
#: notebook/templates/tree.html:23
msgid "Files"
msgstr ""
#: notebook/templates/tree.html:24
msgid "Running"
msgstr ""
#: notebook/templates/tree.html:25
msgid "Clusters"
msgstr ""
#: notebook/templates/tree.html:32
msgid "Select items to perform actions on them."
msgstr ""
#: notebook/templates/tree.html:35
msgid "Duplicate selected"
msgstr ""
#: notebook/templates/tree.html:35
msgid "Duplicate"
msgstr ""
#: notebook/templates/tree.html:36
msgid "Rename selected"
msgstr ""
#: notebook/templates/tree.html:37
msgid "Move selected"
msgstr ""
#: notebook/templates/tree.html:37
msgid "Move"
msgstr ""
#: notebook/templates/tree.html:38
msgid "Download selected"
msgstr ""
#: notebook/templates/tree.html:39
msgid "Shutdown selected notebook(s)"
msgstr ""
#: notebook/templates/tree.html:39
msgid "Shutdown"
msgstr ""
#: notebook/templates/tree.html:40
msgid "View selected"
msgstr ""
#: notebook/templates/tree.html:41
msgid "Edit selected"
msgstr ""
#: notebook/templates/tree.html:42
msgid "Delete selected"
msgstr ""
#: notebook/templates/tree.html:50
msgid "Click to browse for a file to upload."
msgstr ""
#: notebook/templates/tree.html:51
msgid "Upload"
msgstr ""
#: notebook/templates/tree.html:65
msgid "Text File"
msgstr ""
#: notebook/templates/tree.html:68
msgid "Folder"
msgstr ""
#: notebook/templates/tree.html:72
msgid "Terminal"
msgstr ""
#: notebook/templates/tree.html:76
msgid "Terminals Unavailable"
msgstr ""
#: notebook/templates/tree.html:82
msgid "Refresh notebook list"
msgstr ""
#: notebook/templates/tree.html:90
msgid "Select All / None"
msgstr ""
#: notebook/templates/tree.html:93
msgid "Select..."
msgstr ""
#: notebook/templates/tree.html:98
msgid "Select All Folders"
msgstr ""
#: notebook/templates/tree.html:98
msgid " Folders"
msgstr ""
#: notebook/templates/tree.html:99
msgid "Select All Notebooks"
msgstr ""
#: notebook/templates/tree.html:99
msgid " All Notebooks"
msgstr ""
#: notebook/templates/tree.html:100
msgid "Select Running Notebooks"
msgstr ""
#: notebook/templates/tree.html:100
msgid " Running"
msgstr ""
#: notebook/templates/tree.html:101
msgid "Select All Files"
msgstr ""
#: notebook/templates/tree.html:101
msgid " Files"
msgstr ""
#: notebook/templates/tree.html:114
msgid "Last Modified"
msgstr ""
#: notebook/templates/tree.html:120
msgid "Name"
msgstr ""
#: notebook/templates/tree.html:130
msgid "Currently running Jupyter processes"
msgstr ""
#: notebook/templates/tree.html:134
msgid "Refresh running list"
msgstr ""
#: notebook/templates/tree.html:150
msgid "There are no terminals running."
msgstr ""
#: notebook/templates/tree.html:152
msgid "Terminals are unavailable."
msgstr ""
#: notebook/templates/tree.html:162
msgid "Notebooks"
msgstr ""
#: notebook/templates/tree.html:169
msgid "There are no notebooks running."
msgstr ""
#: notebook/templates/tree.html:178
msgid "Clusters tab is now provided by IPython parallel."
msgstr ""
#: notebook/templates/tree.html:179
msgid "See '<a href=\"https://github.com/ipython/ipyparallel\">IPython parallel</a>' for installation details."
msgstr ""

@ -0,0 +1,480 @@
# Translations template for Jupyter.
# Copyright (C) 2017 ORGANIZATION
# This file is distributed under the same license as the Jupyter project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2017.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: Jupyter VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2017-07-08 21:52-0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.3.4\n"
#: notebook/notebookapp.py:53
msgid "The Jupyter Notebook requires tornado >= 4.0"
msgstr ""
#: notebook/notebookapp.py:57
msgid "The Jupyter Notebook requires tornado >= 4.0, but you have < 1.1.0"
msgstr ""
#: notebook/notebookapp.py:59
#, python-format
msgid "The Jupyter Notebook requires tornado >= 4.0, but you have %s"
msgstr ""
#: notebook/notebookapp.py:209
msgid "The `ignore_minified_js` flag is deprecated and no longer works."
msgstr ""
#: notebook/notebookapp.py:210
#, python-format
msgid "Alternatively use `%s` when working on the notebook's Javascript and LESS"
msgstr ""
#: notebook/notebookapp.py:211
msgid "The `ignore_minified_js` flag is deprecated and will be removed in Notebook 6.0"
msgstr ""
#: notebook/notebookapp.py:389
msgid "List currently running notebook servers."
msgstr ""
#: notebook/notebookapp.py:393
msgid "Produce machine-readable JSON output."
msgstr ""
#: notebook/notebookapp.py:397
msgid "If True, each line of output will be a JSON object with the details from the server info file."
msgstr ""
#: notebook/notebookapp.py:402
msgid "Currently running servers:"
msgstr ""
#: notebook/notebookapp.py:419
msgid "Don't open the notebook in a browser after startup."
msgstr ""
#: notebook/notebookapp.py:423
msgid "DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib."
msgstr ""
#: notebook/notebookapp.py:439
msgid "Allow the notebook to be run from root user."
msgstr ""
#: notebook/notebookapp.py:470
msgid ""
"The Jupyter HTML Notebook.\n"
" \n"
" This launches a Tornado based HTML Notebook Server that serves up an HTML5/Javascript Notebook client."
msgstr ""
#: notebook/notebookapp.py:509
msgid "Deprecated: Use minified JS file or not, mainly use during dev to avoid JS recompilation"
msgstr ""
#: notebook/notebookapp.py:540
msgid "Set the Access-Control-Allow-Credentials: true header"
msgstr ""
#: notebook/notebookapp.py:544
msgid "Whether to allow the user to run the notebook as root."
msgstr ""
#: notebook/notebookapp.py:548
msgid "The default URL to redirect to from `/`"
msgstr ""
#: notebook/notebookapp.py:552
msgid "The IP address the notebook server will listen on."
msgstr ""
#: notebook/notebookapp.py:565
#, python-format
msgid ""
"Cannot bind to localhost, using 127.0.0.1 as default ip\n"
"%s"
msgstr ""
#: notebook/notebookapp.py:579
msgid "The port the notebook server will listen on."
msgstr ""
#: notebook/notebookapp.py:583
msgid "The number of additional ports to try if the specified port is not available."
msgstr ""
#: notebook/notebookapp.py:587
msgid "The full path to an SSL/TLS certificate file."
msgstr ""
#: notebook/notebookapp.py:591
msgid "The full path to a private key file for usage with SSL/TLS."
msgstr ""
#: notebook/notebookapp.py:595
msgid "The full path to a certificate authority certificate for SSL/TLS client authentication."
msgstr ""
#: notebook/notebookapp.py:599
msgid "The file where the cookie secret is stored."
msgstr ""
#: notebook/notebookapp.py:628
#, python-format
msgid "Writing notebook server cookie secret to %s"
msgstr ""
#: notebook/notebookapp.py:635
#, python-format
msgid "Could not set permissions on %s"
msgstr ""
#: notebook/notebookapp.py:640
msgid ""
"Token used for authenticating first-time connections to the server.\n"
"\n"
" When no password is enabled,\n"
" the default is to generate a new, random token.\n"
"\n"
" Setting to an empty string disables authentication altogether, which is NOT RECOMMENDED.\n"
" "
msgstr ""
#: notebook/notebookapp.py:650
msgid ""
"One-time token used for opening a browser.\n"
" Once used, this token cannot be used again.\n"
" "
msgstr ""
#: notebook/notebookapp.py:726
msgid ""
"Specify Where to open the notebook on startup. This is the\n"
" `new` argument passed to the standard library method `webbrowser.open`.\n"
" The behaviour is not guaranteed, but depends on browser support. Valid\n"
" values are:\n"
" 2 opens a new tab,\n"
" 1 opens a new window,\n"
" 0 opens in an existing window.\n"
" See the `webbrowser.open` documentation for details.\n"
" "
msgstr ""
#: notebook/notebookapp.py:737
msgid "DEPRECATED, use tornado_settings"
msgstr ""
#: notebook/notebookapp.py:742
msgid ""
"\n"
" webapp_settings is deprecated, use tornado_settings.\n"
msgstr ""
#: notebook/notebookapp.py:746
msgid "Supply overrides for the tornado.web.Application that the Jupyter notebook uses."
msgstr ""
#: notebook/notebookapp.py:750
msgid ""
"\n"
" Set the tornado compression options for websocket connections.\n"
"\n"
" This value will be returned from :meth:`WebSocketHandler.get_compression_options`.\n"
" None (default) will disable compression.\n"
" A dict (even an empty one) will enable compression.\n"
"\n"
" See the tornado docs for WebSocketHandler.get_compression_options for details.\n"
" "
msgstr ""
#: notebook/notebookapp.py:761
msgid "Supply overrides for terminado. Currently only supports \"shell_command\"."
msgstr ""
#: notebook/notebookapp.py:764
msgid "Extra keyword arguments to pass to `set_secure_cookie`. See tornado's set_secure_cookie docs for details."
msgstr ""
#: notebook/notebookapp.py:768
msgid ""
"Supply SSL options for the tornado HTTPServer.\n"
" See the tornado docs for details."
msgstr ""
#: notebook/notebookapp.py:772
msgid "Supply extra arguments that will be passed to Jinja environment."
msgstr ""
#: notebook/notebookapp.py:776
msgid "Extra variables to supply to jinja templates when rendering."
msgstr ""
#: notebook/notebookapp.py:812
msgid "DEPRECATED use base_url"
msgstr ""
#: notebook/notebookapp.py:816
msgid "base_project_url is deprecated, use base_url"
msgstr ""
#: notebook/notebookapp.py:832
msgid "Path to search for custom.js, css"
msgstr ""
#: notebook/notebookapp.py:844
msgid ""
"Extra paths to search for serving jinja templates.\n"
"\n"
" Can be used to override templates from notebook.templates."
msgstr ""
#: notebook/notebookapp.py:855
msgid "extra paths to look for Javascript notebook extensions"
msgstr ""
#: notebook/notebookapp.py:900
#, python-format
msgid "Using MathJax: %s"
msgstr ""
#: notebook/notebookapp.py:903
msgid "The MathJax.js configuration file that is to be used."
msgstr ""
#: notebook/notebookapp.py:908
#, python-format
msgid "Using MathJax configuration file: %s"
msgstr ""
#: notebook/notebookapp.py:914
msgid "The notebook manager class to use."
msgstr ""
#: notebook/notebookapp.py:920
msgid "The kernel manager class to use."
msgstr ""
#: notebook/notebookapp.py:926
msgid "The session manager class to use."
msgstr ""
#: notebook/notebookapp.py:932
msgid "The config manager class to use"
msgstr ""
#: notebook/notebookapp.py:953
msgid "The login handler class to use."
msgstr ""
#: notebook/notebookapp.py:960
msgid "The logout handler class to use."
msgstr ""
#: notebook/notebookapp.py:964
msgid "Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headerssent by the upstream reverse proxy. Necessary if the proxy handles SSL"
msgstr ""
#: notebook/notebookapp.py:976
msgid ""
"\n"
" DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib.\n"
" "
msgstr ""
#: notebook/notebookapp.py:988
msgid "Support for specifying --pylab on the command line has been removed."
msgstr ""
#: notebook/notebookapp.py:990
msgid "Please use `%pylab{0}` or `%matplotlib{0}` in the notebook itself."
msgstr ""
#: notebook/notebookapp.py:995
msgid "The directory to use for notebooks and kernels."
msgstr ""
#: notebook/notebookapp.py:1018
#, python-format
msgid "No such notebook dir: '%r'"
msgstr ""
#: notebook/notebookapp.py:1031
msgid "DEPRECATED use the nbserver_extensions dict instead"
msgstr ""
#: notebook/notebookapp.py:1036
msgid "server_extensions is deprecated, use nbserver_extensions"
msgstr ""
#: notebook/notebookapp.py:1040
msgid "Dict of Python modules to load as notebook server extensions.Entry values can be used to enable and disable the loading ofthe extensions. The extensions will be loaded in alphabetical order."
msgstr ""
#: notebook/notebookapp.py:1049
msgid "Reraise exceptions encountered loading server extensions?"
msgstr ""
#: notebook/notebookapp.py:1052
msgid ""
"(msgs/sec)\n"
" Maximum rate at which messages can be sent on iopub before they are\n"
" limited."
msgstr ""
#: notebook/notebookapp.py:1056
msgid ""
"(bytes/sec)\n"
" Maximum rate at which stream output can be sent on iopub before they are\n"
" limited."
msgstr ""
#: notebook/notebookapp.py:1060
msgid ""
"(sec) Time window used to \n"
" check the message and data rate limits."
msgstr ""
#: notebook/notebookapp.py:1071
#, python-format
msgid "No such file or directory: %s"
msgstr ""
#: notebook/notebookapp.py:1141
msgid "Notebook servers are configured to only be run with a password."
msgstr ""
#: notebook/notebookapp.py:1142
msgid "Hint: run the following command to set a password"
msgstr ""
#: notebook/notebookapp.py:1143
msgid "\t$ python -m notebook.auth password"
msgstr ""
#: notebook/notebookapp.py:1181
#, python-format
msgid "The port %i is already in use, trying another port."
msgstr ""
#: notebook/notebookapp.py:1184
#, python-format
msgid "Permission to listen on port %i denied"
msgstr ""
#: notebook/notebookapp.py:1193
msgid "ERROR: the notebook server could not be started because no available port could be found."
msgstr ""
#: notebook/notebookapp.py:1199
msgid "[all ip addresses on your system]"
msgstr ""
#: notebook/notebookapp.py:1223
#, python-format
msgid "Terminals not available (error was %s)"
msgstr ""
#: notebook/notebookapp.py:1259
msgid "interrupted"
msgstr ""
#: notebook/notebookapp.py:1261
msgid "y"
msgstr ""
#: notebook/notebookapp.py:1262
msgid "n"
msgstr ""
#: notebook/notebookapp.py:1263
#, python-format
msgid "Shutdown this notebook server (%s/[%s])? "
msgstr ""
#: notebook/notebookapp.py:1269
msgid "Shutdown confirmed"
msgstr ""
#: notebook/notebookapp.py:1273
msgid "No answer for 5s:"
msgstr ""
#: notebook/notebookapp.py:1274
msgid "resuming operation..."
msgstr ""
#: notebook/notebookapp.py:1282
#, python-format
msgid "received signal %s, stopping"
msgstr ""
#: notebook/notebookapp.py:1338
#, python-format
msgid "Error loading server extension %s"
msgstr ""
#: notebook/notebookapp.py:1369
#, python-format
msgid "Shutting down %d kernels"
msgstr ""
#: notebook/notebookapp.py:1375
#, python-format
msgid "%d active kernel"
msgid_plural "%d active kernels"
msgstr[0] ""
msgstr[1] ""
#: notebook/notebookapp.py:1379
#, python-format
msgid ""
"The Jupyter Notebook is running at:\n"
"\r"
"%s"
msgstr ""
#: notebook/notebookapp.py:1426
msgid "Running as root is not recommended. Use --allow-root to bypass."
msgstr ""
#: notebook/notebookapp.py:1432
msgid "Use Control-C to stop this server and shut down all kernels (twice to skip confirmation)."
msgstr ""
#: notebook/notebookapp.py:1434
msgid "Welcome to Project Jupyter! Explore the various tools available and their corresponding documentation. If you are interested in contributing to the platform, please visit the communityresources section at http://jupyter.org/community.html."
msgstr ""
#: notebook/notebookapp.py:1445
#, python-format
msgid "No web browser found: %s."
msgstr ""
#: notebook/notebookapp.py:1450
#, python-format
msgid "%s does not exist"
msgstr ""
#: notebook/notebookapp.py:1484
msgid "Interrupted..."
msgstr ""
#: notebook/services/contents/filemanager.py:506
#, python-format
msgid "Serving notebooks from local directory: %s"
msgstr ""
#: notebook/services/contents/manager.py:68
msgid "Untitled"
msgstr ""

@ -324,8 +324,6 @@ class JSController(TestController):
c.start()
env = os.environ.copy()
env.update(self.env)
if self.engine == 'phantomjs':
env['IPYTHON_ALLOW_DRAFT_WEBSOCKETS_FOR_PHANTOMJS'] = '1'
self.server = subprocess.Popen(command,
stdout = c.writefd,
stderr = subprocess.STDOUT,

@ -26,12 +26,12 @@ except ImportError: #PY2
class NbconvertAPI(object):
"""Wrapper for nbconvert API calls."""
def __init__(self, base_url):
self.base_url = base_url
def __init__(self, request):
self.request = request
def _req(self, verb, path, body=None, params=None):
response = requests.request(verb,
url_path_join(self.base_url, 'nbconvert', path),
response = self.request(verb,
url_path_join('nbconvert', path),
data=body, params=params,
)
response.raise_for_status()
@ -55,7 +55,7 @@ b'\x08\xd7c\x90\xfb\xcf\x00\x00\x02\\\x01\x1e.~d\x87\x00\x00\x00\x00IEND\xaeB`\x
class APITest(NotebookTestBase):
def setUp(self):
nbdir = self.notebook_dir.name
nbdir = self.notebook_dir
if not os.path.isdir(pjoin(nbdir, 'foo')):
subdir = pjoin(nbdir, 'foo')
@ -84,7 +84,7 @@ class APITest(NotebookTestBase):
encoding='utf-8') as f:
write(nb, f, version=4)
self.nbconvert_api = NbconvertAPI(self.base_url())
self.nbconvert_api = NbconvertAPI(self.request)
@onlyif_cmds_exist('pandoc')
def test_from_file(self):
@ -118,8 +118,7 @@ class APITest(NotebookTestBase):
@onlyif_cmds_exist('pandoc')
def test_from_post(self):
nbmodel_url = url_path_join(self.base_url(), 'api/contents/foo/testnb.ipynb')
nbmodel = requests.get(nbmodel_url).json()
nbmodel = self.request('GET', 'api/contents/foo/testnb.ipynb').json()
r = self.nbconvert_api.from_post(format='html', nbmodel=nbmodel)
self.assertEqual(r.status_code, 200)
@ -133,8 +132,7 @@ class APITest(NotebookTestBase):
@onlyif_cmds_exist('pandoc')
def test_from_post_zip(self):
nbmodel_url = url_path_join(self.base_url(), 'api/contents/foo/testnb.ipynb')
nbmodel = requests.get(nbmodel_url).json()
nbmodel = self.request('GET', 'api/contents/foo/testnb.ipynb').json()
r = self.nbconvert_api.from_post(format='latex', nbmodel=nbmodel)
self.assertIn(u'application/zip', r.headers['Content-Type'])

@ -21,8 +21,8 @@ except ImportError:
from urllib import urlretrieve
from jupyter_core.paths import (
jupyter_data_dir, jupyter_config_dir, jupyter_config_path,
SYSTEM_JUPYTER_PATH, ENV_JUPYTER_PATH, ENV_CONFIG_PATH, SYSTEM_CONFIG_PATH
jupyter_data_dir, jupyter_config_path, jupyter_path,
SYSTEM_JUPYTER_PATH, ENV_JUPYTER_PATH,
)
from ipython_genutils.path import ensure_dir_exists
from ipython_genutils.py3compat import string_types, cast_unicode_py2
@ -165,7 +165,7 @@ def install_nbextension(path, overwrite=False, symlink=False,
full_dest = None
else:
if not destination:
destination = basename(path)
destination = basename(normpath(path))
destination = cast_unicode_py2(destination)
full_dest = normpath(pjoin(nbext, destination))
if overwrite and os.path.lexists(full_dest):
@ -484,7 +484,7 @@ def validate_nbextension(require, logger=None):
infos = []
js_exists = False
for exts in _nbextension_dirs():
for exts in jupyter_path('nbextensions'):
# Does the Javascript entrypoint actually exist on disk?
js = u"{}.js".format(os.path.join(exts, *require.split("/")))
js_exists = os.path.exists(js)
@ -1014,18 +1014,6 @@ def _get_nbextension_dir(user=False, sys_prefix=False, prefix=None, nbextensions
return nbext
def _nbextension_dirs():
"""The possible locations of nbextensions.
Returns a list of known base extension locations
"""
return [
pjoin(jupyter_data_dir(), u'nbextensions'),
pjoin(ENV_JUPYTER_PATH[0], u'nbextensions'),
pjoin(SYSTEM_JUPYTER_PATH[0], 'nbextensions')
]
def _get_nbextension_metadata(module):
"""Get the list of nbextension paths associated with a Python module.

File diff suppressed because it is too large Load Diff

@ -617,7 +617,36 @@ paths:
/status:
get:
summary: Get the current status / activity of the server
responses:
200:
description: The current status of the server
$ref: '#/definitions/APIStatus'
definitions:
APIStatus:
description: |
Notebook server API status.
Added in notebook 5.0.
properties:
started:
type: string
description: |
ISO8601 timestamp indicating when the notebook server started.
last_activity:
type: string
description: |
ISO8601 timestamp indicating the last activity on the server,
either on the REST API or kernel activity.
connections:
type: number
description: |
The total number of currently open connections to kernels.
kernels:
type: number
description: |
The total number of running kernels.
KernelSpec:
description: Kernel spec (contents of kernel.json)
properties:
@ -697,6 +726,23 @@ definitions:
name:
type: string
description: kernel spec name
last_activity:
type: string
description: |
ISO 8601 timestamp for the last-seen activity on this kernel.
Use this in combination with execution_state == 'idle' to identify
which kernels have been idle since a given time.
Timestamps will be UTC, indicated 'Z' suffix.
Added in notebook server 5.0.
connections:
type: number
description: |
The number of active connections to this kernel.
execution_state:
type: string
description: |
Current execution state of the kernel (typically 'idle' or 'busy', but may be other values, such as 'starting').
Added in notebook server 5.0.
Session:
description: A session
type: object

@ -3,8 +3,14 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
from tornado import web
from ...base.handlers import IPythonHandler
from itertools import chain
import json
from tornado import gen, web
from ...base.handlers import IPythonHandler, APIHandler, json_errors
from notebook._tz import utcfromtimestamp, isoformat
import os
class APISpecHandler(web.StaticFileHandler, IPythonHandler):
@ -18,6 +24,33 @@ class APISpecHandler(web.StaticFileHandler, IPythonHandler):
self.set_header('Content-Type', 'text/x-yaml')
return web.StaticFileHandler.get(self, 'api.yaml')
class APIStatusHandler(APIHandler):
_track_activity = False
@json_errors
@web.authenticated
@gen.coroutine
def get(self):
# if started was missing, use unix epoch
started = self.settings.get('started', utcfromtimestamp(0))
# if we've never seen API activity, use started date
api_last_activity = self.settings.get('api_last_activity', started)
started = isoformat(started)
api_last_activity = isoformat(api_last_activity)
kernels = yield gen.maybe_future(self.kernel_manager.list_kernels())
total_connections = sum(k['connections'] for k in kernels)
last_activity = max(chain([api_last_activity], [k['last_activity'] for k in kernels]))
model = {
'started': started,
'last_activity': last_activity,
'kernels': len(kernels),
'connections': total_connections,
}
self.finish(json.dumps(model, sort_keys=True))
default_handlers = [
(r"/api/spec.yaml", APISpecHandler)
(r"/api/spec.yaml", APISpecHandler),
(r"/api/status", APIStatusHandler),
]

@ -0,0 +1,32 @@
"""Test the basic /api endpoints"""
import requests
from notebook._tz import isoformat
from notebook.utils import url_path_join
from notebook.tests.launchnotebook import NotebookTestBase
class KernelAPITest(NotebookTestBase):
"""Test the kernels web service API"""
def _req(self, verb, path, **kwargs):
r = self.request(verb, url_path_join('api', path))
r.raise_for_status()
return r
def get(self, path, **kwargs):
return self._req('GET', path)
def test_get_spec(self):
r = self.get('spec.yaml')
assert r.text
def test_get_status(self):
r = self.get('status')
data = r.json()
assert data['connections'] == 0
assert data['kernels'] == 0
assert data['last_activity'].endswith('Z')
assert data['started'].endswith('Z')
assert data['started'] == isoformat(self.notebook.web_app.settings['started'])

@ -11,12 +11,12 @@ from notebook.tests.launchnotebook import NotebookTestBase
class ConfigAPI(object):
"""Wrapper for notebook API calls."""
def __init__(self, base_url):
self.base_url = base_url
def __init__(self, request):
self.request = request
def _req(self, verb, section, body=None):
response = requests.request(verb,
url_path_join(self.base_url, 'api/config', section),
response = self.request(verb,
url_path_join('api/config', section),
data=body,
)
response.raise_for_status()
@ -34,7 +34,7 @@ class ConfigAPI(object):
class APITest(NotebookTestBase):
"""Test the config web service API"""
def setUp(self):
self.config_api = ConfigAPI(self.base_url())
self.config_api = ConfigAPI(self.request)
def test_create_retrieve_config(self):
sample = {'foo': 'bar', 'baz': 73}

@ -12,11 +12,12 @@ from .checkpoints import (
)
from .fileio import FileManagerMixin
from . import tz
from ipython_genutils.path import ensure_dir_exists
from ipython_genutils.py3compat import getcwd
from traitlets import Unicode
from notebook import _tz as tz
class FileCheckpoints(FileManagerMixin, Checkpoints):
"""

@ -3,12 +3,13 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
from datetime import datetime
import errno
import io
import os
import shutil
import stat
import sys
import warnings
import mimetypes
import nbformat
@ -19,15 +20,18 @@ from tornado import web
from .filecheckpoints import FileCheckpoints
from .fileio import FileManagerMixin
from .manager import ContentsManager
from ...utils import exists
from ipython_genutils.importstring import import_item
from traitlets import Any, Unicode, Bool, TraitError, observe, default, validate
from ipython_genutils.py3compat import getcwd, string_types
from . import tz
from notebook import _tz as tz
from notebook.utils import (
is_hidden, is_file_hidden,
to_api_path,
)
from notebook.base.handlers import AuthenticatedFileHandler
try:
from os.path import samefile
@ -142,6 +146,7 @@ class FileContentsManager(FileManagerMixin, ContentsManager):
raise TraitError("%r is not a directory" % value)
return value
@default('checkpoints_class')
def _checkpoints_class_default(self):
return FileCheckpoints
@ -150,6 +155,10 @@ class FileContentsManager(FileManagerMixin, ContentsManager):
platform's trash/recycle bin, where they can be recovered. If False,
deleting files really deletes them.""")
@default('files_handler_class')
def _files_handler_class_default(self):
return AuthenticatedFileHandler
def is_hidden(self, path):
"""Does the API style path correspond to a hidden directory or file?
@ -224,14 +233,25 @@ class FileContentsManager(FileManagerMixin, ContentsManager):
"""
path = path.strip('/')
os_path = self._get_os_path(path=path)
return os.path.exists(os_path)
return exists(os_path)
def _base_model(self, path):
"""Build the common base of a contents model"""
os_path = self._get_os_path(path)
info = os.stat(os_path)
last_modified = tz.utcfromtimestamp(info.st_mtime)
created = tz.utcfromtimestamp(info.st_ctime)
info = os.lstat(os_path)
try:
last_modified = tz.utcfromtimestamp(info.st_mtime)
except ValueError:
# Files can rarely have an invalid timestamp
# https://github.com/jupyter/notebook/issues/2539
# Use the Unix epoch as a fallback so we don't crash.
last_modified = datetime(1970, 1, 1, 0, 0, tzinfo=tz.UTC)
try:
created = tz.utcfromtimestamp(info.st_ctime)
except ValueError: # See above
created = datetime(1970, 1, 1, 0, 0, tzinfo=tz.UTC)
# Create the base model.
model = {}
model['name'] = path.rsplit('/', 1)[-1]
@ -279,16 +299,18 @@ class FileContentsManager(FileManagerMixin, ContentsManager):
continue
try:
st = os.stat(os_path)
st = os.lstat(os_path)
except OSError as e:
# skip over broken symlinks in listing
if e.errno == errno.ENOENT:
self.log.warning("%s doesn't exist", os_path)
else:
self.log.warning("Error stat-ing %s: %s", (os_path, e))
self.log.warning("Error stat-ing %s: %s", os_path, e)
continue
if not stat.S_ISREG(st.st_mode) and not stat.S_ISDIR(st.st_mode):
if (not stat.S_ISLNK(st.st_mode)
and not stat.S_ISREG(st.st_mode)
and not stat.S_ISDIR(st.st_mode)):
self.log.debug("%s not a regular file", os_path)
continue
@ -511,7 +533,7 @@ class FileContentsManager(FileManagerMixin, ContentsManager):
raise web.HTTPError(500, u'Unknown error renaming file: %s %s' % (old_path, e))
def info_string(self):
return "Serving notebooks from local directory: %s" % self.root_dir
return _("Serving notebooks from local directory: %s") % self.root_dir
def get_kernel_path(self, path, model=None):
"""Return the initial API path of a kernel associated with a given notebook"""

@ -18,17 +18,6 @@ from notebook.base.handlers import (
)
def sort_key(model):
"""key function for case-insensitive sort by name and type"""
iname = model['name'].lower()
type_key = {
'directory' : '0',
'notebook' : '1',
'file' : '2',
}.get(model['type'], '9')
return u'%s%s' % (type_key, iname)
def validate_model(model, expect_content):
"""
Validate a model returned by a ContentsManager method.
@ -123,10 +112,6 @@ class ContentsHandler(APIHandler):
model = yield gen.maybe_future(self.contents_manager.get(
path=path, type=type, format=format, content=content,
))
if model['type'] == 'directory' and content:
# group listing by type, then by name (case-insensitive)
# FIXME: sorting should be done in the frontends
model['content'].sort(key=sort_key)
validate_model(model, expect_content=content)
self._finish_model(model, location=False)
@ -176,7 +161,9 @@ class ContentsHandler(APIHandler):
@gen.coroutine
def _save(self, model, path):
"""Save an existing file."""
self.log.info(u"Saving file at %s", path)
chunk = model.get("chunk", None)
if not chunk or chunk == -1: # Avoid tedious log information
self.log.info(u"Saving file at %s", path)
model = yield gen.maybe_future(self.contents_manager.save(model, path))
validate_model(model, expect_content=False)
self._finish_model(model)

@ -0,0 +1,70 @@
from notebook.services.contents.filemanager import FileContentsManager
from contextlib import contextmanager
from tornado import web
import nbformat
import base64
import os, io
class LargeFileManager(FileContentsManager):
"""Handle large file upload."""
def save(self, model, path=''):
"""Save the file model and return the model with no content."""
chunk = model.get('chunk', None)
if chunk is not None:
path = path.strip('/')
if 'type' not in model:
raise web.HTTPError(400, u'No file type provided')
if model['type'] != 'file':
raise web.HTTPError(400, u'File type "{}" is not supported for large file transfer'.format(model['type']))
if 'content' not in model and model['type'] != 'directory':
raise web.HTTPError(400, u'No file content provided')
os_path = self._get_os_path(path)
try:
if chunk == 1:
self.log.debug("Saving %s", os_path)
self.run_pre_save_hook(model=model, path=path)
super(LargeFileManager, self)._save_file(os_path, model['content'], model.get('format'))
else:
self._save_large_file(os_path, model['content'], model.get('format'))
except web.HTTPError:
raise
except Exception as e:
self.log.error(u'Error while saving file: %s %s', path, e, exc_info=True)
raise web.HTTPError(500, u'Unexpected error while saving file: %s %s' % (path, e))
model = self.get(path, content=False)
# Last chunk
if chunk == -1:
self.run_post_save_hook(model=model, os_path=os_path)
return model
else:
return super(LargeFileManager, self).save(model, path)
def _save_large_file(self, os_path, content, format):
"""Save content of a generic file."""
if format not in {'text', 'base64'}:
raise web.HTTPError(
400,
"Must specify format of file contents as 'text' or 'base64'",
)
try:
if format == 'text':
bcontent = content.encode('utf8')
else:
b64_bytes = content.encode('ascii')
bcontent = base64.b64decode(b64_bytes)
except Exception as e:
raise web.HTTPError(
400, u'Encoding error saving %s: %s' % (os_path, e)
)
with self.perm_to_403(os_path):
if os.path.islink(os_path):
os_path = os.path.join(os.path.dirname(os_path), os.readlink(os_path))
with io.open(os_path, 'ab') as f:
f.write(bcontent)

@ -4,6 +4,7 @@
# Distributed under the terms of the Modified BSD License.
from fnmatch import fnmatch
import gettext
import itertools
import json
import os
@ -28,6 +29,7 @@ from traitlets import (
default,
)
from ipython_genutils.py3compat import string_types
from notebook.base.handlers import IPythonHandler
copy_pat = re.compile(r'\-Copy\d*\.')
@ -50,6 +52,8 @@ class ContentsManager(LoggingConfigurable):
indicating the root path.
"""
root_dir = Unicode('/', config=True)
notary = Instance(sign.NotebookNotary)
def _notary_default(self):
@ -62,7 +66,7 @@ class ContentsManager(LoggingConfigurable):
Glob patterns to hide in file and directory listings.
""")
untitled_notebook = Unicode("Untitled", config=True,
untitled_notebook = Unicode(_("Untitled"), config=True,
help="The base name used when creating untitled notebooks."
)
@ -127,6 +131,8 @@ class ContentsManager(LoggingConfigurable):
log=self.log,
)
files_handler_class = Type(IPythonHandler, allow_none=True, config=True)
# ContentsManager API part 1: methods that must be
# implemented in subclasses.

@ -50,12 +50,12 @@ def dirs_only(dir_model):
class API(object):
"""Wrapper for contents API calls."""
def __init__(self, base_url):
self.base_url = base_url
def __init__(self, request):
self.request = request
def _req(self, verb, path, body=None, params=None):
response = requests.request(verb,
url_path_join(self.base_url, 'api/contents', path),
response = self.request(verb,
url_path_join('api/contents', path),
data=body, params=params,
)
response.raise_for_status()
@ -152,7 +152,7 @@ class APITest(NotebookTestBase):
return u'%s text file' % name
def to_os_path(self, api_path):
return to_os_path(api_path, root=self.notebook_dir.name)
return to_os_path(api_path, root=self.notebook_dir)
def make_dir(self, api_path):
"""Create a directory at api_path"""
@ -220,7 +220,7 @@ class APITest(NotebookTestBase):
self.make_blob(blobname, blob)
self.addCleanup(partial(self.delete_file, blobname))
self.api = API(self.base_url())
self.api = API(self.request)
def test_list_notebooks(self):
nbs = notebooks_only(self.api.list().json())
@ -249,8 +249,8 @@ class APITest(NotebookTestBase):
self.assertEqual(nbnames, expected)
nbs = notebooks_only(self.api.list('ordering').json())
nbnames = [n['name'] for n in nbs]
expected = ['A.ipynb', 'b.ipynb', 'C.ipynb']
nbnames = {n['name'] for n in nbs}
expected = {'A.ipynb', 'b.ipynb', 'C.ipynb'}
self.assertEqual(nbnames, expected)
def test_list_dirs(self):

@ -0,0 +1,113 @@
from unittest import TestCase
from ipython_genutils.tempdir import TemporaryDirectory
from ..largefilemanager import LargeFileManager
import os
from tornado import web
def _make_dir(contents_manager, api_path):
"""
Make a directory.
"""
os_path = contents_manager._get_os_path(api_path)
try:
os.makedirs(os_path)
except OSError:
print("Directory already exists: %r" % os_path)
class TestLargeFileManager(TestCase):
def setUp(self):
self._temp_dir = TemporaryDirectory()
self.td = self._temp_dir.name
self.contents_manager = LargeFileManager(root_dir=self.td)
def make_dir(self, api_path):
"""make a subdirectory at api_path
override in subclasses if contents are not on the filesystem.
"""
_make_dir(self.contents_manager, api_path)
def test_save(self):
cm = self.contents_manager
# Create a notebook
model = cm.new_untitled(type='notebook')
name = model['name']
path = model['path']
# Get the model with 'content'
full_model = cm.get(path)
# Save the notebook
model = cm.save(full_model, path)
assert isinstance(model, dict)
self.assertIn('name', model)
self.assertIn('path', model)
self.assertEqual(model['name'], name)
self.assertEqual(model['path'], path)
try:
model = {'name': 'test', 'path': 'test', 'chunk': 1}
cm.save(model, model['path'])
except web.HTTPError as e:
self.assertEqual('HTTP 400: Bad Request (No file type provided)', str(e))
try:
model = {'name': 'test', 'path': 'test', 'chunk': 1, 'type': 'notebook'}
cm.save(model, model['path'])
except web.HTTPError as e:
self.assertEqual('HTTP 400: Bad Request (File type "notebook" is not supported for large file transfer)', str(e))
try:
model = {'name': 'test', 'path': 'test', 'chunk': 1, 'type': 'file'}
cm.save(model, model['path'])
except web.HTTPError as e:
self.assertEqual('HTTP 400: Bad Request (No file content provided)', str(e))
try:
model = {'name': 'test', 'path': 'test', 'chunk': 2, 'type': 'file',
'content': u'test', 'format': 'json'}
cm.save(model, model['path'])
except web.HTTPError as e:
self.assertEqual("HTTP 400: Bad Request (Must specify format of file contents as 'text' or 'base64')",
str(e))
# Save model for different chunks
model = {'name': 'test', 'path': 'test', 'type': 'file',
'content': u'test==', 'format': 'text'}
name = model['name']
path = model['path']
cm.save(model, path)
for chunk in (1, 2, -1):
for fm in ('text', 'base64'):
full_model = cm.get(path)
full_model['chunk'] = chunk
full_model['format'] = fm
model_res = cm.save(full_model, path)
assert isinstance(model_res, dict)
self.assertIn('name', model_res)
self.assertIn('path', model_res)
self.assertNotIn('chunk', model_res)
self.assertEqual(model_res['name'], name)
self.assertEqual(model_res['path'], path)
# Test in sub-directory
# Create a directory and notebook in that directory
sub_dir = '/foo/'
self.make_dir('foo')
model = cm.new_untitled(path=sub_dir, type='notebook')
name = model['name']
path = model['path']
model = cm.get(path)
# Change the name in the model for rename
model = cm.save(model, path)
assert isinstance(model, dict)
self.assertIn('name', model)
self.assertIn('path', model)
self.assertEqual(model['name'], 'Untitled.ipynb')
self.assertEqual(model['path'], 'foo/Untitled.ipynb')

@ -108,7 +108,7 @@ class TestFileContentsManager(TestCase):
self.assertEqual(cp_dir, os.path.join(root, cpm.checkpoint_dir, cp_name))
self.assertEqual(cp_subdir, os.path.join(root, subd, cpm.checkpoint_dir, cp_name))
@dec.skip_win32
@dec.skipif(sys.platform == 'win32' and sys.version_info[0] < 3)
def test_bad_symlink(self):
with TemporaryDirectory() as td:
cm = FileContentsManager(root_dir=td)
@ -120,9 +120,16 @@ class TestFileContentsManager(TestCase):
# create a broken symlink
self.symlink(cm, "target", '%s/%s' % (path, 'bad symlink'))
model = cm.get(path)
self.assertEqual(model['content'], [file_model])
@dec.skip_win32
contents = {
content['name']: content for content in model['content']
}
self.assertTrue('untitled.txt' in contents)
self.assertEqual(contents['untitled.txt'], file_model)
# broken symlinks should still be shown in the contents manager
self.assertTrue('bad symlink' in contents)
@dec.skipif(sys.platform == 'win32' and sys.version_info[0] < 3)
def test_good_symlink(self):
with TemporaryDirectory() as td:
cm = FileContentsManager(root_dir=td)
@ -224,7 +231,7 @@ class TestContentsManager(TestCase):
self.assertEqual(entry['name'], "nb.ipynb")
complete_path = "/".join([api_path, "nb.ipynb"])
self.assertEqual(entry["path"], complete_path)
def setUp(self):
self._temp_dir = TemporaryDirectory()
self.td = self._temp_dir.name

@ -1,46 +0,0 @@
# encoding: utf-8
"""
Timezone utilities
Just UTC-awareness right now
"""
#-----------------------------------------------------------------------------
# Copyright (C) 2013 The IPython Development Team
#
# Distributed under the terms of the BSD License. The full license is in
# the file COPYING, distributed as part of this software.
#-----------------------------------------------------------------------------
#-----------------------------------------------------------------------------
# Imports
#-----------------------------------------------------------------------------
from datetime import tzinfo, timedelta, datetime
#-----------------------------------------------------------------------------
# Code
#-----------------------------------------------------------------------------
# constant for zero offset
ZERO = timedelta(0)
class tzUTC(tzinfo):
"""tzinfo object for UTC (zero offset)"""
def utcoffset(self, d):
return ZERO
def dst(self, d):
return ZERO
UTC = tzUTC()
def utc_aware(unaware):
"""decorator for adding UTC tzinfo to datetime's utcfoo methods"""
def utc_method(*args, **kwargs):
dt = unaware(*args, **kwargs)
return dt.replace(tzinfo=UTC)
return utc_method
utcfromtimestamp = utc_aware(datetime.utcfromtimestamp)
utcnow = utc_aware(datetime.utcnow)

@ -31,7 +31,7 @@ class MainKernelHandler(APIHandler):
def get(self):
km = self.kernel_manager
kernels = yield gen.maybe_future(km.list_kernels())
self.finish(json.dumps(kernels))
self.finish(json.dumps(kernels, default=date_default))
@json_errors
@web.authenticated
@ -51,7 +51,7 @@ class MainKernelHandler(APIHandler):
location = url_path_join(self.base_url, 'api', 'kernels', url_escape(kernel_id))
self.set_header('Location', location)
self.set_status(201)
self.finish(json.dumps(model))
self.finish(json.dumps(model, default=date_default))
class KernelHandler(APIHandler):
@ -62,7 +62,7 @@ class KernelHandler(APIHandler):
km = self.kernel_manager
km._check_kernel_id(kernel_id)
model = km.kernel_model(kernel_id)
self.finish(json.dumps(model))
self.finish(json.dumps(model, default=date_default))
@json_errors
@web.authenticated
@ -93,7 +93,7 @@ class KernelActionHandler(APIHandler):
self.set_status(500)
else:
model = km.kernel_model(kernel_id)
self.write(json.dumps(model))
self.write(json.dumps(model, default=date_default))
self.finish()
@ -260,6 +260,7 @@ class ZMQChannelsHandler(AuthenticatedZMQStreamHandler):
def open(self, kernel_id):
super(ZMQChannelsHandler, self).open()
self.kernel_manager.notify_connect(kernel_id)
try:
self.create_stream()
except web.HTTPError as e:
@ -335,7 +336,10 @@ class ZMQChannelsHandler(AuthenticatedZMQStreamHandler):
# Increment the bytes and message count
self._iopub_window_msg_count += 1
byte_count = sum([len(x) for x in msg_list])
if msg_type == 'stream':
byte_count = sum([len(x) for x in msg_list])
else:
byte_count = 0
self._iopub_window_byte_count += byte_count
# Queue a removal of the byte and message count for a time in the
@ -356,7 +360,12 @@ class ZMQChannelsHandler(AuthenticatedZMQStreamHandler):
The notebook server will temporarily stop sending output
to the client in order to avoid crashing it.
To change this limit, set the config variable
`--NotebookApp.iopub_msg_rate_limit`."""))
`--NotebookApp.iopub_msg_rate_limit`.
Current values:
NotebookApp.iopub_msg_rate_limit={} (msgs/sec)
NotebookApp.rate_limit_window={} (secs)
""".format(self.iopub_msg_rate_limit, self.rate_limit_window)))
else:
# resume once we've got some headroom below the limit
if self._iopub_msgs_exceeded and msg_rate < (0.8 * self.iopub_msg_rate_limit):
@ -373,7 +382,12 @@ class ZMQChannelsHandler(AuthenticatedZMQStreamHandler):
The notebook server will temporarily stop sending output
to the client in order to avoid crashing it.
To change this limit, set the config variable
`--NotebookApp.iopub_data_rate_limit`."""))
`--NotebookApp.iopub_data_rate_limit`.
Current values:
NotebookApp.iopub_data_rate_limit={} (bytes/sec)
NotebookApp.rate_limit_window={} (secs)
""".format(self.iopub_data_rate_limit, self.rate_limit_window)))
else:
# resume once we've got some headroom below the limit
if self._iopub_data_exceeded and data_rate < (0.8 * self.iopub_data_rate_limit):
@ -401,6 +415,7 @@ class ZMQChannelsHandler(AuthenticatedZMQStreamHandler):
self._open_sessions.pop(self.session_key)
km = self.kernel_manager
if self.kernel_id in km:
km.notify_disconnect(self.kernel_id)
km.remove_restart_callback(
self.kernel_id, self.on_kernel_restarted,
)

@ -11,14 +11,18 @@ import os
from tornado import gen, web
from tornado.concurrent import Future
from tornado.ioloop import IOLoop
from tornado.ioloop import IOLoop, PeriodicCallback
from jupyter_client.session import Session
from jupyter_client.multikernelmanager import MultiKernelManager
from traitlets import List, Unicode, TraitError, default, validate
from traitlets import Bool, Dict, List, Unicode, TraitError, Integer, default, validate
from notebook.utils import to_os_path
from notebook.utils import to_os_path, exists
from notebook._tz import utcnow, isoformat
from ipython_genutils.py3compat import getcwd
from datetime import datetime, timedelta
class MappingKernelManager(MultiKernelManager):
"""A KernelManager that handles notebook mapping and HTTP error handling"""
@ -30,6 +34,12 @@ class MappingKernelManager(MultiKernelManager):
kernel_argv = List(Unicode())
root_dir = Unicode(config=True)
_kernel_connections = Dict()
_culler_callback = None
_initialized_culler = False
@default('root_dir')
def _default_root_dir(self):
@ -45,10 +55,32 @@ class MappingKernelManager(MultiKernelManager):
if not os.path.isabs(value):
# If we receive a non-absolute path, make it absolute.
value = os.path.abspath(value)
if not os.path.exists(value) or not os.path.isdir(value):
if not exists(value) or not os.path.isdir(value):
raise TraitError("kernel root dir %r is not a directory" % value)
return value
cull_idle_timeout_minimum = 300 # 5 minutes
cull_idle_timeout = Integer(0, config=True,
help="""Timeout (in seconds) after which a kernel is considered idle and ready to be culled. Values of 0 or
lower disable culling. The minimum timeout is 300 seconds (5 minutes). Positive values less than the minimum value
will be set to the minimum."""
)
cull_interval_default = 300 # 5 minutes
cull_interval = Integer(cull_interval_default, config=True,
help="""The interval (in seconds) on which to check for idle kernels exceeding the cull timeout value."""
)
cull_connected = Bool(False, config=True,
help="""Whether to consider culling kernels which have one or more connections.
Only effective if cull_idle_timeout is not 0."""
)
cull_busy = Bool(False, config=True,
help="""Whether to consider culling kernels which are busy.
Only effective if cull_idle_timeout is not 0."""
)
#-------------------------------------------------------------------------
# Methods for managing kernels and sessions
#-------------------------------------------------------------------------
@ -90,6 +122,8 @@ class MappingKernelManager(MultiKernelManager):
kernel_id = yield gen.maybe_future(
super(MappingKernelManager, self).start_kernel(**kwargs)
)
self._kernel_connections[kernel_id] = 0
self.start_watching_activity(kernel_id)
self.log.info("Kernel started: %s" % kernel_id)
self.log.debug("Kernel args: %r" % kwargs)
# register callback for failed auto-restart
@ -100,12 +134,19 @@ class MappingKernelManager(MultiKernelManager):
else:
self._check_kernel_id(kernel_id)
self.log.info("Using existing kernel: %s" % kernel_id)
# Initialize culling if not already
if not self._initialized_culler:
self.initialize_culler()
# py2-compat
raise gen.Return(kernel_id)
def shutdown_kernel(self, kernel_id, now=False):
"""Shutdown a kernel by kernel_id"""
self._check_kernel_id(kernel_id)
self._kernels[kernel_id]._activity_stream.close()
self._kernel_connections.pop(kernel_id, None)
return super(MappingKernelManager, self).shutdown_kernel(kernel_id, now=now)
def restart_kernel(self, kernel_id):
@ -149,12 +190,31 @@ class MappingKernelManager(MultiKernelManager):
timeout = loop.add_timeout(loop.time() + 30, on_timeout)
return future
def notify_connect(self, kernel_id):
"""Notice a new connection to a kernel"""
if kernel_id in self._kernel_connections:
self._kernel_connections[kernel_id] += 1
def notify_disconnect(self, kernel_id):
"""Notice a disconnection from a kernel"""
if kernel_id in self._kernel_connections:
self._kernel_connections[kernel_id] -= 1
def kernel_model(self, kernel_id):
"""Return a dictionary of kernel information described in the
JSON standard model."""
"""Return a JSON-safe dict representing a kernel
For use in representing kernels in the JSON APIs.
"""
self._check_kernel_id(kernel_id)
model = {"id":kernel_id,
"name": self._kernels[kernel_id].kernel_name}
kernel = self._kernels[kernel_id]
model = {
"id":kernel_id,
"name": kernel.kernel_name,
"last_activity": isoformat(kernel.last_activity),
"execution_state": kernel.execution_state,
"connections": self._kernel_connections[kernel_id],
}
return model
def list_kernels(self):
@ -171,3 +231,92 @@ class MappingKernelManager(MultiKernelManager):
"""Check a that a kernel_id exists and raise 404 if not."""
if kernel_id not in self:
raise web.HTTPError(404, u'Kernel does not exist: %s' % kernel_id)
# monitoring activity:
def start_watching_activity(self, kernel_id):
"""Start watching IOPub messages on a kernel for activity.
- update last_activity on every message
- record execution_state from status messages
"""
kernel = self._kernels[kernel_id]
# add busy/activity markers:
kernel.execution_state = 'starting'
kernel.last_activity = utcnow()
kernel._activity_stream = kernel.connect_iopub()
session = Session(
config=kernel.session.config,
key=kernel.session.key,
)
def record_activity(msg_list):
"""Record an IOPub message arriving from a kernel"""
kernel.last_activity = utcnow()
idents, fed_msg_list = session.feed_identities(msg_list)
msg = session.deserialize(fed_msg_list)
msg_type = msg['header']['msg_type']
self.log.debug("activity on %s: %s", kernel_id, msg_type)
if msg_type == 'status':
kernel.execution_state = msg['content']['execution_state']
kernel._activity_stream.on_recv(record_activity)
def initialize_culler(self):
"""Start idle culler if 'cull_idle_timeout' is greater than zero.
Regardless of that value, set flag that we've been here.
"""
if not self._initialized_culler and self.cull_idle_timeout > 0:
if self._culler_callback is None:
if self.cull_idle_timeout < self.cull_idle_timeout_minimum:
self.log.warning("'cull_idle_timeout' (%s) is less than the minimum value (%s) and has been set to the minimum.",
self.cull_idle_timeout, self.cull_idle_timeout_minimum)
self.cull_idle_timeout = self.cull_idle_timeout_minimum
loop = IOLoop.current()
if self.cull_interval <= 0: #handle case where user set invalid value
self.log.warning("Invalid value for 'cull_interval' detected (%s) - using default value (%s).",
self.cull_interval, self.cull_interval_default)
self.cull_interval = self.cull_interval_default
self._culler_callback = PeriodicCallback(
self.cull_kernels, 1000*self.cull_interval, loop)
self.log.info("Culling kernels with idle durations > %s seconds at %s second intervals ...",
self.cull_idle_timeout, self.cull_interval)
if self.cull_busy:
self.log.info("Culling kernels even if busy")
if self.cull_connected:
self.log.info("Culling kernels even with connected clients")
self._culler_callback.start()
self._initialized_culler = True
def cull_kernels(self):
self.log.debug("Polling every %s seconds for kernels idle > %s seconds...",
self.cull_interval, self.cull_idle_timeout)
"""Create a separate list of kernels to avoid conflicting updates while iterating"""
for kernel_id in list(self._kernels):
try:
self.cull_kernel_if_idle(kernel_id)
except Exception as e:
self.log.exception("The following exception was encountered while checking the idle duration of kernel %s: %s",
kernel_id, e)
def cull_kernel_if_idle(self, kernel_id):
kernel = self._kernels[kernel_id]
self.log.debug("kernel_id=%s, kernel_name=%s, last_activity=%s", kernel_id, kernel.kernel_name, kernel.last_activity)
if kernel.last_activity is not None:
dt_now = utcnow()
dt_idle = dt_now - kernel.last_activity
# Compute idle properties
is_idle_time = dt_idle > timedelta(seconds=self.cull_idle_timeout)
is_idle_execute = self.cull_busy or (kernel.execution_state != 'busy')
connections = self._kernel_connections.get(kernel_id, 0)
is_idle_connected = self.cull_connected or not connections
# Cull the kernel if all three criteria are met
if (is_idle_time and is_idle_execute and is_idle_connected):
idle_duration = int(dt_idle.total_seconds())
self.log.warning("Culling '%s' kernel '%s' (%s) with %d connections due to %s seconds of inactivity.",
kernel.execution_state, kernel.kernel_name, kernel_id, connections, idle_duration)
self.shutdown_kernel(kernel_id)

@ -1,7 +1,11 @@
"""Test the kernels service API."""
import json
import requests
import time
from tornado.httpclient import HTTPRequest
from tornado.ioloop import IOLoop
from tornado.websocket import websocket_connect
from jupyter_client.kernelspec import NATIVE_KERNEL_NAME
@ -10,12 +14,14 @@ from notebook.tests.launchnotebook import NotebookTestBase, assert_http_error
class KernelAPI(object):
"""Wrapper for kernel REST API requests"""
def __init__(self, base_url):
def __init__(self, request, base_url, headers):
self.request = request
self.base_url = base_url
self.headers = headers
def _req(self, verb, path, body=None):
response = requests.request(verb,
url_path_join(self.base_url, 'api/kernels', path), data=body)
response = self.request(verb,
url_path_join('api/kernels', path), data=body)
if 400 <= response.status_code < 600:
try:
@ -45,10 +51,23 @@ class KernelAPI(object):
def restart(self, id):
return self._req('POST', url_path_join(id, 'restart'))
def websocket(self, id):
loop = IOLoop()
req = HTTPRequest(
url_path_join(self.base_url.replace('http', 'ws', 1), 'api/kernels', id, 'channels'),
headers=self.headers,
)
f = websocket_connect(req, io_loop=loop)
return loop.run_sync(lambda : f)
class KernelAPITest(NotebookTestBase):
"""Test the kernels web service API"""
def setUp(self):
self.kern_api = KernelAPI(self.base_url())
self.kern_api = KernelAPI(self.request,
base_url=self.base_url(),
headers=self.auth_headers(),
)
def tearDown(self):
for k in self.kern_api.list().json():
@ -144,3 +163,22 @@ class KernelAPITest(NotebookTestBase):
bad_id = '111-111-111-111-111'
with assert_http_error(404, 'Kernel does not exist: ' + bad_id):
self.kern_api.shutdown(bad_id)
def test_connections(self):
kid = self.kern_api.start().json()['id']
model = self.kern_api.get(kid).json()
self.assertEqual(model['connections'], 0)
ws = self.kern_api.websocket(kid)
model = self.kern_api.get(kid).json()
self.assertEqual(model['connections'], 1)
ws.close()
# give it some time to close on the other side:
for i in range(10):
model = self.kern_api.get(kid).json()
if model['connections'] > 0:
time.sleep(0.1)
else:
break
model = self.kern_api.get(kid).json()
self.assertEqual(model['connections'], 0)

@ -26,12 +26,12 @@ some_resource = u"The very model of a modern major general"
class KernelSpecAPI(object):
"""Wrapper for notebook API calls."""
def __init__(self, base_url):
self.base_url = base_url
def __init__(self, request):
self.request = request
def _req(self, verb, path, body=None):
response = requests.request(verb,
url_path_join(self.base_url, path),
response = self.request(verb,
path,
data=body,
)
response.raise_for_status()
@ -52,10 +52,10 @@ class APITest(NotebookTestBase):
def setUp(self):
self.create_spec('sample')
self.create_spec('sample 2')
self.ks_api = KernelSpecAPI(self.base_url())
self.ks_api = KernelSpecAPI(self.request)
def create_spec(self, name):
sample_kernel_dir = pjoin(self.data_dir.name, 'kernels', name)
sample_kernel_dir = pjoin(self.data_dir, 'kernels', name)
try:
os.makedirs(sample_kernel_dir)
except OSError as e:
@ -71,7 +71,7 @@ class APITest(NotebookTestBase):
def test_list_kernelspecs_bad(self):
"""Can list kernelspecs when one is invalid"""
bad_kernel_dir = pjoin(self.data_dir.name, 'kernels', 'bad')
bad_kernel_dir = pjoin(self.data_dir, 'kernels', 'bad')
try:
os.makedirs(bad_kernel_dir)
except OSError as e:

@ -5,12 +5,12 @@ from notebook.tests.launchnotebook import NotebookTestBase
class NbconvertAPI(object):
"""Wrapper for nbconvert API calls."""
def __init__(self, base_url):
self.base_url = base_url
def __init__(self, request):
self.request = request
def _req(self, verb, path, body=None, params=None):
response = requests.request(verb,
url_path_join(self.base_url, 'api/nbconvert', path),
response = self.request(verb,
url_path_join('api/nbconvert', path),
data=body, params=params,
)
response.raise_for_status()
@ -21,7 +21,7 @@ class NbconvertAPI(object):
class APITest(NotebookTestBase):
def setUp(self):
self.nbconvert_api = NbconvertAPI(self.base_url())
self.nbconvert_api = NbconvertAPI(self.request)
def test_list_formats(self):
formats = self.nbconvert_api.list_formats().json()

@ -11,10 +11,16 @@ from . import csp_report_uri
class CSPReportHandler(APIHandler):
'''Accepts a content security policy violation report'''
def skip_origin_check(self):
_track_activity = False
def skip_check_origin(self):
"""Don't check origin when reporting origin-check violations!"""
return True
def check_xsrf_cookie(self):
# don't check XSRF for CSP reports
return
@json_errors
@web.authenticated
def post(self):

@ -41,7 +41,7 @@ class SessionRootHandler(APIHandler):
raise web.HTTPError(400, "No JSON data provided")
if 'notebook' in model and 'path' in model['notebook']:
self.log.warn('Sessions API changed, see updated swagger docs')
self.log.warning('Sessions API changed, see updated swagger docs')
model['path'] = model['notebook']['path']
model['type'] = 'notebook'
@ -119,7 +119,7 @@ class SessionHandler(APIHandler):
changes = {}
if 'notebook' in model and 'path' in model['notebook']:
self.log.warn('Sessions API changed, see updated swagger docs')
self.log.warning('Sessions API changed, see updated swagger docs')
model['path'] = model['notebook']['path']
model['type'] = 'notebook'
if 'path' in model:

@ -9,11 +9,15 @@ from tornado.ioloop import IOLoop
from ..sessionmanager import SessionManager
from notebook.services.kernels.kernelmanager import MappingKernelManager
from notebook.services.contents.manager import ContentsManager
from notebook._tz import utcnow, isoformat
class DummyKernel(object):
def __init__(self, kernel_name='python'):
self.kernel_name = kernel_name
dummy_date = utcnow()
dummy_date_s = isoformat(dummy_date)
class DummyMKM(MappingKernelManager):
"""MappingKernelManager interface that doesn't start kernels, for testing"""
def __init__(self, *args, **kwargs):
@ -25,7 +29,10 @@ class DummyMKM(MappingKernelManager):
def start_kernel(self, kernel_id=None, path=None, kernel_name='python', **kwargs):
kernel_id = kernel_id or self._new_id()
self._kernels[kernel_id] = DummyKernel(kernel_name=kernel_name)
k = self._kernels[kernel_id] = DummyKernel(kernel_name=kernel_name)
self._kernel_connections[kernel_id] = 0
k.last_activity = dummy_date
k.execution_state = 'idle'
return kernel_id
def shutdown_kernel(self, kernel_id, now=False):
@ -65,7 +72,13 @@ class TestSessionManager(TestCase):
'notebook': {'path': u'/path/to/test.ipynb', 'name': None},
'type': 'notebook',
'name': None,
'kernel': {'id':u'A', 'name': 'bar'}}
'kernel': {
'id': 'A',
'name': 'bar',
'connections': 0,
'last_activity': dummy_date_s,
'execution_state': 'idle',
}}
self.assertEqual(model, expected)
def test_bad_get_session(self):
@ -102,19 +115,37 @@ class TestSessionManager(TestCase):
'type': 'notebook',
'notebook': {'path': u'/path/to/1/test1.ipynb', 'name': None},
'name': None,
'kernel':{'id':u'A', 'name':'python'}
'kernel': {
'id': 'A',
'name':'python',
'connections': 0,
'last_activity': dummy_date_s,
'execution_state': 'idle',
}
}, {
'id':sessions[1]['id'],
'path': u'/path/to/2/test2.py',
'type': 'file',
'name': None,
'kernel':{'id':u'B', 'name':'python'}
'kernel': {
'id': 'B',
'name':'python',
'connections': 0,
'last_activity': dummy_date_s,
'execution_state': 'idle',
}
}, {
'id':sessions[2]['id'],
'path': u'/path/to/3',
'type': 'console',
'name': 'foo',
'kernel':{'id':u'C', 'name':'python'}
'kernel': {
'id': 'C',
'name':'python',
'connections': 0,
'last_activity': dummy_date_s,
'execution_state': 'idle',
}
}
]
self.assertEqual(sessions, expected)
@ -136,8 +167,11 @@ class TestSessionManager(TestCase):
'name': None,
'notebook': {'path': u'/path/to/2/test2.ipynb', 'name': None},
'kernel': {
'id': u'B',
'id': 'B',
'name':'python',
'connections': 0,
'last_activity': dummy_date_s,
'execution_state': 'idle',
}
}
]
@ -154,7 +188,14 @@ class TestSessionManager(TestCase):
'type': 'notebook',
'name': None,
'notebook': {'path': u'/path/to/new_name.ipynb', 'name': None},
'kernel':{'id':u'A', 'name':'julia'}}
'kernel': {
'id': 'A',
'name':'julia',
'connections': 0,
'last_activity': dummy_date_s,
'execution_state': 'idle',
}
}
self.assertEqual(model, expected)
def test_bad_update_session(self):
@ -179,13 +220,25 @@ class TestSessionManager(TestCase):
'type': 'notebook',
'name': None,
'notebook': {'path': u'/path/to/1/test1.ipynb', 'name': None},
'kernel': {'id':u'A', 'name':'python'}
'kernel': {
'id': 'A',
'name':'python',
'connections': 0,
'last_activity': dummy_date_s,
'execution_state': 'idle',
}
}, {
'id': sessions[2]['id'],
'type': 'console',
'path': u'/path/to/3',
'name': 'foo',
'kernel': {'id':u'C', 'name':'python'}
'kernel': {
'id': 'C',
'name':'python',
'connections': 0,
'last_activity': dummy_date_s,
'execution_state': 'idle',
}
}
]
self.assertEqual(new_sessions, expected)

@ -18,12 +18,12 @@ from nbformat import write
class SessionAPI(object):
"""Wrapper for notebook API calls."""
def __init__(self, base_url):
self.base_url = base_url
def __init__(self, request):
self.request = request
def _req(self, verb, path, body=None):
response = requests.request(verb,
url_path_join(self.base_url, 'api/sessions', path), data=body)
response = self.request(verb,
url_path_join('api/sessions', path), data=body)
if 400 <= response.status_code < 600:
try:
@ -80,7 +80,7 @@ class SessionAPI(object):
class SessionAPITest(NotebookTestBase):
"""Test the sessions web service API"""
def setUp(self):
nbdir = self.notebook_dir.name
nbdir = self.notebook_dir
subdir = pjoin(nbdir, 'foo')
try:
@ -95,7 +95,7 @@ class SessionAPITest(NotebookTestBase):
nb = new_notebook()
write(nb, f, version=4)
self.sess_api = SessionAPI(self.base_url())
self.sess_api = SessionAPI(self.request)
@self.addCleanup
def cleanup_sessions():
@ -154,7 +154,7 @@ class SessionAPITest(NotebookTestBase):
def test_create_with_kernel_id(self):
# create a new kernel
r = requests.post(url_path_join(self.base_url(), 'api/kernels'))
r = self.request('POST', 'api/kernels')
r.raise_for_status()
kernel = r.json()
@ -222,9 +222,11 @@ class SessionAPITest(NotebookTestBase):
self.assertNotEqual(after['kernel']['id'], before['kernel']['id'])
# check kernel list, to be sure previous kernel was cleaned up
r = requests.get(url_path_join(self.base_url(), 'api/kernels'))
r = self.request('GET', 'api/kernels')
r.raise_for_status()
kernel_list = r.json()
after['kernel'].pop('last_activity')
[ k.pop('last_activity') for k in kernel_list ]
self.assertEqual(kernel_list, [after['kernel']])
def test_modify_kernel_id(self):
@ -232,7 +234,7 @@ class SessionAPITest(NotebookTestBase):
sid = before['id']
# create a new kernel
r = requests.post(url_path_join(self.base_url(), 'api/kernels'))
r = self.request('POST', 'api/kernels')
r.raise_for_status()
kernel = r.json()
@ -245,7 +247,10 @@ class SessionAPITest(NotebookTestBase):
self.assertEqual(after['kernel']['id'], kernel['id'])
# check kernel list, to be sure previous kernel was cleaned up
r = requests.get(url_path_join(self.base_url(), 'api/kernels'))
r = self.request('GET', 'api/kernels')
r.raise_for_status()
kernel_list = r.json()
kernel.pop('last_activity')
[ k.pop('last_activity') for k in kernel_list ]
self.assertEqual(kernel_list, [kernel])

@ -0,0 +1,15 @@
"""HTTP handler to shut down the notebook server.
"""
from tornado import web, ioloop
from notebook.base.handlers import IPythonHandler
class ShutdownHandler(IPythonHandler):
@web.authenticated
def post(self):
self.log.info("Shutting down on /api/shutdown request.")
ioloop.IOLoop.current().stop()
default_handlers = [
(r"/api/shutdown", ShutdownHandler),
]

@ -1,9 +1,9 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
define(['base/js/namespace', 'base/js/page'], function(IPython, page) {
define(['jquery', 'base/js/namespace', 'base/js/page'], function($, IPython, page) {
function login_main() {
var page_instance = new page.Page();
var page_instance = new page.Page('div#header', 'div#site');
$('button#login_submit').addClass("btn btn-default");
page_instance.show();
$('input#password_input').focus();

@ -2,8 +2,9 @@
// Distributed under the terms of the Modified BSD License.
define([
'jquery',
'base/js/utils',
], function(utils){
], function($, utils){
"use strict";
var LoginWidget = function (selector, options) {

@ -3,7 +3,7 @@
define(['base/js/namespace', 'base/js/page'], function(IPython, page) {
function logout_main() {
var page_instance = new page.Page();
var page_instance = new page.Page('div#header', 'div#site');
page_instance.show();
IPython.page = page_instance;

@ -1,6 +1,5 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
__webpack_public_path__ = window['staticURL'] + 'auth/js/built/';
define(['./loginmain', './logoutmain'], function (login_main, logout_main) {
return {

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

@ -1,11 +1,13 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
define(function(require) {
define(['jquery',
'codemirror/lib/codemirror',
'bootstrap',
'base/js/i18n'],
function($, CodeMirror, bs, i18n) {
"use strict";
var CodeMirror = require('codemirror/lib/codemirror');
/**
* A wrapper around bootstrap modal for easier use
* Pass it an option dictionary with the following properties:
@ -53,6 +55,9 @@ define(function(require) {
dialog_content.append(
$("<div/>")
.addClass("modal-header")
.mousedown(function() {
$(".modal").draggable({handle: '.modal-header'});
})
.append($("<button>")
.attr("type", "button")
.addClass("close")
@ -65,9 +70,11 @@ define(function(require) {
.text(options.title || "")
)
).append(
$("<div/>").addClass("modal-body").append(
options.body || $("<p/>")
)
$("<div/>")
.addClass("modal-body")
.append(
options.body || $("<p/>")
)
);
var footer = $("<div/>").addClass("modal-footer");
@ -79,7 +86,7 @@ define(function(require) {
var button = $("<button/>")
.addClass("btn btn-default btn-sm")
.attr("data-dismiss", "modal")
.text(label);
.text(i18n.msg.translate(label).fetch());
if (btn_opts.id) {
button.attr('id', btn_opts.id);
}
@ -150,18 +157,27 @@ define(function(require) {
var edit_metadata = function (options) {
options.name = options.name || "Cell";
var error_div = $('<div/>').css('color', 'red');
var message =
"Manually edit the JSON below to manipulate the metadata for this " + options.name + "." +
" We recommend putting custom metadata attributes in an appropriately named substructure," +
" so they don't conflict with those of others.";
var message_cell =
i18n.msg._("Manually edit the JSON below to manipulate the metadata for this cell.");
var message_notebook =
i18n.msg._("Manually edit the JSON below to manipulate the metadata for this notebook.");
var message_end =
i18n.msg._(" We recommend putting custom metadata attributes in an appropriately named substructure," +
" so they don't conflict with those of others.");
var message;
if (options.name === 'Notebook') {
message = message_notebook + message_end;
} else {
message = message_cell + message_end;
}
var textarea = $('<textarea/>')
.attr('rows', '13')
.attr('cols', '80')
.attr('name', 'metadata')
.text(JSON.stringify(options.md || {}, null, 2));
var dialogform = $('<div/>').attr('title', 'Edit the metadata')
var dialogform = $('<div/>').attr('title', i18n.msg._('Edit the metadata'))
.append(
$('<form/>').append(
$('<fieldset/>').append(
@ -181,8 +197,17 @@ define(function(require) {
autoIndent: true,
mode: 'application/json',
});
var title_msg;
if (options.name === "Notebook") {
title_msg = i18n.msg._("Edit Notebook Metadata");
} else {
title_msg = i18n.msg._("Edit Cell Metadata");
}
// This statement is used simply so that message extraction
// will pick up the strings.
var button_labels = [ i18n.msg._("Cancel"), i18n.msg._("Edit"), i18n.msg._("OK"), i18n.msg._("Apply")];
var modal_obj = modal({
title: "Edit " + options.name + " Metadata",
title: title_msg,
body: dialogform,
default_button: "Cancel",
buttons: {
@ -197,7 +222,7 @@ define(function(require) {
new_md = JSON.parse(editor.getValue());
} catch(e) {
console.log(e);
error_div.text('WARNING: Could not save invalid JSON.');
error_div.text(i18n.msg._('WARNING: Could not save invalid JSON.'));
return false;
}
options.callback(new_md);
@ -219,10 +244,10 @@ define(function(require) {
var message;
var attachments_list;
if (Object.keys(options.attachments).length == 0) {
message = "There are no attachments for this cell.";
message = i18n.msg._("There are no attachments for this cell.");
attachments_list = $('<div>');
} else {
message = "Current cell attachments";
message = i18n.msg._("Current cell attachments");
attachments_list = $('<div>')
.addClass('list_container')
@ -231,7 +256,7 @@ define(function(require) {
.addClass('row list_header')
.append(
$('<div>')
.text('Attachments')
.text(i18n.msg._('Attachments'))
)
);
@ -255,7 +280,7 @@ define(function(require) {
.addClass('btn btn-default btn-xs')
.css('display', 'inline-block');
if (deleted) {
btn.attr('title', 'Restore')
btn.attr('title', i18n.msg._('Restore'))
.append(
$('<i>')
.addClass('fa fa-plus')
@ -265,7 +290,7 @@ define(function(require) {
refresh_attachments_list();
});
} else {
btn.attr('title', 'Delete')
btn.attr('title', i18n.msg._('Delete'))
.addClass('btn-danger')
.append(
$('<i>')
@ -314,12 +339,18 @@ define(function(require) {
}
var dialogform = $('<div/>')
.attr('title', 'Edit attachments')
.attr('title', i18n.msg._('Edit attachments'))
.append(message)
.append('<br />')
.append(attachments_list)
.append(attachments_list);
var title_msg;
if ( options.name === "Notebook" ) {
title_msg = i18n.msg._("Edit Notebook Attachments");
} else {
title_msg = i18n.msg._("Edit Cell Attachments");
}
var modal_obj = modal({
title: "Edit " + options.name + " Attachments",
title: title_msg,
body: dialogform,
buttons: {
Apply: { class : "btn-primary",
@ -339,7 +370,7 @@ define(function(require) {
var insert_image = function (options) {
var message =
"Select a file to insert.";
i18n.msg._("Select a file to insert.");
var file_input = $('<input/>')
.attr('type', 'file')
.attr('accept', 'image/*')
@ -352,7 +383,7 @@ define(function(require) {
$btn.addClass('disabled');
}
});
var dialogform = $('<div/>').attr('title', 'Edit attachments')
var dialogform = $('<div/>').attr('title', i18n.msg._('Edit attachments'))
.append(
$('<form id="insert-image-form" />').append(
$('<fieldset/>').append(
@ -365,7 +396,7 @@ define(function(require) {
)
);
var modal_obj = modal({
title: "Pick a file",
title: i18n.msg._("Select a file"),
body: dialogform,
buttons: {
OK: {

@ -9,7 +9,7 @@
// events.on("event.Namespace", function () { do_stuff(); });
// });
define(['base/js/namespace'], function(Jupyter) {
define(['jquery', 'base/js/namespace'], function($, Jupyter) {
"use strict";
// Events singleton

@ -0,0 +1,54 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
// Module to handle i18n ( Internationalization ) and translated UI
define([
'jed',
'moment',
'json!../../../i18n/nbjs.json',
'base/js/i18nload',
], function(Jed, moment, nbjs, i18nload) {
"use strict";
// Setup language related stuff
var ui_lang = navigator.languages && navigator.languages[0] || // Chrome / Firefox
navigator.language || // All browsers
navigator.userLanguage; // IE <= 10
var init = function() {
var msg_promise;
if (nbjs.supported_languages.indexOf(ui_lang) >= 0) {
moment.locale(ui_lang);
msg_promise = new Promise( function (resolve, reject) {
require([i18nload.id+"!"+ui_lang], function (data) {
var newi18n = new Jed(data);
newi18n._ = newi18n.gettext;
resolve(newi18n);
}, function (error) {
console.log("Error loading translations for language: "+ui_lang);
var newi18n = new Jed(nbjs);
newi18n._ = newi18n.gettext;
resolve(newi18n);
});
});
} else {
msg_promise = new Promise( function (resolve, reject) {
var newi18n = new Jed(nbjs);
newi18n._ = newi18n.gettext;
resolve(newi18n);
});
}
return msg_promise;
}
var i18n = new Jed(nbjs);
i18n._ = i18n.gettext;
i18n.msg = i18n; // Just a place holder until the init promise resolves.
init().then(function (msg) {
i18n.msg = msg;
i18n.msg._ = i18n.msg.gettext;
});
return i18n;
});

@ -0,0 +1,26 @@
/**
* Plugin to load a single locale.
*/
define([
"require",
"module",
// These are only here so that the optimizer knows which ones we MIGHT load.
// We will actually only load the ones we need. There should be one entry
// here for each language you want to support.
// For example, for German....
// "json!base/../../i18n/de/LC_MESSAGES/nbjs.json"
], function (require, module) {
return {
id: module.id,
load: function (locale, callerRequire, onload, loaderConfig) {
var dependencies = "json!base/../../i18n/"+locale+"/LC_MESSAGES/nbjs.json";
// Load the JSON file requested
require([dependencies], function (data) {
onload(data);
});
}
};
});

@ -1,11 +0,0 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
// TODO: Remove me in 6.0!
define([], function() {
console.warn(['Importing jquery and associated libraries, such as',
'bootstrap, is deprecated. This functionality will be remove in the',
'notebook 6.0 in favor of a fully loaded jQuery global'].join(' '));
return window.$;
});

@ -9,9 +9,10 @@
*/
define([
'jquery',
'base/js/utils',
'underscore',
], function(utils, _) {
], function($, utils, _) {
"use strict";
@ -416,8 +417,8 @@ define([
shortcut = shortcut.toLowerCase();
this.remove_shortcut(shortcut);
const patch = {keys:{}};
const b = {bind:{}};
var patch = {keys: {}};
var b = {bind: {}};
patch.keys[this._mode] = {bind:{}};
patch.keys[this._mode].bind[shortcut] = null;
this._config.update(patch);
@ -425,16 +426,16 @@ define([
// if the shortcut we unbind is a default one, we add it to the list of
// things to unbind at startup
if( this._defaults_bindings.indexOf(shortcut) !== -1 ){
const cnf = (this._config.data.keys||{})[this._mode];
const unbind_array = cnf.unbind||[];
var cnf = (this._config.data.keys || {})[this._mode];
var unbind_array = cnf.unbind || [];
// unless it's already there (like if we have remapped a default
// shortcut to another command): unbind it)
if(unbind_array.indexOf(shortcut) === -1){
const _parray = unbind_array.concat(shortcut);
const unbind_patch = {keys:{}};
unbind_patch.keys[this._mode] = {unbind:_parray}
var _parray = unbind_array.concat(shortcut);
var unbind_patch = {keys:{}};
unbind_patch.keys[this._mode] = {unbind:_parray};
console.warn('up:', unbind_patch);
this._config.update(unbind_patch);
}

@ -2,84 +2,81 @@
// Distributed under the terms of the Modified BSD License.
var Jupyter;
if (typeof window !== 'undefined') {
Jupyter = window['Jupyter'] = window['Jupyter'] || {};
} else {
Jupyter = Jupyter || {};
}
var Jupyter = Jupyter || {};
var jprop = function(name, loadedModule, modulePath){
var jprop = function(name, module_path){
Object.defineProperty(Jupyter, name, {
get: function() {
console.warn('accessing `'+name+'` is deprecated. Use `require("'+modulePath+'")`');
return loadedModule;
console.warn('accessing `'+name+'` is deprecated. Use `require("'+module_path+'")`');
return require(module_path);
},
enumerable: true,
configurable: false
});
};
}
var jglobal = function(name, loadedModule, modulePath){
var jglobal = function(name, module_path){
Object.defineProperty(Jupyter, name, {
get: function() {
console.warn('accessing `'+name+'` is deprecated. Use `require("'+modulePath+'").'+name+'`');
return loadedModule[name];
console.warn('accessing `'+name+'` is deprecated. Use `require("'+module_path+'").'+name+'`');
return require(module_path)[name];
},
enumerable: true,
configurable: false
});
};
}
define(function(){
"use strict";
// expose modules
if (!Jupyter.hasOwnProperty('utils')) jprop('utils',require('base/js/utils'), 'base/js/utils');
if (!Jupyter.hasOwnProperty('security')) jprop('security',require('base/js/security'), 'base/js/security');
if (!Jupyter.hasOwnProperty('keyboard')) jprop('keyboard',require('base/js/keyboard'), 'base/js/keyboard');
if (!Jupyter.hasOwnProperty('dialog')) jprop('dialog',require('base/js/dialog'), 'base/js/dialog');
if (!Jupyter.hasOwnProperty('mathjaxutils')) jprop('mathjaxutils',require('notebook/js/mathjaxutils'), 'notebook/js/mathjaxutils');
jprop('utils','base/js/utils')
//Jupyter.load_extensions = Jupyter.utils.load_extensions;
//
jprop('security','base/js/security');
jprop('keyboard','base/js/keyboard');
jprop('dialog','base/js/dialog');
jprop('mathjaxutils','notebook/js/mathjaxutils');
//// exposed constructors
if (!Jupyter.hasOwnProperty('CommManager')) jglobal('CommManager',require('services/kernels/comm'), 'services/kernels/comm');
if (!Jupyter.hasOwnProperty('Comm')) jglobal('Comm',require('services/kernels/comm'), 'services/kernels/comm');
jglobal('CommManager','services/kernels/comm')
jglobal('Comm','services/kernels/comm')
if (!Jupyter.hasOwnProperty('NotificationWidget')) jglobal('NotificationWidget',require('base/js/notificationwidget'), 'base/js/notificationwidget');
if (!Jupyter.hasOwnProperty('Kernel')) jglobal('Kernel',require('services/kernels/kernel'), 'services/kernels/kernel');
if (!Jupyter.hasOwnProperty('Session')) jglobal('Session',require('services/sessions/session'), 'services/sessions/session');
if (!Jupyter.hasOwnProperty('LoginWidget')) jglobal('LoginWidget',require('auth/js/loginwidget'), 'auth/js/loginwidget');
if (!Jupyter.hasOwnProperty('Page')) jglobal('Page',require('base/js/page'), 'base/js/page');
jglobal('NotificationWidget','base/js/notificationwidget');
jglobal('Kernel','services/kernels/kernel');
jglobal('Session','services/sessions/session');
jglobal('LoginWidget','auth/js/loginwidget');
jglobal('Page','base/js/page');
// notebook
if (!Jupyter.hasOwnProperty('TextCell')) jglobal('TextCell',require('notebook/js/textcell'), 'notebook/js/textcell');
if (!Jupyter.hasOwnProperty('OutputArea')) jglobal('OutputArea',require('notebook/js/outputarea'), 'notebook/js/outputarea');
if (!Jupyter.hasOwnProperty('KeyboardManager')) jglobal('KeyboardManager',require('notebook/js/keyboardmanager'), 'notebook/js/keyboardmanager');
if (!Jupyter.hasOwnProperty('Completer')) jglobal('Completer',require('notebook/js/completer'), 'notebook/js/completer');
if (!Jupyter.hasOwnProperty('Notebook')) jglobal('Notebook',require('notebook/js/notebook'), 'notebook/js/notebook');
if (!Jupyter.hasOwnProperty('Tooltip')) jglobal('Tooltip',require('notebook/js/tooltip'), 'notebook/js/tooltip');
if (!Jupyter.hasOwnProperty('Toolbar')) jglobal('Toolbar',require('notebook/js/toolbar'), 'notebook/js/toolbar');
if (!Jupyter.hasOwnProperty('SaveWidget')) jglobal('SaveWidget',require('notebook/js/savewidget'), 'notebook/js/savewidget');
if (!Jupyter.hasOwnProperty('Pager')) jglobal('Pager',require('notebook/js/pager'), 'notebook/js/pager');
if (!Jupyter.hasOwnProperty('QuickHelp')) jglobal('QuickHelp',require('notebook/js/quickhelp'), 'notebook/js/quickhelp');
if (!Jupyter.hasOwnProperty('MarkdownCell')) jglobal('MarkdownCell',require('notebook/js/textcell'), 'notebook/js/textcell');
if (!Jupyter.hasOwnProperty('RawCell')) jglobal('RawCell',require('notebook/js/textcell'), 'notebook/js/textcell');
if (!Jupyter.hasOwnProperty('Cell')) jglobal('Cell',require('notebook/js/cell'), 'notebook/js/cell');
if (!Jupyter.hasOwnProperty('MainToolBar')) jglobal('MainToolBar',require('notebook/js/maintoolbar'), 'notebook/js/maintoolbar');
if (!Jupyter.hasOwnProperty('NotebookNotificationArea')) jglobal('NotebookNotificationArea',require('notebook/js/notificationarea'), 'notebook/js/notificationarea');
if (!Jupyter.hasOwnProperty('NotebookTour')) jglobal('NotebookTour',require('notebook/js/tour'), 'notebook/js/tour');
if (!Jupyter.hasOwnProperty('MenuBar')) jglobal('MenuBar',require('notebook/js/menubar'), 'notebook/js/menubar');
jglobal('TextCell','notebook/js/textcell');
jglobal('OutputArea','notebook/js/outputarea');
jglobal('KeyboardManager','notebook/js/keyboardmanager');
jglobal('Completer','notebook/js/completer');
jglobal('Notebook','notebook/js/notebook');
jglobal('Tooltip','notebook/js/tooltip');
jglobal('Toolbar','notebook/js/toolbar');
jglobal('SaveWidget','notebook/js/savewidget');
jglobal('Pager','notebook/js/pager');
jglobal('QuickHelp','notebook/js/quickhelp');
jglobal('MarkdownCell','notebook/js/textcell');
jglobal('RawCell','notebook/js/textcell');
jglobal('Cell','notebook/js/cell');
jglobal('MainToolBar','notebook/js/maintoolbar');
jglobal('NotebookNotificationArea','notebook/js/notificationarea');
jglobal('NotebookTour', 'notebook/js/tour');
jglobal('MenuBar', 'notebook/js/menubar');
// tree
if (!Jupyter.hasOwnProperty('SessionList')) jglobal('SessionList',require('tree/js/sessionlist'), 'tree/js/sessionlist');
jglobal('SessionList','tree/js/sessionlist');
Jupyter.version = "5.0.0.dev";
Jupyter.version = "5.1.0.dev";
Jupyter._target = '_blank';
return Jupyter;
});
// deprecated since 4.0, remove in 5+
window['IPython'] = Jupyter;
var IPython = Jupyter;

@ -2,8 +2,9 @@
// Distributed under the terms of the Modified BSD License.
define([
'jquery',
'base/js/notificationwidget',
], function(notificationwidget) {
], function($, notificationwidget) {
"use strict";
// store reference to the NotificationWidget class

@ -1,7 +1,7 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
define([], function() {
define(['jquery'], function($) {
"use strict";
/**

@ -2,11 +2,22 @@
// Distributed under the terms of the Modified BSD License.
define([
'jquery',
'base/js/events',
], function(events){
], function($, events){
"use strict";
var Page = function () {
var Page = function (header_div_selector, site_div_selector) {
/**
* Constructor
*
* Parameters
* header_div_selector: string
* site_div_selector: string
*/
this.header_div_element = $(header_div_selector);
this.site_div_element = $(site_div_selector);
this.bind_events();
};
@ -37,24 +48,32 @@ define([
/**
* The header and site divs start out hidden to prevent FLOUC.
* Main scripts should call this method after styling everything.
* TODO: selector are hardcoded, pass as constructor argument
*/
$('div#header').css('display','block');
this.header_div_element.css('display','block');
};
Page.prototype.show_site = function () {
/**
* The header and site divs start out hidden to prevent FLOUC.
* Main scripts should call this method after styling everything.
* TODO: selector are hardcoded, pass as constructor argument
*/
$('div#site').css('display', 'block');
this.site_div_element.css('display', 'block');
this._resize_site();
};
Page.prototype._resize_site = function() {
// Update the site's size.
$('div#site').height($(window).height() - $('#header').height());
Page.prototype._resize_site = function(e) {
/**
* Update the site's size.
*/
// In the case an event is passed in, only trigger if the event does
// *not* have a target DOM node (i.e., it is not bubbling up). See
// https://bugs.jquery.com/ticket/9841#comment:8
if (!(e && e.target && e.target.tagName)) {
$('div#site').height($(window).height() - $('#header').height());
}
};
return {'Page': Page};

@ -2,8 +2,9 @@
// Distributed under the terms of the Modified BSD License.
define([
'jquery',
'components/google-caja/html-css-sanitizer-minified',
], function(sanitize) {
], function($, sanitize) {
"use strict";
var noop = function (x) { return x; };
@ -116,7 +117,7 @@ define([
if (allow_css) {
// sanitize style tags as stylesheets
sanitized = sanitize_stylesheets(result.sanitized, policy);
sanitized = sanitize_stylesheets(sanitized, policy);
}
return sanitized;

@ -1,10 +0,0 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
// TODO: Remove me in 6.0!
define([], function() {
console.warn(['Importing bootstrap tour is deprecated. This feature will',
'be remove in the notebook 6.0 in favor of the Tour global object.'].join(' '));
return window.Tour;
});

@ -1,12 +0,0 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
// TODO: Remove me in 6.0!
define([], function() {
console.warn(['Importing typeahead is deprecated because it is associated',
'with a global jquery variable. This feature will be removed in the',
'notebook 6.0 in favor of a jquery global. Alternatively use',
'typeahead via `$.typeahead.`'].join(' '));
return $.typeahead;
});

@ -2,12 +2,13 @@
// Distributed under the terms of the Modified BSD License.
define([
'jquery',
'codemirror/lib/codemirror',
'moment',
'underscore',
// silently upgrades CodeMirror
'codemirror/mode/meta',
], function(CodeMirror, moment, _){
], function($, CodeMirror, moment, _){
"use strict";
// keep track of which extensions have been loaded already
@ -34,7 +35,7 @@ define([
requirejs([ext_path], function(module) {
if (!is_loaded(extension)) {
console.log("Loading extension: " + extension);
if (module.load_ipython_extension) {
if (module && module.load_ipython_extension) {
Promise.resolve(module.load_ipython_extension()).then(function() {
resolve(module);
}).catch(reject);
@ -281,6 +282,8 @@ define([
var fg = [];
var bg = [];
var bold = false;
var underline = false;
var inverse = false;
var match;
var out = [];
var numbers = [];
@ -329,6 +332,14 @@ define([
classes.push("ansi-bold");
}
if (underline) {
classes.push("ansi-underline");
}
if (inverse) {
classes.push("ansi-inverse");
}
if (classes.length || styles.length) {
out.push("<span");
if (classes.length) {
@ -352,11 +363,19 @@ define([
case 0:
fg = bg = [];
bold = false;
underline = false;
inverse = false;
break;
case 1:
case 5:
bold = true;
break;
case 4:
underline = true;
break;
case 7:
inverse = true;
break;
case 21:
case 22:
bold = false;
@ -445,11 +464,11 @@ define([
// carriage return characters
function fixCarriageReturn(txt) {
txt = txt.replace(/\r+\n/gm, '\n'); // \r followed by \n --> newline
while (txt.search(/\r/g) > -1) {
var base = txt.match(/^.*\r+/m)[0].replace(/\r/, '');
var insert = txt.match(/\r+.*$/m)[0].replace(/\r/, '');
while (txt.search(/\r[^$]/g) > -1) {
var base = txt.match(/^(.*)\r+/m)[1];
var insert = txt.match(/\r+(.*)$/m)[1];
insert = insert + base.slice(insert.length, base.length);
txt = txt.replace(/\r+.*$/m, '\r').replace(/^.*\r+/m, insert);
txt = txt.replace(/\r+.*$/m, '\r').replace(/^.*\r/m, insert);
}
return txt;
}
@ -596,14 +615,14 @@ define([
* until we are building an actual request
*/
var val = $('body').data(key);
if (!val)
if (typeof val === 'undefined')
return val;
return decodeURIComponent(val);
};
var to_absolute_cursor_pos = function (cm, cursor) {
console.warn('`utils.to_absolute_cursor_pos(cm, pos)` is deprecated. Use `cm.indexFromPos(cursor)`');
return cm.indexFromPos(cusrsor);
return cm.indexFromPos(cursor);
};
var from_absolute_cursor_pos = function (cm, cursor_pos) {
@ -752,6 +771,41 @@ define([
return wrapped_error;
};
var ajax = function (url, settings) {
// like $.ajax, but ensure XSRF or Authorization header is set
if (typeof url === "object") {
// called with single argument: $.ajax({url: '...'})
settings = url;
url = settings.url;
delete settings.url;
}
settings = _add_auth_header(settings);
return $.ajax(url, settings);
};
var _get_cookie = function (name) {
// from tornado docs: http://www.tornadoweb.org/en/stable/guide/security.html
var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
return r ? r[1] : undefined;
}
var _add_auth_header = function (settings) {
/**
* Adds auth header to jquery ajax settings
*/
settings = settings || {};
if (!settings.headers) {
settings.headers = {};
}
if (!settings.headers.Authorization) {
var xsrf_token = _get_cookie('_xsrf');
if (xsrf_token) {
settings.headers['X-XSRFToken'] = xsrf_token;
}
}
return settings;
};
var promising_ajax = function(url, settings) {
/**
* Like $.ajax, but returning an ES6 promise. success and error settings
@ -766,7 +820,7 @@ define([
log_ajax_error(jqXHR, status, error);
reject(wrap_ajax_error(jqXHR, status, error));
};
$.ajax(url, settings);
ajax(url, settings);
});
};
@ -888,7 +942,10 @@ define([
}
return $el.map(function(){
// MathJax takes a DOM node: $.map makes `this` the context
return MathJax.Hub.Queue(["Typeset", MathJax.Hub, this]);
return MathJax.Hub.Queue(
["Typeset", MathJax.Hub, this],
["resetEquationNumbers",MathJax.InputJax.TeX]
);
});
};
@ -963,6 +1020,50 @@ define([
}
};
// javascript stores text as utf16 and string indices use "code units",
// which stores high-codepoint characters as "surrogate pairs",
// which occupy two indices in the javascript string.
// We need to translate cursor_pos in the protocol (in characters)
// to js offset (with surrogate pairs taking two spots).
function js_idx_to_char_idx (js_idx, text) {
var char_idx = js_idx;
for (var i = 0; i + 1 < text.length && i < js_idx; i++) {
var char_code = text.charCodeAt(i);
// check for surrogate pair
if (char_code >= 0xD800 && char_code <= 0xDBFF) {
var next_char_code = text.charCodeAt(i+1);
if (next_char_code >= 0xDC00 && next_char_code <= 0xDFFF) {
char_idx--;
i++;
}
}
}
return char_idx;
}
function char_idx_to_js_idx (char_idx, text) {
var js_idx = char_idx;
for (var i = 0; i + 1 < text.length && i < js_idx; i++) {
var char_code = text.charCodeAt(i);
// check for surrogate pair
if (char_code >= 0xD800 && char_code <= 0xDBFF) {
var next_char_code = text.charCodeAt(i+1);
if (next_char_code >= 0xDC00 && next_char_code <= 0xDFFF) {
js_idx++;
i++;
}
}
}
return js_idx;
}
if ('𝐚'.length === 1) {
// If javascript fixes string indices of non-BMP characters,
// don't keep shifting offsets to compensate for surrogate pairs
char_idx_to_js_idx = js_idx_to_char_idx = function (idx, text) { return idx; };
}
// Test if a drag'n'drop event contains a file (as opposed to an HTML
// element/text from the document)
var dnd_contain_file = function(event) {
@ -979,7 +1080,42 @@ define([
return false;
};
var throttle = function(fn, time) {
var pending = null;
return function () {
if (pending) return;
pending = setTimeout(run, time);
return function () {
clearTimeout(pending);
pending = null;
}
}
function run () {
pending = null;
fn();
}
}
var change_favicon = function (src) {
var link = document.createElement('link'),
oldLink = document.getElementById('favicon');
link.id = 'favicon';
link.type = 'image/x-icon';
link.rel = 'shortcut icon';
link.href = utils.url_path_join(utils.get_body_data('baseUrl'), src);
if (oldLink && (link.href === oldLink.href)) {
// This favicon is already set, don't modify the DOM.
return;
}
if (oldLink) document.head.removeChild(oldLink);
document.head.appendChild(link);
};
var utils = {
throttle: throttle,
is_loaded: is_loaded,
load_extension: load_extension,
load_extensions: load_extensions,
@ -1010,10 +1146,11 @@ define([
is_or_has : is_or_has,
is_focused : is_focused,
mergeopt: mergeopt,
ajax_error_msg : ajax_error_msg,
log_ajax_error : log_ajax_error,
requireCodeMirrorMode : requireCodeMirrorMode,
XHR_ERROR : XHR_ERROR,
ajax : ajax,
ajax_error_msg : ajax_error_msg,
log_ajax_error : log_ajax_error,
wrap_ajax_error : wrap_ajax_error,
promising_ajax : promising_ajax,
WrappedError: WrappedError,
@ -1026,7 +1163,10 @@ define([
format_datetime: format_datetime,
datetime_sort_helper: datetime_sort_helper,
dnd_contain_file: dnd_contain_file,
_ansispan:_ansispan
js_idx_to_char_idx: js_idx_to_char_idx,
char_idx_to_js_idx: char_idx_to_js_idx,
_ansispan:_ansispan,
change_favicon: change_favicon
};
return utils;

@ -27,6 +27,10 @@ body > #header {
z-index: 100;
#header-container {
display: flex;
flex-direction: row;
justify-content: space-between;
padding: 5px;
padding-bottom: 5px;
padding-top: 5px;
.border-box-sizing();
@ -57,12 +61,11 @@ body > #header {
padding-left: 0px;
padding-top: (@navbar-height - @logo_height) / 2;
padding-bottom: (@navbar-height - @logo_height) / 2;
@media (max-width: @screen-sm-max){
margin-left: 10px;
}
}
.flex-spacer {
flex: 1;
}
#noscript {
width: auto;
@ -99,8 +102,12 @@ input.ui-button {
padding: 0.3em 0.9em;
}
span#kernel_logo_widget {
margin: 0 10px;
}
span#login_widget {
float: right;
}
span#login_widget > .button,
@ -129,6 +136,10 @@ span#login_widget > .button,
}
}
.modal-header {
cursor: move;
}
@media (min-width: @screen-sm-min) {
.modal .modal-dialog {
width: 700px;

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save