Merge remote-tracking branch 'public-upstream/master' into links-rebase

Conflicts:
	examples/Interactive Widgets/Widget Events.ipynb
Jason Grout 11 years ago
commit 9ca509d915

4
.gitignore vendored

@ -5,6 +5,7 @@ _build
docs/man/*.gz
docs/source/api/generated
docs/source/config/options
docs/source/interactive/magics-generated.txt
docs/gh-pages
IPython/html/notebook/static/mathjax
IPython/html/static/style/*.map
@ -16,3 +17,6 @@ __pycache__
.ipynb_checkpoints
.tox
.DS_Store
\#*#
.#*
.coverage

@ -5,13 +5,11 @@ FROM ubuntu:14.04
MAINTAINER IPython Project <ipython-dev@scipy.org>
# Make sure apt is up to date
RUN apt-get update
RUN apt-get upgrade -y
ENV DEBIAN_FRONTEND noninteractive
# Not essential, but wise to set the lang
# Note: Users with other languages should set this in their derivative image
RUN apt-get install -y language-pack-en
RUN apt-get update && apt-get install -y language-pack-en
ENV LANGUAGE en_US.UTF-8
ENV LANG en_US.UTF-8
ENV LC_ALL en_US.UTF-8
@ -20,14 +18,32 @@ RUN locale-gen en_US.UTF-8
RUN dpkg-reconfigure locales
# Python binary dependencies, developer tools
RUN apt-get install -y -q build-essential make gcc zlib1g-dev git && \
apt-get install -y -q python python-dev python-pip python3-dev python3-pip && \
apt-get install -y -q libzmq3-dev sqlite3 libsqlite3-dev pandoc libcurl4-openssl-dev nodejs nodejs-legacy npm
RUN apt-get update && apt-get install -y -q \
build-essential \
make \
gcc \
zlib1g-dev \
git \
python \
python-dev \
python-pip \
python3-dev \
python3-pip \
python-sphinx \
python3-sphinx \
libzmq3-dev \
sqlite3 \
libsqlite3-dev \
pandoc \
libcurl4-openssl-dev \
nodejs \
nodejs-legacy \
npm
# In order to build from source, need less
RUN npm install -g less
RUN npm install -g less@1.7.5
RUN apt-get -y install fabric
RUN pip install invoke
RUN mkdir -p /srv/
WORKDIR /srv/
@ -37,10 +53,14 @@ RUN chmod -R +rX /srv/ipython
# .[all] only works with -e, so use file://path#egg
# Can't use -e because ipython2 and ipython3 will clobber each other
RUN pip2 install --upgrade file:///srv/ipython#egg=ipython[all]
RUN pip3 install --upgrade file:///srv/ipython#egg=ipython[all]
RUN pip2 install file:///srv/ipython#egg=ipython[all]
RUN pip3 install file:///srv/ipython#egg=ipython[all]
# install kernels
RUN python2 -m IPython kernelspec install-self --system
RUN python3 -m IPython kernelspec install-self --system
WORKDIR /tmp/
RUN iptest2
RUN iptest3

@ -4,10 +4,9 @@
Developers of the IPython Notebook will need to install the following tools:
* fabric
* invoke
* node.js
* less (`npm install -g less`)
* bower (`npm install -g bower`)
## Components
@ -15,14 +14,13 @@ We are moving to a model where our JavaScript dependencies are managed using
[bower](http://bower.io/). These packages are installed in `static/components`
and committed into a separate git repo [ipython/ipython-components](ipython/ipython-components).
Our dependencies are described in the file
`static/components/bower.json`. To update our bower packages, run `fab update`
`static/components/bower.json`. To update our bower packages, run `bower install`
in this directory.
## less
If you edit our `.less` files you will need to run the less compiler to build
our minified css files. This can be done by running `fab css` from this directory,
or `python setup.py css` from the root of the repository.
our minified css files. This can be done by running `python setup.py css` from the root of the repository.
If you are working frequently with `.less` files please consider installing git hooks that
rebuild the css files and corresponding maps in `${RepoRoot}/git-hooks/install-hooks.sh`.

@ -4,6 +4,22 @@ import os
# Packagers: modify this line if you store the notebook static files elsewhere
DEFAULT_STATIC_FILES_PATH = os.path.join(os.path.dirname(__file__), "static")
# Packagers: modify the next line if you store the notebook template files
# elsewhere
# Include both IPython/html/ and IPython/html/templates/. This makes it
# possible for users to override a template with a file that inherits from that
# template.
#
# For example, if you want to override a specific block of notebook.html, you
# can create a file called notebook.html that inherits from
# templates/notebook.html, and the latter will resolve correctly to the base
# implementation.
DEFAULT_TEMPLATE_PATH_LIST = [
os.path.dirname(__file__),
os.path.join(os.path.dirname(__file__), "templates"),
]
del os
from .nbextensions import install_nbextension
from .nbextensions import install_nbextension

@ -0,0 +1,311 @@
"""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)

@ -24,33 +24,45 @@ try:
except ImportError:
app_log = logging.getLogger()
import IPython
from IPython.utils.sysinfo import get_sys_info
from IPython.config import Application
from IPython.utils.path import filefind
from IPython.utils.py3compat import string_types
from IPython.html.utils import is_hidden, url_path_join, url_escape
from IPython.html.services.security import csp_report_uri
#-----------------------------------------------------------------------------
# Top-level handlers
#-----------------------------------------------------------------------------
non_alphanum = re.compile(r'[^A-Za-z0-9]')
sys_info = json.dumps(get_sys_info())
class AuthenticatedHandler(web.RequestHandler):
"""A RequestHandler with an authenticated user."""
def set_default_headers(self):
headers = self.settings.get('headers', {})
if "X-Frame-Options" not in headers:
headers["X-Frame-Options"] = "SAMEORIGIN"
if "Content-Security-Policy" not in headers:
headers["Content-Security-Policy"] = (
"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) + ";"
)
# Allow for overriding headers
for header_name,value in headers.items() :
try:
self.set_header(header_name, value)
except Exception:
except Exception as e:
# tornado raise Exception (not a subclass)
# if method is unsupported (websocket and Access-Control-Allow-Origin
# for example, so just ignore)
pass
self.log.debug(e)
def clear_login_cookie(self):
self.clear_cookie(self.cookie_name)
@ -120,6 +132,11 @@ class IPythonHandler(AuthenticatedHandler):
# URLs
#---------------------------------------------------------------
@property
def version_hash(self):
"""The version hash to use for cache hints for static files"""
return self.settings.get('version_hash', '')
@property
def mathjax_url(self):
return self.settings.get('mathjax_url', '')
@ -131,6 +148,12 @@ class IPythonHandler(AuthenticatedHandler):
@property
def ws_url(self):
return self.settings.get('websocket_url', '')
@property
def contents_js_source(self):
self.log.debug("Using contents: %s", self.settings.get('contents_js_source',
'services/contents'))
return self.settings.get('contents_js_source', 'services/contents')
#---------------------------------------------------------------
# Manager objects
@ -152,10 +175,18 @@ class IPythonHandler(AuthenticatedHandler):
def session_manager(self):
return self.settings['session_manager']
@property
def terminal_manager(self):
return self.settings['terminal_manager']
@property
def kernel_spec_manager(self):
return self.settings['kernel_spec_manager']
@property
def config_manager(self):
return self.settings['config_manager']
#---------------------------------------------------------------
# CORS
#---------------------------------------------------------------
@ -219,6 +250,9 @@ class IPythonHandler(AuthenticatedHandler):
logged_in=self.logged_in,
login_available=self.login_available,
static_url=self.static_url,
sys_info=sys_info,
contents_js_source=self.contents_js_source,
version_hash=self.version_hash,
)
def get_json_body(self):
@ -285,12 +319,18 @@ class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
@web.authenticated
def get(self, path):
if os.path.splitext(path)[1] == '.ipynb':
name = os.path.basename(path)
name = path.rsplit('/', 1)[-1]
self.set_header('Content-Type', 'application/json')
self.set_header('Content-Disposition','attachment; filename="%s"' % name)
return web.StaticFileHandler.get(self, path)
def set_headers(self):
super(AuthenticatedFileHandler, self).set_headers()
# disable browser caching, rely on 304 replies for savings
if "v" not in self.request.arguments:
self.add_header("Cache-Control", "no-cache")
def compute_etag(self):
return None
@ -359,7 +399,16 @@ class FileFindHandler(web.StaticFileHandler):
# cache search results, don't search for files more than once
_static_paths = {}
def initialize(self, path, default_filename=None):
def set_headers(self):
super(FileFindHandler, self).set_headers()
# disable browser caching, rely on 304 replies for savings
if "v" not in self.request.arguments or \
any(self.request.path.startswith(path) for path in self.no_cache_paths):
self.add_header("Cache-Control", "no-cache")
def initialize(self, path, default_filename=None, no_cache_paths=None):
self.no_cache_paths = no_cache_paths or []
if isinstance(path, string_types):
path = [path]
@ -398,43 +447,49 @@ class FileFindHandler(web.StaticFileHandler):
return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
class ApiVersionHandler(IPythonHandler):
@json_errors
def get(self):
# not authenticated, so give as few info as possible
self.finish(json.dumps({"version":IPython.__version__}))
class TrailingSlashHandler(web.RequestHandler):
"""Simple redirect handler that strips trailing slashes
This should be the first, highest priority handler.
"""
SUPPORTED_METHODS = ['GET']
def get(self):
self.redirect(self.request.uri.rstrip('/'))
post = put = get
class FilesRedirectHandler(IPythonHandler):
"""Handler for redirecting relative URLs to the /files/ handler"""
def get(self, path=''):
cm = self.contents_manager
if cm.path_exists(path):
if cm.dir_exists(path):
# it's a *directory*, redirect to /tree
url = url_path_join(self.base_url, 'tree', path)
else:
orig_path = path
# otherwise, redirect to /files
parts = path.split('/')
path = '/'.join(parts[:-1])
name = parts[-1]
if not cm.file_exists(name=name, path=path) and 'files' in parts:
if not cm.file_exists(path=path) and 'files' in parts:
# redirect without files/ iff it would 404
# this preserves pre-2.0-style 'files/' links
self.log.warn("Deprecated files/ URL: %s", orig_path)
parts.remove('files')
path = '/'.join(parts[:-1])
path = '/'.join(parts)
if not cm.file_exists(name=name, path=path):
if not cm.file_exists(path=path):
raise web.HTTPError(404)
url = url_path_join(self.base_url, 'files', path, name)
url = url_path_join(self.base_url, 'files', path)
url = url_escape(url)
self.log.debug("Redirecting %s to %s", self.request.path, url)
self.redirect(url)
@ -444,11 +499,9 @@ class FilesRedirectHandler(IPythonHandler):
# URL pattern fragments for re-use
#-----------------------------------------------------------------------------
path_regex = r"(?P<path>(?:/.*)*)"
notebook_name_regex = r"(?P<name>[^/]+\.ipynb)"
notebook_path_regex = "%s/%s" % (path_regex, notebook_name_regex)
file_name_regex = r"(?P<name>[^/]+)"
file_path_regex = "%s/%s" % (path_regex, file_name_regex)
# path matches any number of `/foo[/bar...]` or just `/` or ''
path_regex = r"(?P<path>(?:(?:/[^/]+)+|/?))"
notebook_path_regex = r"(?P<path>(?:/[^/]+)+\.ipynb)"
#-----------------------------------------------------------------------------
# URL to handler mappings
@ -456,5 +509,6 @@ file_path_regex = "%s/%s" % (path_regex, file_name_regex)
default_handlers = [
(r".*/", TrailingSlashHandler)
(r".*/", TrailingSlashHandler),
(r"api", ApiVersionHandler)
]

@ -1,34 +1,98 @@
# coding: utf-8
"""Tornado handlers for WebSocket <-> ZMQ sockets."""
# Copyright (c) IPython Development Team.
# Distributed under the terms of the Modified BSD License.
import os
import json
import struct
import warnings
try:
from urllib.parse import urlparse # Py 3
except ImportError:
from urlparse import urlparse # Py 2
try:
from http.cookies import SimpleCookie # Py 3
except ImportError:
from Cookie import SimpleCookie # Py 2
import logging
import tornado
from tornado import ioloop
from tornado import web
from tornado import websocket
from tornado import gen, ioloop, web
from tornado.websocket import WebSocketHandler
from IPython.kernel.zmq.session import Session
from IPython.utils.jsonutil import date_default
from IPython.utils.py3compat import PY3, cast_unicode
from IPython.utils.jsonutil import date_default, extract_dates
from IPython.utils.py3compat import cast_unicode
from .handlers import IPythonHandler
def serialize_binary_message(msg):
"""serialize a message as a binary blob
Header:
4 bytes: number of msg parts (nbufs) as 32b int
4 * nbufs bytes: offset for each buffer as integer as 32b int
Offsets are from the start of the buffer, including the header.
Returns
-------
The message serialized to bytes.
"""
# don't modify msg or buffer list in-place
msg = msg.copy()
buffers = list(msg.pop('buffers'))
bmsg = json.dumps(msg, default=date_default).encode('utf8')
buffers.insert(0, bmsg)
nbufs = len(buffers)
offsets = [4 * (nbufs + 1)]
for buf in buffers[:-1]:
offsets.append(offsets[-1] + len(buf))
offsets_buf = struct.pack('!' + 'I' * (nbufs + 1), nbufs, *offsets)
buffers.insert(0, offsets_buf)
return b''.join(buffers)
def deserialize_binary_message(bmsg):
"""deserialize a message from a binary blog
Header:
4 bytes: number of msg parts (nbufs) as 32b int
4 * nbufs bytes: offset for each buffer as integer as 32b int
class ZMQStreamHandler(websocket.WebSocketHandler):
Offsets are from the start of the buffer, including the header.
Returns
-------
message dictionary
"""
nbufs = struct.unpack('!i', bmsg[:4])[0]
offsets = list(struct.unpack('!' + 'I' * nbufs, bmsg[4:4*(nbufs+1)]))
offsets.append(None)
bufs = []
for start, stop in zip(offsets[:-1], offsets[1:]):
bufs.append(bmsg[start:stop])
msg = json.loads(bufs[0].decode('utf8'))
msg['header'] = extract_dates(msg['header'])
msg['parent_header'] = extract_dates(msg['parent_header'])
msg['buffers'] = bufs[1:]
return msg
# 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 IPython.html import allow76
WebSocketHandler = allow76.AllowDraftWebSocketHandler
# draft 76 doesn't support ping
WS_PING_INTERVAL = 0
class ZMQStreamHandler(WebSocketHandler):
def check_origin(self, origin):
"""Check Origin == Host or Access-Control-Allow-Origin.
@ -77,23 +141,19 @@ class ZMQStreamHandler(websocket.WebSocketHandler):
def _reserialize_reply(self, msg_list):
"""Reserialize a reply message using JSON.
This takes the msg list from the ZMQ socket, unserializes it using
This takes the msg list from the ZMQ socket, deserializes it using
self.session and then serializes the result using JSON. This method
should be used by self._on_zmq_reply to build messages that can
be sent back to the browser.
"""
idents, msg_list = self.session.feed_identities(msg_list)
msg = self.session.unserialize(msg_list)
try:
msg['header'].pop('date')
except KeyError:
pass
try:
msg['parent_header'].pop('date')
except KeyError:
pass
msg.pop('buffers')
return json.dumps(msg, default=date_default)
msg = self.session.deserialize(msg_list)
if msg['buffers']:
buf = serialize_binary_message(msg)
return buf
else:
smsg = json.dumps(msg, default=date_default)
return cast_unicode(smsg)
def _on_zmq_reply(self, msg_list):
# Sometimes this gets triggered when the on_close method is scheduled in the
@ -104,18 +164,7 @@ class ZMQStreamHandler(websocket.WebSocketHandler):
except Exception:
self.log.critical("Malformed message: %r" % msg_list, exc_info=True)
else:
self.write_message(msg)
def allow_draft76(self):
"""Allow draft 76, until browsers such as Safari update to RFC 6455.
This has been disabled by default in tornado in release 2.2.0, and
support will be removed in later versions.
"""
return True
# ping interval for keeping websockets alive (30 seconds)
WS_PING_INTERVAL = 30000
self.write_message(msg, binary=isinstance(msg, bytes))
class AuthenticatedZMQStreamHandler(ZMQStreamHandler, IPythonHandler):
ping_callback = None
@ -146,18 +195,37 @@ class AuthenticatedZMQStreamHandler(ZMQStreamHandler, IPythonHandler):
which doesn't make sense for websockets
"""
pass
def open(self, kernel_id):
self.kernel_id = cast_unicode(kernel_id, 'ascii')
# Check to see that origin matches host directly, including ports
# Tornado 4 already does CORS checking
if tornado.version_info[0] < 4:
if not self.check_origin(self.get_origin()):
raise web.HTTPError(403)
def pre_get(self):
"""Run before finishing the GET request
Extend this method to add logic that should fire before
the websocket finishes completing.
"""
# authenticate the request before opening the websocket
if self.get_current_user() is None:
self.log.warn("Couldn't authenticate WebSocket connection")
raise web.HTTPError(403)
if self.get_argument('session_id', False):
self.session.session = cast_unicode(self.get_argument('session_id'))
else:
self.log.warn("No session ID specified")
@gen.coroutine
def get(self, *args, **kwargs):
# pre_get can be a coroutine in subclasses
# assign and yield in two step to avoid tornado 3 issues
res = self.pre_get()
yield gen.maybe_future(res)
super(AuthenticatedZMQStreamHandler, self).get(*args, **kwargs)
def initialize(self):
self.log.debug("Initializing websocket connection %s", self.request.path)
self.session = Session(config=self.config)
self.save_on_message = self.on_message
self.on_message = self.on_first_message
def open(self, *args, **kwargs):
self.log.debug("Opening websocket %s", self.request.path)
# start the pinging
if self.ping_interval > 0:
@ -187,28 +255,3 @@ class AuthenticatedZMQStreamHandler(ZMQStreamHandler, IPythonHandler):
def on_pong(self, data):
self.last_pong = ioloop.IOLoop.instance().time()
def _inject_cookie_message(self, msg):
"""Inject the first message, which is the document cookie,
for authentication."""
if not PY3 and isinstance(msg, unicode):
# Cookie constructor doesn't accept unicode strings
# under Python 2.x for some reason
msg = msg.encode('utf8', 'replace')
try:
identity, msg = msg.split(':', 1)
self.session.session = cast_unicode(identity, 'ascii')
except Exception:
logging.error("First ws message didn't have the form 'identity:[cookie]' - %r", msg)
try:
self.request._cookies = SimpleCookie(msg)
except:
self.log.warn("couldn't parse cookie string: %s",msg, exc_info=True)
def on_first_message(self, msg):
self._inject_cookie_message(msg)
if self.get_current_user() is None:
self.log.warn("Couldn't authenticate WebSocket connection")
raise web.HTTPError(403)
self.on_message = self.save_on_message

@ -0,0 +1,29 @@
#encoding: utf-8
"""Tornado handlers for the terminal emulator."""
# Copyright (c) IPython Development Team.
# Distributed under the terms of the Modified BSD License.
from tornado import web
from ..base.handlers import IPythonHandler, path_regex
from ..utils import url_escape
class EditorHandler(IPythonHandler):
"""Render the text editor interface."""
@web.authenticated
def get(self, path):
path = path.strip('/')
if not self.contents_manager.file_exists(path):
raise web.HTTPError(404, u'File does not exist: %s' % path)
basename = path.rsplit('/', 1)[-1]
self.write(self.render_template('edit.html',
file_path=url_escape(path),
basename=basename,
page_title=basename + " (editing)",
)
)
default_handlers = [
(r"/edit%s" % path_regex, EditorHandler),
]

@ -0,0 +1,54 @@
"""Serve files directly from the ContentsManager."""
# Copyright (c) IPython Development Team.
# Distributed under the terms of the Modified BSD License.
import os
import mimetypes
import json
import base64
from tornado import web
from IPython.html.base.handlers import IPythonHandler
class FilesHandler(IPythonHandler):
"""serve files via ContentsManager"""
@web.authenticated
def get(self, path):
cm = self.contents_manager
if cm.is_hidden(path):
self.log.info("Refusing to serve hidden file, via 404 Error")
raise web.HTTPError(404)
path = path.strip('/')
if '/' in path:
_, name = path.rsplit('/', 1)
else:
name = path
model = cm.get(path)
if self.get_argument("download", False):
self.set_header('Content-Disposition','attachment; filename="%s"' % name)
if model['type'] == 'notebook':
self.set_header('Content-Type', 'application/json')
else:
cur_mime = mimetypes.guess_type(name)[0]
if cur_mime is not None:
self.set_header('Content-Type', cur_mime)
if model['format'] == 'base64':
b64_bytes = model['content'].encode('ascii')
self.write(base64.decodestring(b64_bytes))
elif model['format'] == 'json':
self.write(json.dumps(model['content']))
else:
self.write(model['content'])
self.flush()
default_handlers = [
(r"/files/(.*)", FilesHandler),
]

@ -13,7 +13,7 @@ from ..base.handlers import (
IPythonHandler, FilesRedirectHandler,
notebook_path_regex, path_regex,
)
from IPython.nbformat.current import to_notebook_json
from IPython.nbformat import from_dict
from IPython.utils.py3compat import cast_bytes
@ -43,7 +43,7 @@ def respond_zip(handler, name, output, resources):
# Prepare the zip file
buffer = io.BytesIO()
zipf = zipfile.ZipFile(buffer, mode='w', compression=zipfile.ZIP_DEFLATED)
output_filename = os.path.splitext(name)[0] + '.' + resources['output_extension']
output_filename = os.path.splitext(name)[0] + resources['output_extension']
zipf.writestr(output_filename, cast_bytes(output, 'utf-8'))
for filename, data in output_files.items():
zipf.writestr(os.path.basename(filename), data)
@ -76,12 +76,13 @@ class NbconvertFileHandler(IPythonHandler):
SUPPORTED_METHODS = ('GET',)
@web.authenticated
def get(self, format, path='', name=None):
def get(self, format, path):
exporter = get_exporter(format, config=self.config, log=self.log)
path = path.strip('/')
model = self.contents_manager.get_model(name=name, path=path)
model = self.contents_manager.get(path=path)
name = model['name']
self.set_header('Last-Modified', model['last_modified'])
@ -95,7 +96,7 @@ class NbconvertFileHandler(IPythonHandler):
# Force download if requested
if self.get_argument('download', 'false').lower() == 'true':
filename = os.path.splitext(name)[0] + '.' + resources['output_extension']
filename = os.path.splitext(name)[0] + resources['output_extension']
self.set_header('Content-Disposition',
'attachment; filename="%s"' % filename)
@ -109,19 +110,20 @@ class NbconvertFileHandler(IPythonHandler):
class NbconvertPostHandler(IPythonHandler):
SUPPORTED_METHODS = ('POST',)
@web.authenticated
@web.authenticated
def post(self, format):
exporter = get_exporter(format, config=self.config)
model = self.get_json_body()
nbnode = to_notebook_json(model['content'])
name = model.get('name', 'notebook.ipynb')
nbnode = from_dict(model['content'])
try:
output, resources = exporter.from_notebook_node(nbnode)
except Exception as e:
raise web.HTTPError(500, "nbconvert failed: %s" % e)
if respond_zip(self, nbnode.metadata.name, output, resources):
if respond_zip(self, name, output, resources):
return
# MIME type

@ -10,9 +10,10 @@ import requests
from IPython.html.utils import url_path_join
from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
from IPython.nbformat.current import (new_notebook, write, new_worksheet,
new_heading_cell, new_code_cell,
new_output)
from IPython.nbformat import write
from IPython.nbformat.v4 import (
new_notebook, new_markdown_cell, new_code_cell, new_output,
)
from IPython.testing.decorators import onlyif_cmds_exist
@ -43,7 +44,8 @@ class NbconvertAPI(object):
png_green_pixel = base64.encodestring(b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00'
b'\x00\x00\x01\x00\x00x00\x01\x08\x02\x00\x00\x00\x90wS\xde\x00\x00\x00\x0cIDAT'
b'\x08\xd7c\x90\xfb\xcf\x00\x00\x02\\\x01\x1e.~d\x87\x00\x00\x00\x00IEND\xaeB`\x82')
b'\x08\xd7c\x90\xfb\xcf\x00\x00\x02\\\x01\x1e.~d\x87\x00\x00\x00\x00IEND\xaeB`\x82'
).decode('ascii')
class APITest(NotebookTestBase):
def setUp(self):
@ -52,19 +54,20 @@ class APITest(NotebookTestBase):
if not os.path.isdir(pjoin(nbdir, 'foo')):
os.mkdir(pjoin(nbdir, 'foo'))
nb = new_notebook(name='testnb')
nb = new_notebook()
ws = new_worksheet()
nb.worksheets = [ws]
ws.cells.append(new_heading_cell(u'Created by test ³'))
cc1 = new_code_cell(input=u'print(2*6)')
cc1.outputs.append(new_output(output_text=u'12', output_type='stream'))
cc1.outputs.append(new_output(output_png=png_green_pixel, output_type='pyout'))
ws.cells.append(cc1)
nb.cells.append(new_markdown_cell(u'Created by test ³'))
cc1 = new_code_cell(source=u'print(2*6)')
cc1.outputs.append(new_output(output_type="stream", text=u'12'))
cc1.outputs.append(new_output(output_type="execute_result",
data={'image/png' : png_green_pixel},
execution_count=1,
))
nb.cells.append(cc1)
with io.open(pjoin(nbdir, 'foo', 'testnb.ipynb'), 'w',
encoding='utf-8') as f:
write(nb, f, format='ipynb')
write(nb, f, version=4)
self.nbconvert_api = NbconvertAPI(self.base_url())

@ -93,7 +93,9 @@ def install_nbextension(files, overwrite=False, symlink=False, ipython_dir=None,
If True, always install the files, regardless of what may already be installed.
symlink : bool [default: False]
If True, create a symlink in nbextensions, rather than copying files.
Not allowed with URLs or archives.
Not allowed with URLs or archives. Windows support for symlinks requires
Vista or above, Python 3, and a permission bit which only admin users
have by default, so don't rely on it.
ipython_dir : str [optional]
The path to an IPython directory, if the default value is not desired.
get_ipython_dir() is used by default.
@ -147,7 +149,7 @@ def install_nbextension(files, overwrite=False, symlink=False, ipython_dir=None,
if overwrite and os.path.exists(dest):
if verbose >= 1:
print("removing %s" % dest)
if os.path.isdir(dest):
if os.path.isdir(dest) and not os.path.islink(dest):
shutil.rmtree(dest)
else:
os.remove(dest)

@ -17,18 +17,16 @@ from ..utils import url_escape
class NotebookHandler(IPythonHandler):
@web.authenticated
def get(self, path='', name=None):
def get(self, path):
"""get renders the notebook template if a name is given, or
redirects to the '/files/' handler if the name is not given."""
path = path.strip('/')
cm = self.contents_manager
if name is None:
raise web.HTTPError(500, "This shouldn't be accessible: %s" % self.request.uri)
# a .ipynb filename was given
if not cm.file_exists(name, path):
raise web.HTTPError(404, u'Notebook does not exist: %s/%s' % (path, name))
name = url_escape(name)
if not cm.file_exists(path):
raise web.HTTPError(404, u'Notebook does not exist: %s' % path)
name = url_escape(path.rsplit('/', 1)[-1])
path = url_escape(path)
self.write(self.render_template('notebook.html',
notebook_path=path,

@ -7,6 +7,7 @@
from __future__ import print_function
import base64
import datetime
import errno
import io
import json
@ -35,7 +36,7 @@ from zmq.eventloop import ioloop
ioloop.install()
# check for tornado 3.1.0
msg = "The IPython Notebook requires tornado >= 3.1.0"
msg = "The IPython Notebook requires tornado >= 4.0"
try:
import tornado
except ImportError:
@ -44,14 +45,17 @@ try:
version_info = tornado.version_info
except AttributeError:
raise ImportError(msg + ", but you have < 1.1.0")
if version_info < (3,1,0):
if version_info < (4,0):
raise ImportError(msg + ", but you have %s" % tornado.version)
from tornado import httpserver
from tornado import web
from tornado.log import LogFormatter
from tornado.log import LogFormatter, app_log, access_log, gen_log
from IPython.html import DEFAULT_STATIC_FILES_PATH
from IPython.html import (
DEFAULT_STATIC_FILES_PATH,
DEFAULT_TEMPLATE_PATH_LIST,
)
from .base.handlers import Template404
from .log import log_request
from .services.kernels.kernelmanager import MappingKernelManager
@ -81,6 +85,7 @@ from IPython.utils.traitlets import (
)
from IPython.utils import py3compat
from IPython.utils.path import filefind, get_ipython_dir
from IPython.utils.sysinfo import get_sys_info
from .utils import url_path_join
@ -122,37 +127,43 @@ def load_handlers(name):
class NotebookWebApplication(web.Application):
def __init__(self, ipython_app, kernel_manager, contents_manager,
cluster_manager, session_manager, kernel_spec_manager, log,
cluster_manager, session_manager, kernel_spec_manager,
config_manager, log,
base_url, default_url, settings_overrides, jinja_env_options):
settings = self.init_settings(
ipython_app, kernel_manager, contents_manager, cluster_manager,
session_manager, kernel_spec_manager, log, base_url, default_url,
settings_overrides, jinja_env_options)
session_manager, kernel_spec_manager, config_manager, log, base_url,
default_url, settings_overrides, jinja_env_options)
handlers = self.init_handlers(settings)
super(NotebookWebApplication, self).__init__(handlers, **settings)
def init_settings(self, ipython_app, kernel_manager, contents_manager,
cluster_manager, session_manager, kernel_spec_manager,
config_manager,
log, base_url, default_url, settings_overrides,
jinja_env_options=None):
# Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
# base_url will always be unicode, which will in turn
# make the patterns unicode, and ultimately result in unicode
# keys in kwargs to handler._execute(**kwargs) in tornado.
# This enforces that base_url be ascii in that situation.
#
# Note that the URLs these patterns check against are escaped,
# and thus guaranteed to be ASCII: 'héllo' is really 'h%C3%A9llo'.
base_url = py3compat.unicode_to_str(base_url, 'ascii')
_template_path = settings_overrides.get("template_path", os.path.join(os.path.dirname(__file__), "templates"))
_template_path = settings_overrides.get(
"template_path",
ipython_app.template_file_path,
)
if isinstance(_template_path, str):
_template_path = (_template_path,)
template_path = [os.path.expanduser(path) for path in _template_path]
jenv_opt = jinja_env_options if jinja_env_options else {}
env = Environment(loader=FileSystemLoader(template_path), **jenv_opt)
sys_info = get_sys_info()
if sys_info['commit_source'] == 'repository':
# don't cache (rely on 304) when working from master
version_hash = ''
else:
# reset the cache on server restart
version_hash = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
settings = dict(
# basics
log_function=log_request,
@ -162,6 +173,11 @@ class NotebookWebApplication(web.Application):
static_path=ipython_app.static_file_path,
static_handler_class = FileFindHandler,
static_url_prefix = url_path_join(base_url,'/static/'),
static_handler_args = {
# don't cache custom.js
'no_cache_paths': [url_path_join(base_url, 'static', 'custom')],
},
version_hash=version_hash,
# authentication
cookie_secret=ipython_app.cookie_secret,
@ -174,6 +190,7 @@ class NotebookWebApplication(web.Application):
cluster_manager=cluster_manager,
session_manager=session_manager,
kernel_spec_manager=kernel_spec_manager,
config_manager=config_manager,
# IPython stuff
nbextensions_path = ipython_app.nbextensions_path,
@ -181,6 +198,7 @@ class NotebookWebApplication(web.Application):
mathjax_url=ipython_app.mathjax_url,
config=ipython_app.config,
jinja2_env=env,
terminals_available=False, # Set later if terminals are available
)
# allow custom overrides for the tornado web app.
@ -188,30 +206,34 @@ class NotebookWebApplication(web.Application):
return settings
def init_handlers(self, settings):
# Load the (URL pattern, handler) tuples for each component.
"""Load the (URL pattern, handler) tuples for each component."""
# Order matters. The first handler to match the URL will handle the request.
handlers = []
handlers.extend(load_handlers('base.handlers'))
handlers.extend(load_handlers('tree.handlers'))
handlers.extend(load_handlers('auth.login'))
handlers.extend(load_handlers('auth.logout'))
handlers.extend(load_handlers('files.handlers'))
handlers.extend(load_handlers('notebook.handlers'))
handlers.extend(load_handlers('nbconvert.handlers'))
handlers.extend(load_handlers('kernelspecs.handlers'))
handlers.extend(load_handlers('edit.handlers'))
handlers.extend(load_handlers('services.config.handlers'))
handlers.extend(load_handlers('services.kernels.handlers'))
handlers.extend(load_handlers('services.contents.handlers'))
handlers.extend(load_handlers('services.clusters.handlers'))
handlers.extend(load_handlers('services.sessions.handlers'))
handlers.extend(load_handlers('services.nbconvert.handlers'))
handlers.extend(load_handlers('services.kernelspecs.handlers'))
# FIXME: /files/ should be handled by the Contents service when it exists
cm = settings['contents_manager']
if hasattr(cm, 'root_dir'):
handlers.append(
(r"/files/(.*)", AuthenticatedFileHandler, {'path' : cm.root_dir}),
)
handlers.extend(load_handlers('services.security.handlers'))
handlers.append(
(r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}),
(r"/nbextensions/(.*)", FileFindHandler, {
'path': settings['nbextensions_path'],
'no_cache_paths': ['/'], # don't cache anything in nbextensions
}),
)
# register base handlers last
handlers.extend(load_handlers('base.handlers'))
# set the URL that will be redirected from `/`
handlers.append(
(r'/?', web.RedirectHandler, {
@ -325,7 +347,7 @@ class NotebookApp(BaseIPythonApplication):
list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
)
kernel_argv = List(Unicode)
ipython_kernel_argv = List(Unicode)
_log_formatter_cls = LogFormatter
@ -345,11 +367,6 @@ class NotebookApp(BaseIPythonApplication):
# file to be opened in the notebook server
file_to_run = Unicode('', config=True)
def _file_to_run_changed(self, name, old, new):
path, base = os.path.split(new)
if path:
self.file_to_run = base
self.notebook_dir = path
# Network related information
@ -531,7 +548,20 @@ class NotebookApp(BaseIPythonApplication):
def static_file_path(self):
"""return extra paths + the default location"""
return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
extra_template_paths = List(Unicode, config=True,
help="""Extra paths to search for serving jinja templates.
Can be used to override templates from IPython.html.templates."""
)
def _extra_template_paths_default(self):
return []
@property
def template_file_path(self):
"""return extra paths + the default locations"""
return self.extra_template_paths + DEFAULT_TEMPLATE_PATH_LIST
nbextensions_path = List(Unicode, config=True,
help="""paths for Javascript extensions. By default, this is just IPYTHONDIR/nbextensions"""
)
@ -599,26 +629,38 @@ class NotebookApp(BaseIPythonApplication):
help='The cluster manager class to use.'
)
config_manager_class = DottedObjectName('IPython.html.services.config.manager.ConfigManager',
config = True,
help='The config manager class to use'
)
kernel_spec_manager = Instance(KernelSpecManager)
def _kernel_spec_manager_default(self):
return KernelSpecManager(ipython_dir=self.ipython_dir)
kernel_spec_manager_class = DottedObjectName('IPython.kernel.kernelspec.KernelSpecManager',
config=True,
help="""
The kernel spec manager class to use. Should be a subclass
of `IPython.kernel.kernelspec.KernelSpecManager`.
The Api of KernelSpecManager is provisional and might change
without warning between this version of IPython and the next stable one.
""")
trust_xheaders = Bool(False, config=True,
help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
"sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
)
info_file = Unicode()
def _info_file_default(self):
info_file = "nbserver-%s.json"%os.getpid()
return os.path.join(self.profile_dir.security_dir, info_file)
notebook_dir = Unicode(py3compat.getcwd(), config=True,
help="The directory to use for notebooks and kernels."
)
pylab = Unicode('disabled', config=True,
help="""
DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib.
@ -636,6 +678,16 @@ class NotebookApp(BaseIPythonApplication):
)
self.exit(1)
notebook_dir = Unicode(config=True,
help="The directory to use for notebooks and kernels."
)
def _notebook_dir_default(self):
if self.file_to_run:
return os.path.dirname(os.path.abspath(self.file_to_run))
else:
return py3compat.getcwd()
def _notebook_dir_changed(self, name, old, new):
"""Do a bit of validation of the notebook dir."""
if not os.path.isabs(new):
@ -671,16 +723,20 @@ class NotebookApp(BaseIPythonApplication):
self.update_config(c)
def init_kernel_argv(self):
"""construct the kernel arguments"""
"""add the profile-dir to arguments to be passed to IPython kernels"""
# FIXME: remove special treatment of IPython kernels
# Kernel should get *absolute* path to profile directory
self.kernel_argv = ["--profile-dir", self.profile_dir.location]
self.ipython_kernel_argv = ["--profile-dir", self.profile_dir.location]
def init_configurables(self):
# force Session default to be secure
default_secure(self.config)
kls = import_item(self.kernel_spec_manager_class)
self.kernel_spec_manager = kls(ipython_dir=self.ipython_dir)
kls = import_item(self.kernel_manager_class)
self.kernel_manager = kls(
parent=self, log=self.log, kernel_argv=self.kernel_argv,
parent=self, log=self.log, ipython_kernel_argv=self.ipython_kernel_argv,
connection_dir = self.profile_dir.security_dir,
)
kls = import_item(self.contents_manager_class)
@ -693,12 +749,19 @@ class NotebookApp(BaseIPythonApplication):
self.cluster_manager = kls(parent=self, log=self.log)
self.cluster_manager.update_profiles()
kls = import_item(self.config_manager_class)
self.config_manager = kls(parent=self, log=self.log,
profile_dir=self.profile_dir.location)
def init_logging(self):
# This prevents double log messages because tornado use a root logger that
# self.log is a child of. The logging module dipatches log messages to a log
# and all of its ancenstors until propagate is set to False.
self.log.propagate = False
for log in app_log, access_log, gen_log:
# consistent log output name (NotebookApp instead of tornado.access, etc.)
log.name = self.log.name
# hook up tornado 3's loggers to our app handlers
logger = logging.getLogger('tornado')
logger.propagate = True
@ -715,6 +778,7 @@ class NotebookApp(BaseIPythonApplication):
self.web_app = NotebookWebApplication(
self, self.kernel_manager, self.contents_manager,
self.cluster_manager, self.session_manager, self.kernel_spec_manager,
self.config_manager,
self.log, self.base_url, self.default_url, self.tornado_settings,
self.jinja_environment_options
)
@ -771,6 +835,14 @@ class NotebookApp(BaseIPythonApplication):
proto = 'https' if self.certfile else 'http'
return "%s://%s:%i%s" % (proto, ip, self.port, self.base_url)
def init_terminals(self):
try:
from .terminal import initialize
initialize(self.web_app)
self.web_app.settings['terminals_available'] = True
except ImportError as e:
self.log.info("Terminals not available (error was %s)", e)
def init_signal(self):
if not sys.platform.startswith('win'):
signal.signal(signal.SIGINT, self._handle_sigint)
@ -850,6 +922,7 @@ class NotebookApp(BaseIPythonApplication):
self.init_configurables()
self.init_components()
self.init_webapp()
self.init_terminals()
self.init_signal()
def cleanup_kernels(self):
@ -917,12 +990,12 @@ class NotebookApp(BaseIPythonApplication):
browser = None
if self.file_to_run:
fullpath = os.path.join(self.notebook_dir, self.file_to_run)
if not os.path.exists(fullpath):
self.log.critical("%s does not exist" % fullpath)
if not os.path.exists(self.file_to_run):
self.log.critical("%s does not exist" % self.file_to_run)
self.exit(1)
uri = url_path_join('notebooks', self.file_to_run)
relpath = os.path.relpath(self.file_to_run, self.notebook_dir)
uri = url_path_join('notebooks', *relpath.split(os.sep))
else:
uri = 'tree'
if browser:

@ -1,44 +1,23 @@
"""Manage IPython.parallel clusters in the notebook.
"""Manage IPython.parallel clusters in the notebook."""
Authors:
* Brian Granger
"""
#-----------------------------------------------------------------------------
# Copyright (C) 2008-2011 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
#-----------------------------------------------------------------------------
# Copyright (c) IPython Development Team.
# Distributed under the terms of the Modified BSD License.
from tornado import web
from zmq.eventloop import ioloop
from IPython.config.configurable import LoggingConfigurable
from IPython.utils.traitlets import Dict, Instance, CFloat
from IPython.utils.traitlets import Dict, Instance, Float
from IPython.core.profileapp import list_profiles_in
from IPython.core.profiledir import ProfileDir
from IPython.utils import py3compat
from IPython.utils.path import get_ipython_dir
#-----------------------------------------------------------------------------
# Classes
#-----------------------------------------------------------------------------
class ClusterManager(LoggingConfigurable):
profiles = Dict()
delay = CFloat(1., config=True,
delay = Float(1., config=True,
help="delay (in s) between starting the controller and the engines")
loop = Instance('zmq.eventloop.ioloop.IOLoop')
@ -75,16 +54,24 @@ class ClusterManager(LoggingConfigurable):
def update_profiles(self):
"""List all profiles in the ipython_dir and cwd.
"""
stale = set(self.profiles)
for path in [get_ipython_dir(), py3compat.getcwd()]:
for profile in list_profiles_in(path):
if profile in stale:
stale.remove(profile)
pd = self.get_profile_dir(profile, path)
if profile not in self.profiles:
self.log.debug("Adding cluster profile '%s'" % profile)
self.log.debug("Adding cluster profile '%s'", profile)
self.profiles[profile] = {
'profile': profile,
'profile_dir': pd,
'status': 'stopped'
}
for profile in stale:
# remove profiles that no longer exist
self.log.debug("Profile '%s' no longer exists", profile)
self.profiles.pop(stale)
def list_profiles(self):
self.update_profiles()
@ -133,11 +120,13 @@ class ClusterManager(LoggingConfigurable):
esl.stop()
clean_data()
cl.on_stop(controller_stopped)
dc = ioloop.DelayedCallback(lambda: cl.start(), 0, self.loop)
dc.start()
dc = ioloop.DelayedCallback(lambda: esl.start(n), 1000*self.delay, self.loop)
dc.start()
loop = self.loop
def start():
"""start the controller, then the engines after a delay"""
cl.start()
loop.add_timeout(self.loop.time() + self.delay, lambda : esl.start(n))
self.loop.add_callback(start)
self.log.debug('Cluster started')
data['controller_launcher'] = cl

@ -0,0 +1 @@
from .manager import ConfigManager

@ -0,0 +1,44 @@
"""Tornado handlers for frontend config storage."""
# Copyright (c) IPython Development Team.
# Distributed under the terms of the Modified BSD License.
import json
import os
import io
import errno
from tornado import web
from IPython.utils.py3compat import PY3
from ...base.handlers import IPythonHandler, json_errors
class ConfigHandler(IPythonHandler):
SUPPORTED_METHODS = ('GET', 'PUT', 'PATCH')
@web.authenticated
@json_errors
def get(self, section_name):
self.set_header("Content-Type", 'application/json')
self.finish(json.dumps(self.config_manager.get(section_name)))
@web.authenticated
@json_errors
def put(self, section_name):
data = self.get_json_body() # Will raise 400 if content is not valid JSON
self.config_manager.set(section_name, data)
self.set_status(204)
@web.authenticated
@json_errors
def patch(self, section_name):
new_data = self.get_json_body()
section = self.config_manager.update(section_name, new_data)
self.finish(json.dumps(section))
# URL to handler mappings
section_name_regex = r"(?P<section_name>\w+)"
default_handlers = [
(r"/api/config/%s" % section_name_regex, ConfigHandler),
]

@ -0,0 +1,90 @@
"""Manager to read and modify frontend config data in JSON files.
"""
# Copyright (c) IPython Development Team.
# Distributed under the terms of the Modified BSD License.
import errno
import io
import json
import os
from IPython.config import LoggingConfigurable
from IPython.utils.path import locate_profile
from IPython.utils.py3compat import PY3
from IPython.utils.traitlets import Unicode
def recursive_update(target, new):
"""Recursively update one dictionary using another.
None values will delete their keys.
"""
for k, v in new.items():
if isinstance(v, dict):
if k not in target:
target[k] = {}
recursive_update(target[k], v)
if not target[k]:
# Prune empty subdicts
del target[k]
elif v is None:
target.pop(k, None)
else:
target[k] = v
class ConfigManager(LoggingConfigurable):
profile_dir = Unicode()
def _profile_dir_default(self):
return locate_profile()
@property
def config_dir(self):
return os.path.join(self.profile_dir, 'nbconfig')
def ensure_config_dir_exists(self):
try:
os.mkdir(self.config_dir, 0o755)
except OSError as e:
if e.errno != errno.EEXIST:
raise
def file_name(self, section_name):
return os.path.join(self.config_dir, section_name+'.json')
def get(self, section_name):
"""Retrieve the config data for the specified section.
Returns the data as a dictionary, or an empty dictionary if the file
doesn't exist.
"""
filename = self.file_name(section_name)
if os.path.isfile(filename):
with io.open(filename, encoding='utf-8') as f:
return json.load(f)
else:
return {}
def set(self, section_name, data):
"""Store the given config data.
"""
filename = self.file_name(section_name)
self.ensure_config_dir_exists()
if PY3:
f = io.open(filename, 'w', encoding='utf-8')
else:
f = open(filename, 'wb')
with f:
json.dump(data, f)
def update(self, section_name, new_data):
"""Modify the config section by recursively updating it with new_data.
Returns the modified config data as a dictionary.
"""
data = self.get(section_name)
recursive_update(data, new_data)
self.set(section_name, data)
return data

@ -0,0 +1,68 @@
# coding: utf-8
"""Test the config webservice API."""
import json
import requests
from IPython.html.utils import url_path_join
from IPython.html.tests.launchnotebook import NotebookTestBase
class ConfigAPI(object):
"""Wrapper for notebook API calls."""
def __init__(self, base_url):
self.base_url = base_url
def _req(self, verb, section, body=None):
response = requests.request(verb,
url_path_join(self.base_url, 'api/config', section),
data=body,
)
response.raise_for_status()
return response
def get(self, section):
return self._req('GET', section)
def set(self, section, values):
return self._req('PUT', section, json.dumps(values))
def modify(self, section, values):
return self._req('PATCH', section, json.dumps(values))
class APITest(NotebookTestBase):
"""Test the config web service API"""
def setUp(self):
self.config_api = ConfigAPI(self.base_url())
def test_create_retrieve_config(self):
sample = {'foo': 'bar', 'baz': 73}
r = self.config_api.set('example', sample)
self.assertEqual(r.status_code, 204)
r = self.config_api.get('example')
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json(), sample)
def test_modify(self):
sample = {'foo': 'bar', 'baz': 73,
'sub': {'a': 6, 'b': 7}, 'sub2': {'c': 8}}
self.config_api.set('example', sample)
r = self.config_api.modify('example', {'foo': None, # should delete foo
'baz': 75,
'wib': [1,2,3],
'sub': {'a': 8, 'b': None, 'd': 9},
'sub2': {'c': None} # should delete sub2
})
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json(), {'baz': 75, 'wib': [1,2,3],
'sub': {'a': 8, 'd': 9}})
def test_get_unknown(self):
# We should get an empty config dictionary instead of a 404
r = self.config_api.get('nonexistant')
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json(), {})

@ -4,27 +4,66 @@
# Distributed under the terms of the Modified BSD License.
import base64
import errno
import io
import os
import glob
import shutil
from contextlib import contextmanager
import mimetypes
from tornado import web
from .manager import ContentsManager
from IPython.nbformat import current
from IPython import nbformat
from IPython.utils.io import atomic_writing
from IPython.utils.path import ensure_dir_exists
from IPython.utils.traitlets import Unicode, Bool, TraitError
from IPython.utils.py3compat import getcwd
from IPython.utils.py3compat import getcwd, str_to_unicode
from IPython.utils import tz
from IPython.html.utils import is_hidden, to_os_path, url_path_join
from IPython.html.utils import is_hidden, to_os_path, to_api_path
class FileContentsManager(ContentsManager):
root_dir = Unicode(getcwd(), config=True)
root_dir = Unicode(config=True)
def _root_dir_default(self):
try:
return self.parent.notebook_dir
except AttributeError:
return getcwd()
@contextmanager
def perm_to_403(self, os_path=''):
"""context manager for turning permission errors into 403"""
try:
yield
except OSError as e:
if e.errno in {errno.EPERM, errno.EACCES}:
# make 403 error message without root prefix
# this may not work perfectly on unicode paths on Python 2,
# but nobody should be doing that anyway.
if not os_path:
os_path = str_to_unicode(e.filename or 'unknown file')
path = to_api_path(os_path, self.root_dir)
raise web.HTTPError(403, u'Permission denied: %s' % path)
else:
raise
@contextmanager
def open(self, os_path, *args, **kwargs):
"""wrapper around io.open that turns permission errors into 403"""
with self.perm_to_403(os_path):
with io.open(os_path, *args, **kwargs) as f:
yield f
@contextmanager
def atomic_writing(self, os_path, *args, **kwargs):
"""wrapper around atomic_writing that turns permission errors into 403"""
with self.perm_to_403(os_path):
with atomic_writing(os_path, *args, **kwargs) as f:
yield f
save_script = Bool(False, config=True, help='DEPRECATED, IGNORED')
def _save_script_changed(self):
self.log.warn("""
@ -61,27 +100,22 @@ class FileContentsManager(ContentsManager):
except OSError as e:
self.log.debug("copystat on %s failed", dest, exc_info=True)
def _get_os_path(self, name=None, path=''):
"""Given a filename and API path, return its file system
path.
def _get_os_path(self, path):
"""Given an API path, return its file system path.
Parameters
----------
name : string
A filename
path : string
The relative API path to the named file.
Returns
-------
path : string
API path to be evaluated relative to root_dir.
Native, absolute OS path to for a file.
"""
if name is not None:
path = url_path_join(path, name)
return to_os_path(path, self.root_dir)
def path_exists(self, path):
def dir_exists(self, path):
"""Does the API-style path refer to an extant directory?
API-style wrapper for os.path.isdir
@ -112,25 +146,22 @@ class FileContentsManager(ContentsManager):
Returns
-------
exists : bool
Whether the path is hidden.
hidden : bool
Whether the path exists and is hidden.
"""
path = path.strip('/')
os_path = self._get_os_path(path=path)
return is_hidden(os_path, self.root_dir)
def file_exists(self, name, path=''):
def file_exists(self, path):
"""Returns True if the file exists, else returns False.
API-style wrapper for os.path.isfile
Parameters
----------
name : string
The name of the file you are checking.
path : string
The relative path to the file's directory (with '/' as separator)
The relative path to the file (with '/' as separator)
Returns
-------
@ -138,20 +169,18 @@ class FileContentsManager(ContentsManager):
Whether the file exists.
"""
path = path.strip('/')
nbpath = self._get_os_path(name, path=path)
return os.path.isfile(nbpath)
os_path = self._get_os_path(path)
return os.path.isfile(os_path)
def exists(self, name=None, path=''):
"""Returns True if the path [and name] exists, else returns False.
def exists(self, path):
"""Returns True if the path exists, else returns False.
API-style wrapper for os.path.exists
Parameters
----------
name : string
The name of the file you are checking.
path : string
The relative path to the file's directory (with '/' as separator)
The API path to the file (with '/' as separator)
Returns
-------
@ -159,33 +188,39 @@ class FileContentsManager(ContentsManager):
Whether the target exists.
"""
path = path.strip('/')
os_path = self._get_os_path(name, path=path)
os_path = self._get_os_path(path=path)
return os.path.exists(os_path)
def _base_model(self, name, path=''):
def _base_model(self, path):
"""Build the common base of a contents model"""
os_path = self._get_os_path(name, path)
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)
# Create the base model.
model = {}
model['name'] = name
model['name'] = path.rsplit('/', 1)[-1]
model['path'] = path
model['last_modified'] = last_modified
model['created'] = created
model['content'] = None
model['format'] = None
model['mimetype'] = None
try:
model['writable'] = os.access(os_path, os.W_OK)
except OSError:
self.log.error("Failed to check write permissions on %s", os_path)
model['writable'] = False
return model
def _dir_model(self, name, path='', content=True):
def _dir_model(self, path, content=True):
"""Build a model for a directory
if content is requested, will include a listing of the directory
"""
os_path = self._get_os_path(name, path)
os_path = self._get_os_path(path)
four_o_four = u'directory does not exist: %r' % os_path
four_o_four = u'directory does not exist: %r' % path
if not os.path.isdir(os_path):
raise web.HTTPError(404, four_o_four)
@ -195,80 +230,105 @@ class FileContentsManager(ContentsManager):
)
raise web.HTTPError(404, four_o_four)
if name is None:
if '/' in path:
path, name = path.rsplit('/', 1)
else:
name = ''
model = self._base_model(name, path)
model = self._base_model(path)
model['type'] = 'directory'
dir_path = u'{}/{}'.format(path, name)
if content:
model['content'] = contents = []
for os_path in glob.glob(self._get_os_path('*', dir_path)):
name = os.path.basename(os_path)
os_dir = self._get_os_path(path)
for name in os.listdir(os_dir):
os_path = os.path.join(os_dir, name)
# skip over broken symlinks in listing
if not os.path.exists(os_path):
self.log.warn("%s doesn't exist", os_path)
continue
elif not os.path.isfile(os_path) and not os.path.isdir(os_path):
self.log.debug("%s not a regular file", os_path)
continue
if self.should_list(name) and not is_hidden(os_path, self.root_dir):
contents.append(self.get_model(name=name, path=dir_path, content=False))
contents.append(self.get(
path='%s/%s' % (path, name),
content=False)
)
model['format'] = 'json'
return model
def _file_model(self, name, path='', content=True):
def _file_model(self, path, content=True, format=None):
"""Build a model for a file
if content is requested, include the file contents.
UTF-8 text files will be unicode, binary files will be base64-encoded.
format:
If 'text', the contents will be decoded as UTF-8.
If 'base64', the raw bytes contents will be encoded as base64.
If not specified, try to decode as UTF-8, and fall back to base64
"""
model = self._base_model(name, path)
model = self._base_model(path)
model['type'] = 'file'
os_path = self._get_os_path(path)
model['mimetype'] = mimetypes.guess_type(os_path)[0] or 'text/plain'
if content:
os_path = self._get_os_path(name, path)
with io.open(os_path, 'rb') as f:
if not os.path.isfile(os_path):
# could be FIFO
raise web.HTTPError(400, "Cannot get content of non-file %s" % os_path)
with self.open(os_path, 'rb') as f:
bcontent = f.read()
try:
model['content'] = bcontent.decode('utf8')
except UnicodeError as e:
if format != 'base64':
try:
model['content'] = bcontent.decode('utf8')
except UnicodeError as e:
if format == 'text':
raise web.HTTPError(400, "%s is not UTF-8 encoded" % path)
else:
model['format'] = 'text'
if model['content'] is None:
model['content'] = base64.encodestring(bcontent).decode('ascii')
model['format'] = 'base64'
else:
model['format'] = 'text'
return model
def _notebook_model(self, name, path='', content=True):
def _notebook_model(self, path, content=True):
"""Build a notebook model
if content is requested, the notebook content will be populated
as a JSON structure (not double-serialized)
"""
model = self._base_model(name, path)
model = self._base_model(path)
model['type'] = 'notebook'
if content:
os_path = self._get_os_path(name, path)
with io.open(os_path, 'r', encoding='utf-8') as f:
os_path = self._get_os_path(path)
with self.open(os_path, 'r', encoding='utf-8') as f:
try:
nb = current.read(f, u'json')
nb = nbformat.read(f, as_version=4)
except Exception as e:
raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e))
self.mark_trusted_cells(nb, name, path)
raise web.HTTPError(400, u"Unreadable Notebook: %s %r" % (os_path, e))
self.mark_trusted_cells(nb, path)
model['content'] = nb
model['format'] = 'json'
self.validate_notebook_model(model)
return model
def get_model(self, name, path='', content=True):
""" Takes a path and name for an entity and returns its model
def get(self, path, content=True, type_=None, format=None):
""" Takes a path for an entity and returns its model
Parameters
----------
name : str
the name of the target
path : str
the API path that describes the relative path for the target
content : bool
Whether to include the contents in the reply
type_ : str, optional
The requested type - 'file', 'notebook', or 'directory'.
Will raise HTTPError 400 if the content doesn't match.
format : str, optional
The requested format for file contents. 'text' or 'base64'.
Ignored if this returns a notebook or directory model.
Returns
-------
@ -278,32 +338,35 @@ class FileContentsManager(ContentsManager):
"""
path = path.strip('/')
if not self.exists(name=name, path=path):
raise web.HTTPError(404, u'No such file or directory: %s/%s' % (path, name))
if not self.exists(path):
raise web.HTTPError(404, u'No such file or directory: %s' % path)
os_path = self._get_os_path(name, path)
os_path = self._get_os_path(path)
if os.path.isdir(os_path):
model = self._dir_model(name, path, content)
elif name.endswith('.ipynb'):
model = self._notebook_model(name, path, content)
if type_ not in (None, 'directory'):
raise web.HTTPError(400,
u'%s is a directory, not a %s' % (path, type_))
model = self._dir_model(path, content=content)
elif type_ == 'notebook' or (type_ is None and path.endswith('.ipynb')):
model = self._notebook_model(path, content=content)
else:
model = self._file_model(name, path, content)
if type_ == 'directory':
raise web.HTTPError(400,
u'%s is not a directory')
model = self._file_model(path, content=content, format=format)
return model
def _save_notebook(self, os_path, model, name='', path=''):
def _save_notebook(self, os_path, model, path=''):
"""save a notebook file"""
# Save the notebook file
nb = current.to_notebook_json(model['content'])
nb = nbformat.from_dict(model['content'])
self.check_and_sign(nb, name, path)
self.check_and_sign(nb, path)
if 'name' in nb['metadata']:
nb['metadata']['name'] = u''
with self.atomic_writing(os_path, encoding='utf-8') as f:
nbformat.write(nb, f, version=nbformat.NO_CONVERT)
with atomic_writing(os_path, encoding='utf-8') as f:
current.write(nb, f, u'json')
def _save_file(self, os_path, model, name='', path=''):
def _save_file(self, os_path, model, path=''):
"""save a non-notebook file"""
fmt = model.get('format', None)
if fmt not in {'text', 'base64'}:
@ -317,21 +380,22 @@ class FileContentsManager(ContentsManager):
bcontent = base64.decodestring(b64_bytes)
except Exception as e:
raise web.HTTPError(400, u'Encoding error saving %s: %s' % (os_path, e))
with atomic_writing(os_path, text=False) as f:
with self.atomic_writing(os_path, text=False) as f:
f.write(bcontent)
def _save_directory(self, os_path, model, name='', path=''):
def _save_directory(self, os_path, model, path=''):
"""create a directory"""
if is_hidden(os_path, self.root_dir):
raise web.HTTPError(400, u'Cannot create hidden directory %r' % os_path)
if not os.path.exists(os_path):
os.mkdir(os_path)
with self.perm_to_403():
os.mkdir(os_path)
elif not os.path.isdir(os_path):
raise web.HTTPError(400, u'Not a directory: %s' % (os_path))
else:
self.log.debug("Directory %r already exists", os_path)
def save(self, model, name='', path=''):
def save(self, model, path=''):
"""Save the file model and return the model with no content."""
path = path.strip('/')
@ -341,52 +405,53 @@ class FileContentsManager(ContentsManager):
raise web.HTTPError(400, u'No file content provided')
# One checkpoint should always exist
if self.file_exists(name, path) and not self.list_checkpoints(name, path):
self.create_checkpoint(name, path)
new_path = model.get('path', path).strip('/')
new_name = model.get('name', name)
if self.file_exists(path) and not self.list_checkpoints(path):
self.create_checkpoint(path)
if path != new_path or name != new_name:
self.rename(name, path, new_name, new_path)
os_path = self._get_os_path(new_name, new_path)
os_path = self._get_os_path(path)
self.log.debug("Saving %s", os_path)
try:
if model['type'] == 'notebook':
self._save_notebook(os_path, model, new_name, new_path)
self._save_notebook(os_path, model, path)
elif model['type'] == 'file':
self._save_file(os_path, model, new_name, new_path)
self._save_file(os_path, model, path)
elif model['type'] == 'directory':
self._save_directory(os_path, model, new_name, new_path)
self._save_directory(os_path, model, path)
else:
raise web.HTTPError(400, "Unhandled contents type: %s" % model['type'])
except web.HTTPError:
raise
except Exception as e:
raise web.HTTPError(400, u'Unexpected error while saving file: %s %s' % (os_path, 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))
validation_message = None
if model['type'] == 'notebook':
self.validate_notebook_model(model)
validation_message = model.get('message', None)
model = self.get_model(new_name, new_path, content=False)
model = self.get(path, content=False)
if validation_message:
model['message'] = validation_message
return model
def update(self, model, name, path=''):
"""Update the file's path and/or name
def update(self, model, path):
"""Update the file's path
For use in PATCH requests, to enable renaming a file without
re-uploading its contents. Only used for renaming at the moment.
"""
path = path.strip('/')
new_name = model.get('name', name)
new_path = model.get('path', path).strip('/')
if path != new_path or name != new_name:
self.rename(name, path, new_name, new_path)
model = self.get_model(new_name, new_path, content=False)
if path != new_path:
self.rename(path, new_path)
model = self.get(new_path, content=False)
return model
def delete(self, name, path=''):
"""Delete file by name and path."""
def delete(self, path):
"""Delete file at path."""
path = path.strip('/')
os_path = self._get_os_path(name, path)
os_path = self._get_os_path(path)
rm = os.unlink
if os.path.isdir(os_path):
listing = os.listdir(os_path)
@ -397,71 +462,81 @@ class FileContentsManager(ContentsManager):
raise web.HTTPError(404, u'File does not exist: %s' % os_path)
# clear checkpoints
for checkpoint in self.list_checkpoints(name, path):
for checkpoint in self.list_checkpoints(path):
checkpoint_id = checkpoint['id']
cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
cp_path = self.get_checkpoint_path(checkpoint_id, path)
if os.path.isfile(cp_path):
self.log.debug("Unlinking checkpoint %s", cp_path)
os.unlink(cp_path)
with self.perm_to_403():
rm(cp_path)
if os.path.isdir(os_path):
self.log.debug("Removing directory %s", os_path)
shutil.rmtree(os_path)
with self.perm_to_403():
shutil.rmtree(os_path)
else:
self.log.debug("Unlinking file %s", os_path)
rm(os_path)
with self.perm_to_403():
rm(os_path)
def rename(self, old_name, old_path, new_name, new_path):
def rename(self, old_path, new_path):
"""Rename a file."""
old_path = old_path.strip('/')
new_path = new_path.strip('/')
if new_name == old_name and new_path == old_path:
if new_path == old_path:
return
new_os_path = self._get_os_path(new_name, new_path)
old_os_path = self._get_os_path(old_name, old_path)
new_os_path = self._get_os_path(new_path)
old_os_path = self._get_os_path(old_path)
# Should we proceed with the move?
if os.path.isfile(new_os_path):
raise web.HTTPError(409, u'File with name already exists: %s' % new_os_path)
if os.path.exists(new_os_path):
raise web.HTTPError(409, u'File already exists: %s' % new_path)
# Move the file
try:
shutil.move(old_os_path, new_os_path)
with self.perm_to_403():
shutil.move(old_os_path, new_os_path)
except web.HTTPError:
raise
except Exception as e:
raise web.HTTPError(500, u'Unknown error renaming file: %s %s' % (old_os_path, e))
raise web.HTTPError(500, u'Unknown error renaming file: %s %s' % (old_path, e))
# Move the checkpoints
old_checkpoints = self.list_checkpoints(old_name, old_path)
old_checkpoints = self.list_checkpoints(old_path)
for cp in old_checkpoints:
checkpoint_id = cp['id']
old_cp_path = self.get_checkpoint_path(checkpoint_id, old_name, old_path)
new_cp_path = self.get_checkpoint_path(checkpoint_id, new_name, new_path)
old_cp_path = self.get_checkpoint_path(checkpoint_id, old_path)
new_cp_path = self.get_checkpoint_path(checkpoint_id, new_path)
if os.path.isfile(old_cp_path):
self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
shutil.move(old_cp_path, new_cp_path)
with self.perm_to_403():
shutil.move(old_cp_path, new_cp_path)
# Checkpoint-related utilities
def get_checkpoint_path(self, checkpoint_id, name, path=''):
def get_checkpoint_path(self, checkpoint_id, path):
"""find the path to a checkpoint"""
path = path.strip('/')
parent, name = ('/' + path).rsplit('/', 1)
parent = parent.strip('/')
basename, ext = os.path.splitext(name)
filename = u"{name}-{checkpoint_id}{ext}".format(
name=basename,
checkpoint_id=checkpoint_id,
ext=ext,
)
os_path = self._get_os_path(path=path)
os_path = self._get_os_path(path=parent)
cp_dir = os.path.join(os_path, self.checkpoint_dir)
ensure_dir_exists(cp_dir)
with self.perm_to_403():
ensure_dir_exists(cp_dir)
cp_path = os.path.join(cp_dir, filename)
return cp_path
def get_checkpoint_model(self, checkpoint_id, name, path=''):
def get_checkpoint_model(self, checkpoint_id, path):
"""construct the info dict for a given checkpoint"""
path = path.strip('/')
cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
cp_path = self.get_checkpoint_path(checkpoint_id, path)
stats = os.stat(cp_path)
last_modified = tz.utcfromtimestamp(stats.st_mtime)
info = dict(
@ -472,58 +547,62 @@ class FileContentsManager(ContentsManager):
# public checkpoint API
def create_checkpoint(self, name, path=''):
def create_checkpoint(self, path):
"""Create a checkpoint from the current state of a file"""
path = path.strip('/')
src_path = self._get_os_path(name, path)
if not self.file_exists(path):
raise web.HTTPError(404)
src_path = self._get_os_path(path)
# only the one checkpoint ID:
checkpoint_id = u"checkpoint"
cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
self.log.debug("creating checkpoint for %s", name)
self._copy(src_path, cp_path)
cp_path = self.get_checkpoint_path(checkpoint_id, path)
self.log.debug("creating checkpoint for %s", path)
with self.perm_to_403():
self._copy(src_path, cp_path)
# return the checkpoint info
return self.get_checkpoint_model(checkpoint_id, name, path)
return self.get_checkpoint_model(checkpoint_id, path)
def list_checkpoints(self, name, path=''):
def list_checkpoints(self, path):
"""list the checkpoints for a given file
This contents manager currently only supports one checkpoint per file.
"""
path = path.strip('/')
checkpoint_id = "checkpoint"
os_path = self.get_checkpoint_path(checkpoint_id, name, path)
os_path = self.get_checkpoint_path(checkpoint_id, path)
if not os.path.exists(os_path):
return []
else:
return [self.get_checkpoint_model(checkpoint_id, name, path)]
return [self.get_checkpoint_model(checkpoint_id, path)]
def restore_checkpoint(self, checkpoint_id, name, path=''):
def restore_checkpoint(self, checkpoint_id, path):
"""restore a file to a checkpointed state"""
path = path.strip('/')
self.log.info("restoring %s from checkpoint %s", name, checkpoint_id)
nb_path = self._get_os_path(name, path)
cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
self.log.info("restoring %s from checkpoint %s", path, checkpoint_id)
nb_path = self._get_os_path(path)
cp_path = self.get_checkpoint_path(checkpoint_id, path)
if not os.path.isfile(cp_path):
self.log.debug("checkpoint file does not exist: %s", cp_path)
raise web.HTTPError(404,
u'checkpoint does not exist: %s-%s' % (name, checkpoint_id)
u'checkpoint does not exist: %s@%s' % (path, checkpoint_id)
)
# ensure notebook is readable (never restore from an unreadable notebook)
if cp_path.endswith('.ipynb'):
with io.open(cp_path, 'r', encoding='utf-8') as f:
current.read(f, u'json')
self._copy(cp_path, nb_path)
with self.open(cp_path, 'r', encoding='utf-8') as f:
nbformat.read(f, as_version=4)
self.log.debug("copying %s -> %s", cp_path, nb_path)
with self.perm_to_403():
self._copy(cp_path, nb_path)
def delete_checkpoint(self, checkpoint_id, name, path=''):
def delete_checkpoint(self, checkpoint_id, path):
"""delete a file's checkpoint"""
path = path.strip('/')
cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
cp_path = self.get_checkpoint_path(checkpoint_id, path)
if not os.path.isfile(cp_path):
raise web.HTTPError(404,
u'Checkpoint does not exist: %s%s-%s' % (path, name, checkpoint_id)
u'Checkpoint does not exist: %s@%s' % (path, checkpoint_id)
)
self.log.debug("unlinking %s", cp_path)
os.unlink(cp_path)
@ -531,6 +610,10 @@ class FileContentsManager(ContentsManager):
def info_string(self):
return "Serving notebooks from local directory: %s" % self.root_dir
def get_kernel_path(self, name, path='', model=None):
"""Return the initial working dir a kernel associated with a given notebook"""
return os.path.join(self.root_dir, path)
def get_kernel_path(self, path, model=None):
"""Return the initial API path of a kernel associated with a given notebook"""
if '/' in path:
parent_dir = path.rsplit('/', 1)[0]
else:
parent_dir = ''
return parent_dir

@ -10,9 +10,9 @@ from tornado import web
from IPython.html.utils import url_path_join, url_escape
from IPython.utils.jsonutil import date_default
from IPython.html.base.handlers import (IPythonHandler, json_errors,
file_path_regex, path_regex,
file_name_regex)
from IPython.html.base.handlers import (
IPythonHandler, json_errors, path_regex,
)
def sort_key(model):
@ -29,38 +29,44 @@ class ContentsHandler(IPythonHandler):
SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE')
def location_url(self, name, path):
def location_url(self, path):
"""Return the full URL location of a file.
Parameters
----------
name : unicode
The base name of the file, such as "foo.ipynb".
path : unicode
The API path of the file, such as "foo/bar".
The API path of the file, such as "foo/bar.txt".
"""
return url_escape(url_path_join(
self.base_url, 'api', 'contents', path, name
self.base_url, 'api', 'contents', path
))
def _finish_model(self, model, location=True):
"""Finish a JSON request with a model, setting relevant headers, etc."""
if location:
location = self.location_url(model['name'], model['path'])
location = self.location_url(model['path'])
self.set_header('Location', location)
self.set_header('Last-Modified', model['last_modified'])
self.finish(json.dumps(model, default=date_default))
@web.authenticated
@json_errors
def get(self, path='', name=None):
def get(self, path=''):
"""Return a model for a file or directory.
A directory model contains a list of models (without content)
of the files and directories it contains.
"""
path = path or ''
model = self.contents_manager.get_model(name=name, path=path)
type_ = self.get_query_argument('type', default=None)
if type_ not in {None, 'directory', 'file', 'notebook'}:
raise web.HTTPError(400, u'Type %r is invalid' % type_)
format = self.get_query_argument('format', default=None)#
if format not in {None, 'text', 'base64'}:
raise web.HTTPError(400, u'Format %r is invalid' % format)
model = self.contents_manager.get(path=path, type_=type_, format=format)
if model['type'] == 'directory':
# group listing by type, then by name (case-insensitive)
# FIXME: sorting should be done in the frontends
@ -69,112 +75,83 @@ class ContentsHandler(IPythonHandler):
@web.authenticated
@json_errors
def patch(self, path='', name=None):
"""PATCH renames a notebook without re-uploading content."""
def patch(self, path=''):
"""PATCH renames a file or directory without re-uploading content."""
cm = self.contents_manager
if name is None:
raise web.HTTPError(400, u'Filename missing')
model = self.get_json_body()
if model is None:
raise web.HTTPError(400, u'JSON body missing')
model = cm.update(model, name, path)
model = cm.update(model, path)
self._finish_model(model)
def _copy(self, copy_from, path, copy_to=None):
"""Copy a file, optionally specifying the new name.
"""
self.log.info(u"Copying {copy_from} to {path}/{copy_to}".format(
def _copy(self, copy_from, copy_to=None):
"""Copy a file, optionally specifying a target directory."""
self.log.info(u"Copying {copy_from} to {copy_to}".format(
copy_from=copy_from,
path=path,
copy_to=copy_to or '',
))
model = self.contents_manager.copy(copy_from, copy_to, path)
model = self.contents_manager.copy(copy_from, copy_to)
self.set_status(201)
self._finish_model(model)
def _upload(self, model, path, name=None):
"""Handle upload of a new file
If name specified, create it in path/name,
otherwise create a new untitled file in path.
"""
self.log.info(u"Uploading file to %s/%s", path, name or '')
if name:
model['name'] = name
model = self.contents_manager.create_file(model, path)
def _upload(self, model, path):
"""Handle upload of a new file to path"""
self.log.info(u"Uploading file to %s", path)
model = self.contents_manager.new(model, path)
self.set_status(201)
self._finish_model(model)
def _create_empty_file(self, path, name=None, ext='.ipynb'):
"""Create an empty file in path
If name specified, create it in path/name.
"""
self.log.info(u"Creating new file in %s/%s", path, name or '')
model = {}
if name:
model['name'] = name
model = self.contents_manager.create_file(model, path=path, ext=ext)
def _new_untitled(self, path, type='', ext=''):
"""Create a new, empty untitled entity"""
self.log.info(u"Creating new %s in %s", type or 'file', path)
model = self.contents_manager.new_untitled(path=path, type=type, ext=ext)
self.set_status(201)
self._finish_model(model)
def _save(self, model, path, name):
def _save(self, model, path):
"""Save an existing file."""
self.log.info(u"Saving file at %s/%s", path, name)
model = self.contents_manager.save(model, name, path)
if model['path'] != path.strip('/') or model['name'] != name:
# a rename happened, set Location header
location = True
else:
location = False
self._finish_model(model, location)
self.log.info(u"Saving file at %s", path)
model = self.contents_manager.save(model, path)
self._finish_model(model)
@web.authenticated
@json_errors
def post(self, path='', name=None):
"""Create a new file or directory in the specified path.
def post(self, path=''):
"""Create a new file in the specified path.
POST creates new files or directories. The server always decides on the name.
POST creates new files. The server always decides on the name.
POST /api/contents/path
New untitled notebook in path. If content specified, upload a
notebook, otherwise start empty.
New untitled, empty file or directory.
POST /api/contents/path
with body {"copy_from" : "OtherNotebook.ipynb"}
with body {"copy_from" : "/path/to/OtherNotebook.ipynb"}
New copy of OtherNotebook in path
"""
if name is not None:
path = u'{}/{}'.format(path, name)
cm = self.contents_manager
if cm.file_exists(path):
raise web.HTTPError(400, "Cannot POST to existing files, use PUT instead.")
raise web.HTTPError(400, "Cannot POST to files, use PUT instead.")
if not cm.path_exists(path):
if not cm.dir_exists(path):
raise web.HTTPError(404, "No such directory: %s" % path)
model = self.get_json_body()
if model is not None:
copy_from = model.get('copy_from')
ext = model.get('ext', '.ipynb')
if model.get('content') is not None:
if copy_from:
raise web.HTTPError(400, "Can't upload and copy at the same time.")
self._upload(model, path)
elif copy_from:
ext = model.get('ext', '')
type = model.get('type', '')
if copy_from:
self._copy(copy_from, path)
else:
self._create_empty_file(path, ext=ext)
self._new_untitled(path, type=type, ext=ext)
else:
self._create_empty_file(path)
self._new_untitled(path)
@web.authenticated
@json_errors
def put(self, path='', name=None):
def put(self, path=''):
"""Saves the file in the location specified by name and path.
PUT is very similar to POST, but the requester specifies the name,
@ -184,39 +161,25 @@ class ContentsHandler(IPythonHandler):
Save notebook at ``path/Name.ipynb``. Notebook structure is specified
in `content` key of JSON request body. If content is not specified,
create a new empty notebook.
PUT /api/contents/path/Name.ipynb
with JSON body::
{
"copy_from" : "[path/to/]OtherNotebook.ipynb"
}
Copy OtherNotebook to Name
"""
if name is None:
raise web.HTTPError(400, "name must be specified with PUT.")
model = self.get_json_body()
if model:
copy_from = model.get('copy_from')
if copy_from:
if model.get('content'):
raise web.HTTPError(400, "Can't upload and copy at the same time.")
self._copy(copy_from, path, name)
elif self.contents_manager.file_exists(name, path):
self._save(model, path, name)
if model.get('copy_from'):
raise web.HTTPError(400, "Cannot copy with PUT, only POST")
if self.contents_manager.file_exists(path):
self._save(model, path)
else:
self._upload(model, path, name)
self._upload(model, path)
else:
self._create_empty_file(path, name)
self._new_untitled(path)
@web.authenticated
@json_errors
def delete(self, path='', name=None):
def delete(self, path=''):
"""delete a file in the given path"""
cm = self.contents_manager
self.log.warn('delete %s:%s', path, name)
cm.delete(name, path)
self.log.warn('delete %s', path)
cm.delete(path)
self.set_status(204)
self.finish()
@ -227,22 +190,22 @@ class CheckpointsHandler(IPythonHandler):
@web.authenticated
@json_errors
def get(self, path='', name=None):
def get(self, path=''):
"""get lists checkpoints for a file"""
cm = self.contents_manager
checkpoints = cm.list_checkpoints(name, path)
checkpoints = cm.list_checkpoints(path)
data = json.dumps(checkpoints, default=date_default)
self.finish(data)
@web.authenticated
@json_errors
def post(self, path='', name=None):
def post(self, path=''):
"""post creates a new checkpoint"""
cm = self.contents_manager
checkpoint = cm.create_checkpoint(name, path)
checkpoint = cm.create_checkpoint(path)
data = json.dumps(checkpoint, default=date_default)
location = url_path_join(self.base_url, 'api/contents',
path, name, 'checkpoints', checkpoint['id'])
path, 'checkpoints', checkpoint['id'])
self.set_header('Location', url_escape(location))
self.set_status(201)
self.finish(data)
@ -254,22 +217,38 @@ class ModifyCheckpointsHandler(IPythonHandler):
@web.authenticated
@json_errors
def post(self, path, name, checkpoint_id):
def post(self, path, checkpoint_id):
"""post restores a file from a checkpoint"""
cm = self.contents_manager
cm.restore_checkpoint(checkpoint_id, name, path)
cm.restore_checkpoint(checkpoint_id, path)
self.set_status(204)
self.finish()
@web.authenticated
@json_errors
def delete(self, path, name, checkpoint_id):
def delete(self, path, checkpoint_id):
"""delete clears a checkpoint for a given file"""
cm = self.contents_manager
cm.delete_checkpoint(checkpoint_id, name, path)
cm.delete_checkpoint(checkpoint_id, path)
self.set_status(204)
self.finish()
class NotebooksRedirectHandler(IPythonHandler):
"""Redirect /api/notebooks to /api/contents"""
SUPPORTED_METHODS = ('GET', 'PUT', 'PATCH', 'POST', 'DELETE')
def get(self, path):
self.log.warn("/api/notebooks is deprecated, use /api/contents")
self.redirect(url_path_join(
self.base_url,
'api/contents',
path
))
put = patch = post = delete = get
#-----------------------------------------------------------------------------
# URL to handler mappings
#-----------------------------------------------------------------------------
@ -278,9 +257,9 @@ class ModifyCheckpointsHandler(IPythonHandler):
_checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
default_handlers = [
(r"/api/contents%s/checkpoints" % file_path_regex, CheckpointsHandler),
(r"/api/contents%s/checkpoints/%s" % (file_path_regex, _checkpoint_id_regex),
(r"/api/contents%s/checkpoints" % path_regex, CheckpointsHandler),
(r"/api/contents%s/checkpoints/%s" % (path_regex, _checkpoint_id_regex),
ModifyCheckpointsHandler),
(r"/api/contents%s" % file_path_regex, ContentsHandler),
(r"/api/contents%s" % path_regex, ContentsHandler),
(r"/api/notebooks/?(.*)", NotebooksRedirectHandler),
]

@ -5,14 +5,18 @@
from fnmatch import fnmatch
import itertools
import json
import os
import re
from tornado.web import HTTPError
from IPython.config.configurable import LoggingConfigurable
from IPython.nbformat import current, sign
from IPython.nbformat import sign, validate, ValidationError
from IPython.nbformat.v4 import new_notebook
from IPython.utils.traitlets import Instance, Unicode, List
copy_pat = re.compile(r'\-Copy\d*\.')
class ContentsManager(LoggingConfigurable):
"""Base class for serving files and directories.
@ -31,14 +35,6 @@ class ContentsManager(LoggingConfigurable):
- if unspecified, path defaults to '',
indicating the root path.
name is also unicode, and refers to a specfic target:
- unicode, not url-escaped
- must not contain '/'
- It refers to an individual filename
- It may refer to a directory name,
in the case of listing or creating directories.
"""
notary = Instance(sign.NotebookNotary)
@ -67,7 +63,7 @@ class ContentsManager(LoggingConfigurable):
# ContentsManager API part 1: methods that must be
# implemented in subclasses.
def path_exists(self, path):
def dir_exists(self, path):
"""Does the API-style path (directory) actually exist?
Like os.path.isdir
@ -103,8 +99,8 @@ class ContentsManager(LoggingConfigurable):
"""
raise NotImplementedError
def file_exists(self, name, path=''):
"""Does a file exist at the given name and path?
def file_exists(self, path=''):
"""Does a file exist at the given path?
Like os.path.isfile
@ -124,15 +120,13 @@ class ContentsManager(LoggingConfigurable):
"""
raise NotImplementedError('must be implemented in a subclass')
def exists(self, name, path=''):
"""Does a file or directory exist at the given name and path?
def exists(self, path):
"""Does a file or directory exist at the given path?
Like os.path.exists
Parameters
----------
name : string
The name of the file you are checking.
path : string
The relative path to the file's directory (with '/' as separator)
@ -141,17 +135,17 @@ class ContentsManager(LoggingConfigurable):
exists : bool
Whether the target exists.
"""
return self.file_exists(name, path) or self.path_exists("%s/%s" % (path, name))
return self.file_exists(path) or self.dir_exists(path)
def get_model(self, name, path='', content=True):
def get(self, path, content=True, type_=None, format=None):
"""Get the model of a file or directory with or without content."""
raise NotImplementedError('must be implemented in a subclass')
def save(self, model, name, path=''):
def save(self, model, path):
"""Save the file or directory and return the model with no content."""
raise NotImplementedError('must be implemented in a subclass')
def update(self, model, name, path=''):
def update(self, model, path):
"""Update the file or directory and return the model with no content.
For use in PATCH requests, to enable renaming a file without
@ -159,26 +153,26 @@ class ContentsManager(LoggingConfigurable):
"""
raise NotImplementedError('must be implemented in a subclass')
def delete(self, name, path=''):
"""Delete file or directory by name and path."""
def delete(self, path):
"""Delete file or directory by path."""
raise NotImplementedError('must be implemented in a subclass')
def create_checkpoint(self, name, path=''):
def create_checkpoint(self, path):
"""Create a checkpoint of the current state of a file
Returns a checkpoint_id for the new checkpoint.
"""
raise NotImplementedError("must be implemented in a subclass")
def list_checkpoints(self, name, path=''):
def list_checkpoints(self, path):
"""Return a list of checkpoints for a given file"""
return []
def restore_checkpoint(self, checkpoint_id, name, path=''):
def restore_checkpoint(self, checkpoint_id, path):
"""Restore a file from one of its checkpoints"""
raise NotImplementedError("must be implemented in a subclass")
def delete_checkpoint(self, checkpoint_id, name, path=''):
def delete_checkpoint(self, checkpoint_id, path):
"""delete a checkpoint for a file"""
raise NotImplementedError("must be implemented in a subclass")
@ -188,11 +182,19 @@ class ContentsManager(LoggingConfigurable):
def info_string(self):
return "Serving contents"
def get_kernel_path(self, name, path='', model=None):
""" Return the path to start kernel in """
return path
def get_kernel_path(self, path, model=None):
"""Return the API path for the kernel
KernelManagers can turn this value into a filesystem path,
or ignore it altogether.
def increment_filename(self, filename, path=''):
The default value here will start kernels in the directory of the
notebook server. FileContentsManager overrides this to use the
directory containing the notebook.
"""
return ''
def increment_filename(self, filename, path='', insert=''):
"""Increment a filename until it is unique.
Parameters
@ -210,87 +212,140 @@ class ContentsManager(LoggingConfigurable):
path = path.strip('/')
basename, ext = os.path.splitext(filename)
for i in itertools.count():
name = u'{basename}{i}{ext}'.format(basename=basename, i=i,
ext=ext)
if not self.file_exists(name, path):
if i:
insert_i = '{}{}'.format(insert, i)
else:
insert_i = ''
name = u'{basename}{insert}{ext}'.format(basename=basename,
insert=insert_i, ext=ext)
if not self.exists(u'{}/{}'.format(path, name)):
break
return name
def create_file(self, model=None, path='', ext='.ipynb'):
"""Create a new file or directory and return its model with no content."""
def validate_notebook_model(self, model):
"""Add failed-validation message to model"""
try:
validate(model['content'])
except ValidationError as e:
model['message'] = u'Notebook Validation failed: {}:\n{}'.format(
e.message, json.dumps(e.instance, indent=1, default=lambda obj: '<UNKNOWN>'),
)
return model
def new_untitled(self, path='', type='', ext=''):
"""Create a new untitled file or directory in path
path must be a directory
File extension can be specified.
Use `new` to create files with a fully specified path (including filename).
"""
path = path.strip('/')
if not self.dir_exists(path):
raise HTTPError(404, 'No such directory: %s' % path)
model = {}
if type:
model['type'] = type
if ext == '.ipynb':
model.setdefault('type', 'notebook')
else:
model.setdefault('type', 'file')
insert = ''
if model['type'] == 'directory':
untitled = self.untitled_directory
insert = ' '
elif model['type'] == 'notebook':
untitled = self.untitled_notebook
ext = '.ipynb'
elif model['type'] == 'file':
untitled = self.untitled_file
else:
raise HTTPError(400, "Unexpected model type: %r" % model['type'])
name = self.increment_filename(untitled + ext, path, insert=insert)
path = u'{0}/{1}'.format(path, name)
return self.new(model, path)
def new(self, model=None, path=''):
"""Create a new file or directory and return its model with no content.
To create a new untitled entity in a directory, use `new_untitled`.
"""
path = path.strip('/')
if model is None:
model = {}
if 'content' not in model and model.get('type', None) != 'directory':
if ext == '.ipynb':
metadata = current.new_metadata(name=u'')
model['content'] = current.new_notebook(metadata=metadata)
model['type'] = 'notebook'
if path.endswith('.ipynb'):
model.setdefault('type', 'notebook')
else:
model.setdefault('type', 'file')
# no content, not a directory, so fill out new-file model
if 'content' not in model and model['type'] != 'directory':
if model['type'] == 'notebook':
model['content'] = new_notebook()
model['format'] = 'json'
else:
model['content'] = ''
model['type'] = 'file'
model['format'] = 'text'
if 'name' not in model:
if model['type'] == 'directory':
untitled = self.untitled_directory
elif model['type'] == 'notebook':
untitled = self.untitled_notebook
elif model['type'] == 'file':
untitled = self.untitled_file
else:
raise HTTPError(400, "Unexpected model type: %r" % model['type'])
model['name'] = self.increment_filename(untitled + ext, path)
model['path'] = path
model = self.save(model, model['name'], model['path'])
model = self.save(model, path)
return model
def copy(self, from_name, to_name=None, path=''):
def copy(self, from_path, to_path=None):
"""Copy an existing file and return its new model.
If to_name not specified, increment `from_name-Copy#.ext`.
If to_path not specified, it will be the parent directory of from_path.
If to_path is a directory, filename will increment `from_path-Copy#.ext`.
copy_from can be a full path to a file,
or just a base name. If a base name, `path` is used.
from_path must be a full path to a file.
"""
path = path.strip('/')
if '/' in from_name:
from_path, from_name = from_name.rsplit('/', 1)
path = from_path.strip('/')
if '/' in path:
from_dir, from_name = path.rsplit('/', 1)
else:
from_path = path
model = self.get_model(from_name, from_path)
from_dir = ''
from_name = path
model = self.get(path)
model.pop('path', None)
model.pop('name', None)
if model['type'] == 'directory':
raise HTTPError(400, "Can't copy directories")
if not to_name:
base, ext = os.path.splitext(from_name)
copy_name = u'{0}-Copy{1}'.format(base, ext)
to_name = self.increment_filename(copy_name, path)
model['name'] = to_name
model['path'] = path
model = self.save(model, to_name, path)
if not to_path:
to_path = from_dir
if self.dir_exists(to_path):
name = copy_pat.sub(u'.', from_name)
to_name = self.increment_filename(name, to_path, insert='-Copy')
to_path = u'{0}/{1}'.format(to_path, to_name)
model = self.save(model, to_path)
return model
def log_info(self):
self.log.info(self.info_string())
def trust_notebook(self, name, path=''):
def trust_notebook(self, path):
"""Explicitly trust a notebook
Parameters
----------
name : string
The filename of the notebook
path : string
The notebook's directory
The path of a notebook
"""
model = self.get_model(name, path)
model = self.get(path)
nb = model['content']
self.log.warn("Trusting notebook %s/%s", path, name)
self.log.warn("Trusting notebook %s", path)
self.notary.mark_cells(nb, True)
self.save(model, name, path)
self.save(model, path)
def check_and_sign(self, nb, name='', path=''):
def check_and_sign(self, nb, path=''):
"""Check for trusted cells, and sign the notebook.
Called as a part of saving notebooks.
@ -298,18 +353,16 @@ class ContentsManager(LoggingConfigurable):
Parameters
----------
nb : dict
The notebook object (in nbformat.current format)
name : string
The filename of the notebook (for logging)
The notebook dict
path : string
The notebook's directory (for logging)
The notebook's path (for logging)
"""
if self.notary.check_cells(nb):
self.notary.sign(nb)
else:
self.log.warn("Saving untrusted notebook %s/%s", path, name)
self.log.warn("Saving untrusted notebook %s", path)
def mark_trusted_cells(self, nb, name='', path=''):
def mark_trusted_cells(self, nb, path=''):
"""Mark cells as trusted if the notebook signature matches.
Called as a part of loading notebooks.
@ -317,15 +370,13 @@ class ContentsManager(LoggingConfigurable):
Parameters
----------
nb : dict
The notebook object (in nbformat.current format)
name : string
The filename of the notebook (for logging)
The notebook object (in current nbformat)
path : string
The notebook's directory (for logging)
The notebook's path (for logging)
"""
trusted = self.notary.check_signature(nb)
if not trusted:
self.log.warn("Notebook %s/%s is not trusted", path, name)
self.log.warn("Notebook %s is not trusted", path)
self.notary.mark_cells(nb, trusted)
def should_list(self, name):

@ -14,9 +14,10 @@ import requests
from IPython.html.utils import url_path_join, url_escape
from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
from IPython.nbformat import current
from IPython.nbformat.current import (new_notebook, write, read, new_worksheet,
new_heading_cell, to_notebook_json)
from IPython.nbformat import read, write, from_dict
from IPython.nbformat.v4 import (
new_notebook, new_markdown_cell,
)
from IPython.nbformat import v2
from IPython.utils import py3compat
from IPython.utils.data import uniq_stable
@ -34,10 +35,10 @@ class API(object):
def __init__(self, base_url):
self.base_url = base_url
def _req(self, verb, path, body=None):
def _req(self, verb, path, body=None, params=None):
response = requests.request(verb,
url_path_join(self.base_url, 'api/contents', path),
data=body,
data=body, params=params,
)
response.raise_for_status()
return response
@ -45,56 +46,64 @@ class API(object):
def list(self, path='/'):
return self._req('GET', path)
def read(self, name, path='/'):
return self._req('GET', url_path_join(path, name))
def read(self, path, type_=None, format=None):
params = {}
if type_ is not None:
params['type'] = type_
if format is not None:
params['format'] = format
return self._req('GET', path, params=params)
def create_untitled(self, path='/', ext=None):
def create_untitled(self, path='/', ext='.ipynb'):
body = None
if ext:
body = json.dumps({'ext': ext})
return self._req('POST', path, body)
def upload_untitled(self, body, path='/'):
return self._req('POST', path, body)
def mkdir_untitled(self, path='/'):
return self._req('POST', path, json.dumps({'type': 'directory'}))
def copy_untitled(self, copy_from, path='/'):
def copy(self, copy_from, path='/'):
body = json.dumps({'copy_from':copy_from})
return self._req('POST', path, body)
def create(self, name, path='/'):
return self._req('PUT', url_path_join(path, name))
def create(self, path='/'):
return self._req('PUT', path)
def upload(self, path, body):
return self._req('PUT', path, body)
def upload(self, name, body, path='/'):
return self._req('PUT', url_path_join(path, name), body)
def mkdir_untitled(self, path='/'):
return self._req('POST', path, json.dumps({'type': 'directory'}))
def mkdir(self, name, path='/'):
return self._req('PUT', url_path_join(path, name), json.dumps({'type': 'directory'}))
def mkdir(self, path='/'):
return self._req('PUT', path, json.dumps({'type': 'directory'}))
def copy(self, copy_from, copy_to, path='/'):
def copy_put(self, copy_from, path='/'):
body = json.dumps({'copy_from':copy_from})
return self._req('PUT', url_path_join(path, copy_to), body)
return self._req('PUT', path, body)
def save(self, name, body, path='/'):
return self._req('PUT', url_path_join(path, name), body)
def save(self, path, body):
return self._req('PUT', path, body)
def delete(self, name, path='/'):
return self._req('DELETE', url_path_join(path, name))
def delete(self, path='/'):
return self._req('DELETE', path)
def rename(self, name, path, new_name):
body = json.dumps({'name': new_name})
return self._req('PATCH', url_path_join(path, name), body)
def rename(self, path, new_path):
body = json.dumps({'path': new_path})
return self._req('PATCH', path, body)
def get_checkpoints(self, name, path):
return self._req('GET', url_path_join(path, name, 'checkpoints'))
def get_checkpoints(self, path):
return self._req('GET', url_path_join(path, 'checkpoints'))
def new_checkpoint(self, name, path):
return self._req('POST', url_path_join(path, name, 'checkpoints'))
def new_checkpoint(self, path):
return self._req('POST', url_path_join(path, 'checkpoints'))
def restore_checkpoint(self, name, path, checkpoint_id):
return self._req('POST', url_path_join(path, name, 'checkpoints', checkpoint_id))
def restore_checkpoint(self, path, checkpoint_id):
return self._req('POST', url_path_join(path, 'checkpoints', checkpoint_id))
def delete_checkpoint(self, name, path, checkpoint_id):
return self._req('DELETE', url_path_join(path, name, 'checkpoints', checkpoint_id))
def delete_checkpoint(self, path, checkpoint_id):
return self._req('DELETE', url_path_join(path, 'checkpoints', checkpoint_id))
class APITest(NotebookTestBase):
"""Test the kernels web service API"""
@ -130,8 +139,6 @@ class APITest(NotebookTestBase):
self.blob = os.urandom(100)
self.b64_blob = base64.encodestring(self.blob).decode('ascii')
for d in (self.dirs + self.hidden_dirs):
d.replace('/', os.sep)
if not os.path.isdir(pjoin(nbdir, d)):
@ -142,8 +149,8 @@ class APITest(NotebookTestBase):
# create a notebook
with io.open(pjoin(nbdir, d, '%s.ipynb' % name), 'w',
encoding='utf-8') as f:
nb = new_notebook(name=name)
write(nb, f, format='ipynb')
nb = new_notebook()
write(nb, f, version=4)
# create a text file
with io.open(pjoin(nbdir, d, '%s.txt' % name), 'w',
@ -177,12 +184,12 @@ class APITest(NotebookTestBase):
nbs = notebooks_only(self.api.list(u'/unicodé/').json())
self.assertEqual(len(nbs), 1)
self.assertEqual(nbs[0]['name'], 'innonascii.ipynb')
self.assertEqual(nbs[0]['path'], u'unicodé')
self.assertEqual(nbs[0]['path'], u'unicodé/innonascii.ipynb')
nbs = notebooks_only(self.api.list('/foo/bar/').json())
self.assertEqual(len(nbs), 1)
self.assertEqual(nbs[0]['name'], 'baz.ipynb')
self.assertEqual(nbs[0]['path'], 'foo/bar')
self.assertEqual(nbs[0]['path'], 'foo/bar/baz.ipynb')
nbs = notebooks_only(self.api.list('foo').json())
self.assertEqual(len(nbs), 4)
@ -197,8 +204,11 @@ class APITest(NotebookTestBase):
self.assertEqual(nbnames, expected)
def test_list_dirs(self):
print(self.api.list().json())
dirs = dirs_only(self.api.list().json())
dir_names = {normalize('NFC', d['name']) for d in dirs}
print(dir_names)
print(self.top_level_dirs)
self.assertEqual(dir_names, self.top_level_dirs) # Excluding hidden dirs
def test_list_nonexistant_dir(self):
@ -207,8 +217,10 @@ class APITest(NotebookTestBase):
def test_get_nb_contents(self):
for d, name in self.dirs_nbs:
nb = self.api.read('%s.ipynb' % name, d+'/').json()
path = url_path_join(d, name + '.ipynb')
nb = self.api.read(path).json()
self.assertEqual(nb['name'], u'%s.ipynb' % name)
self.assertEqual(nb['path'], path)
self.assertEqual(nb['type'], 'notebook')
self.assertIn('content', nb)
self.assertEqual(nb['format'], 'json')
@ -219,12 +231,14 @@ class APITest(NotebookTestBase):
def test_get_contents_no_such_file(self):
# Name that doesn't exist - should be a 404
with assert_http_error(404):
self.api.read('q.ipynb', 'foo')
self.api.read('foo/q.ipynb')
def test_get_text_file_contents(self):
for d, name in self.dirs_nbs:
model = self.api.read(u'%s.txt' % name, d+'/').json()
path = url_path_join(d, name + '.txt')
model = self.api.read(path).json()
self.assertEqual(model['name'], u'%s.txt' % name)
self.assertEqual(model['path'], path)
self.assertIn('content', model)
self.assertEqual(model['format'], 'text')
self.assertEqual(model['type'], 'file')
@ -232,12 +246,18 @@ class APITest(NotebookTestBase):
# Name that doesn't exist - should be a 404
with assert_http_error(404):
self.api.read('q.txt', 'foo')
self.api.read('foo/q.txt')
# Specifying format=text should fail on a non-UTF-8 file
with assert_http_error(400):
self.api.read('foo/bar/baz.blob', type_='file', format='text')
def test_get_binary_file_contents(self):
for d, name in self.dirs_nbs:
model = self.api.read(u'%s.blob' % name, d+'/').json()
path = url_path_join(d, name + '.blob')
model = self.api.read(path).json()
self.assertEqual(model['name'], u'%s.blob' % name)
self.assertEqual(model['path'], path)
self.assertIn('content', model)
self.assertEqual(model['format'], 'base64')
self.assertEqual(model['type'], 'file')
@ -246,66 +266,78 @@ class APITest(NotebookTestBase):
# Name that doesn't exist - should be a 404
with assert_http_error(404):
self.api.read('q.txt', 'foo')
self.api.read('foo/q.txt')
def _check_created(self, resp, name, path, type='notebook'):
def test_get_bad_type(self):
with assert_http_error(400):
self.api.read(u'unicodé', type_='file') # this is a directory
with assert_http_error(400):
self.api.read(u'unicodé/innonascii.ipynb', type_='directory')
def _check_created(self, resp, path, type='notebook'):
self.assertEqual(resp.status_code, 201)
location_header = py3compat.str_to_unicode(resp.headers['Location'])
self.assertEqual(location_header, url_escape(url_path_join(u'/api/contents', path, name)))
self.assertEqual(location_header, url_escape(url_path_join(u'/api/contents', path)))
rjson = resp.json()
self.assertEqual(rjson['name'], name)
self.assertEqual(rjson['name'], path.rsplit('/', 1)[-1])
self.assertEqual(rjson['path'], path)
self.assertEqual(rjson['type'], type)
isright = os.path.isdir if type == 'directory' else os.path.isfile
assert isright(pjoin(
self.notebook_dir.name,
path.replace('/', os.sep),
name,
))
def test_create_untitled(self):
resp = self.api.create_untitled(path=u'å b')
self._check_created(resp, 'Untitled0.ipynb', u'å b')
self._check_created(resp, u'å b/Untitled.ipynb')
# Second time
resp = self.api.create_untitled(path=u'å b')
self._check_created(resp, 'Untitled1.ipynb', u'å b')
self._check_created(resp, u'å b/Untitled1.ipynb')
# And two directories down
resp = self.api.create_untitled(path='foo/bar')
self._check_created(resp, 'Untitled0.ipynb', 'foo/bar')
self._check_created(resp, 'foo/bar/Untitled.ipynb')
def test_create_untitled_txt(self):
resp = self.api.create_untitled(path='foo/bar', ext='.txt')
self._check_created(resp, 'untitled0.txt', 'foo/bar', type='file')
self._check_created(resp, 'foo/bar/untitled.txt', type='file')
resp = self.api.read(path='foo/bar', name='untitled0.txt')
resp = self.api.read(path='foo/bar/untitled.txt')
model = resp.json()
self.assertEqual(model['type'], 'file')
self.assertEqual(model['format'], 'text')
self.assertEqual(model['content'], '')
def test_upload_untitled(self):
nb = new_notebook(name='Upload test')
nbmodel = {'content': nb, 'type': 'notebook'}
resp = self.api.upload_untitled(path=u'å b',
body=json.dumps(nbmodel))
self._check_created(resp, 'Untitled0.ipynb', u'å b')
def test_upload(self):
nb = new_notebook(name=u'ignored')
nb = new_notebook()
nbmodel = {'content': nb, 'type': 'notebook'}
resp = self.api.upload(u'Upload tést.ipynb', path=u'å b',
body=json.dumps(nbmodel))
self._check_created(resp, u'Upload tést.ipynb', u'å b')
path = u'å b/Upload tést.ipynb'
resp = self.api.upload(path, body=json.dumps(nbmodel))
self._check_created(resp, path)
def test_mkdir_untitled(self):
resp = self.api.mkdir_untitled(path=u'å b')
self._check_created(resp, u'å b/Untitled Folder', type='directory')
# Second time
resp = self.api.mkdir_untitled(path=u'å b')
self._check_created(resp, u'å b/Untitled Folder 1', type='directory')
# And two directories down
resp = self.api.mkdir_untitled(path='foo/bar')
self._check_created(resp, 'foo/bar/Untitled Folder', type='directory')
def test_mkdir(self):
resp = self.api.mkdir(u'New ∂ir', path=u'å b')
self._check_created(resp, u'New ∂ir', u'å b', type='directory')
path = u'å b/New ∂ir'
resp = self.api.mkdir(path)
self._check_created(resp, path, type='directory')
def test_mkdir_hidden_400(self):
with assert_http_error(400):
resp = self.api.mkdir(u'.hidden', path=u'å b')
resp = self.api.mkdir(u'å b/.hidden')
def test_upload_txt(self):
body = u'ünicode téxt'
@ -314,11 +346,11 @@ class APITest(NotebookTestBase):
'format' : 'text',
'type' : 'file',
}
resp = self.api.upload(u'Upload tést.txt', path=u'å b',
body=json.dumps(model))
path = u'å b/Upload tést.txt'
resp = self.api.upload(path, body=json.dumps(model))
# check roundtrip
resp = self.api.read(path=u'å b', name=u'Upload tést.txt')
resp = self.api.read(path)
model = resp.json()
self.assertEqual(model['type'], 'file')
self.assertEqual(model['format'], 'text')
@ -332,13 +364,14 @@ class APITest(NotebookTestBase):
'format' : 'base64',
'type' : 'file',
}
resp = self.api.upload(u'Upload tést.blob', path=u'å b',
body=json.dumps(model))
path = u'å b/Upload tést.blob'
resp = self.api.upload(path, body=json.dumps(model))
# check roundtrip
resp = self.api.read(path=u'å b', name=u'Upload tést.blob')
resp = self.api.read(path)
model = resp.json()
self.assertEqual(model['type'], 'file')
self.assertEqual(model['path'], path)
self.assertEqual(model['format'], 'base64')
decoded = base64.decodestring(model['content'].encode('ascii'))
self.assertEqual(decoded, body)
@ -349,46 +382,62 @@ class APITest(NotebookTestBase):
nb.worksheets.append(ws)
ws.cells.append(v2.new_code_cell(input='print("hi")'))
nbmodel = {'content': nb, 'type': 'notebook'}
resp = self.api.upload(u'Upload tést.ipynb', path=u'å b',
body=json.dumps(nbmodel))
self._check_created(resp, u'Upload tést.ipynb', u'å b')
resp = self.api.read(u'Upload tést.ipynb', u'å b')
path = u'å b/Upload tést.ipynb'
resp = self.api.upload(path, body=json.dumps(nbmodel))
self._check_created(resp, path)
resp = self.api.read(path)
data = resp.json()
self.assertEqual(data['content']['nbformat'], current.nbformat)
self.assertEqual(data['content']['orig_nbformat'], 2)
def test_copy_untitled(self):
resp = self.api.copy_untitled(u'ç d.ipynb', path=u'å b')
self._check_created(resp, u'ç d-Copy0.ipynb', u'å b')
self.assertEqual(data['content']['nbformat'], 4)
def test_copy(self):
resp = self.api.copy(u'ç d.ipynb', u'cøpy.ipynb', path=u'å b')
self._check_created(resp, u'cøpy.ipynb', u'å b')
resp = self.api.copy(u'å b/ç d.ipynb', u'å b')
self._check_created(resp, u'å b/ç d-Copy1.ipynb')
resp = self.api.copy(u'å b/ç d.ipynb', u'å b')
self._check_created(resp, u'å b/ç d-Copy2.ipynb')
def test_copy_copy(self):
resp = self.api.copy(u'å b/ç d.ipynb', u'å b')
self._check_created(resp, u'å b/ç d-Copy1.ipynb')
resp = self.api.copy(u'å b/ç d-Copy1.ipynb', u'å b')
self._check_created(resp, u'å b/ç d-Copy2.ipynb')
def test_copy_path(self):
resp = self.api.copy(u'foo/a.ipynb', u'cøpyfoo.ipynb', path=u'å b')
self._check_created(resp, u'cøpyfoo.ipynb', u'å b')
resp = self.api.copy(u'foo/a.ipynb', u'å b')
self._check_created(resp, u'å b/a.ipynb')
resp = self.api.copy(u'foo/a.ipynb', u'å b')
self._check_created(resp, u'å b/a-Copy1.ipynb')
def test_copy_put_400(self):
with assert_http_error(400):
resp = self.api.copy_put(u'å b/ç d.ipynb', u'å b/cøpy.ipynb')
def test_copy_dir_400(self):
# can't copy directories
with assert_http_error(400):
resp = self.api.copy(u'å b', u'å c')
resp = self.api.copy(u'å b', u'foo')
def test_delete(self):
for d, name in self.dirs_nbs:
resp = self.api.delete('%s.ipynb' % name, d)
print('%r, %r' % (d, name))
resp = self.api.delete(url_path_join(d, name + '.ipynb'))
self.assertEqual(resp.status_code, 204)
for d in self.dirs + ['/']:
nbs = notebooks_only(self.api.list(d).json())
self.assertEqual(len(nbs), 0)
print('------')
print(d)
print(nbs)
self.assertEqual(nbs, [])
def test_delete_dirs(self):
# depth-first delete everything, so we don't try to delete empty directories
for name in sorted(self.dirs + ['/'], key=len, reverse=True):
listing = self.api.list(name).json()['content']
for model in listing:
self.api.delete(model['name'], model['path'])
self.api.delete(model['path'])
listing = self.api.list('/').json()['content']
self.assertEqual(listing, [])
@ -398,9 +447,10 @@ class APITest(NotebookTestBase):
self.api.delete(u'å b')
def test_rename(self):
resp = self.api.rename('a.ipynb', 'foo', 'z.ipynb')
resp = self.api.rename('foo/a.ipynb', 'foo/z.ipynb')
self.assertEqual(resp.headers['Location'].split('/')[-1], 'z.ipynb')
self.assertEqual(resp.json()['name'], 'z.ipynb')
self.assertEqual(resp.json()['path'], 'foo/z.ipynb')
assert os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'z.ipynb'))
nbs = notebooks_only(self.api.list('foo').json())
@ -410,43 +460,31 @@ class APITest(NotebookTestBase):
def test_rename_existing(self):
with assert_http_error(409):
self.api.rename('a.ipynb', 'foo', 'b.ipynb')
self.api.rename('foo/a.ipynb', 'foo/b.ipynb')
def test_save(self):
resp = self.api.read('a.ipynb', 'foo')
resp = self.api.read('foo/a.ipynb')
nbcontent = json.loads(resp.text)['content']
nb = to_notebook_json(nbcontent)
ws = new_worksheet()
nb.worksheets = [ws]
ws.cells.append(new_heading_cell(u'Created by test ³'))
nb = from_dict(nbcontent)
nb.cells.append(new_markdown_cell(u'Created by test ³'))
nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb, 'type': 'notebook'}
resp = self.api.save('a.ipynb', path='foo', body=json.dumps(nbmodel))
nbmodel= {'content': nb, 'type': 'notebook'}
resp = self.api.save('foo/a.ipynb', body=json.dumps(nbmodel))
nbfile = pjoin(self.notebook_dir.name, 'foo', 'a.ipynb')
with io.open(nbfile, 'r', encoding='utf-8') as f:
newnb = read(f, format='ipynb')
self.assertEqual(newnb.worksheets[0].cells[0].source,
newnb = read(f, as_version=4)
self.assertEqual(newnb.cells[0].source,
u'Created by test ³')
nbcontent = self.api.read('a.ipynb', 'foo').json()['content']
newnb = to_notebook_json(nbcontent)
self.assertEqual(newnb.worksheets[0].cells[0].source,
nbcontent = self.api.read('foo/a.ipynb').json()['content']
newnb = from_dict(nbcontent)
self.assertEqual(newnb.cells[0].source,
u'Created by test ³')
# Save and rename
nbmodel= {'name': 'a2.ipynb', 'path':'foo/bar', 'content': nb, 'type': 'notebook'}
resp = self.api.save('a.ipynb', path='foo', body=json.dumps(nbmodel))
saved = resp.json()
self.assertEqual(saved['name'], 'a2.ipynb')
self.assertEqual(saved['path'], 'foo/bar')
assert os.path.isfile(pjoin(self.notebook_dir.name,'foo','bar','a2.ipynb'))
assert not os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'a.ipynb'))
with assert_http_error(404):
self.api.read('a.ipynb', 'foo')
def test_checkpoints(self):
resp = self.api.read('a.ipynb', 'foo')
r = self.api.new_checkpoint('a.ipynb', 'foo')
resp = self.api.read('foo/a.ipynb')
r = self.api.new_checkpoint('foo/a.ipynb')
self.assertEqual(r.status_code, 201)
cp1 = r.json()
self.assertEqual(set(cp1), {'id', 'last_modified'})
@ -454,32 +492,30 @@ class APITest(NotebookTestBase):
# Modify it
nbcontent = json.loads(resp.text)['content']
nb = to_notebook_json(nbcontent)
ws = new_worksheet()
nb.worksheets = [ws]
hcell = new_heading_cell('Created by test')
ws.cells.append(hcell)
nb = from_dict(nbcontent)
hcell = new_markdown_cell('Created by test')
nb.cells.append(hcell)
# Save
nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb, 'type': 'notebook'}
resp = self.api.save('a.ipynb', path='foo', body=json.dumps(nbmodel))
nbmodel= {'content': nb, 'type': 'notebook'}
resp = self.api.save('foo/a.ipynb', body=json.dumps(nbmodel))
# List checkpoints
cps = self.api.get_checkpoints('a.ipynb', 'foo').json()
cps = self.api.get_checkpoints('foo/a.ipynb').json()
self.assertEqual(cps, [cp1])
nbcontent = self.api.read('a.ipynb', 'foo').json()['content']
nb = to_notebook_json(nbcontent)
self.assertEqual(nb.worksheets[0].cells[0].source, 'Created by test')
nbcontent = self.api.read('foo/a.ipynb').json()['content']
nb = from_dict(nbcontent)
self.assertEqual(nb.cells[0].source, 'Created by test')
# Restore cp1
r = self.api.restore_checkpoint('a.ipynb', 'foo', cp1['id'])
r = self.api.restore_checkpoint('foo/a.ipynb', cp1['id'])
self.assertEqual(r.status_code, 204)
nbcontent = self.api.read('a.ipynb', 'foo').json()['content']
nb = to_notebook_json(nbcontent)
self.assertEqual(nb.worksheets, [])
nbcontent = self.api.read('foo/a.ipynb').json()['content']
nb = from_dict(nbcontent)
self.assertEqual(nb.cells, [])
# Delete cp1
r = self.api.delete_checkpoint('a.ipynb', 'foo', cp1['id'])
r = self.api.delete_checkpoint('foo/a.ipynb', cp1['id'])
self.assertEqual(r.status_code, 204)
cps = self.api.get_checkpoints('a.ipynb', 'foo').json()
cps = self.api.get_checkpoints('foo/a.ipynb').json()
self.assertEqual(cps, [])

@ -9,7 +9,7 @@ from tornado.web import HTTPError
from unittest import TestCase
from tempfile import NamedTemporaryFile
from IPython.nbformat import current
from IPython.nbformat import v4 as nbformat
from IPython.utils.tempdir import TemporaryDirectory
from IPython.utils.traitlets import TraitError
@ -42,7 +42,7 @@ class TestFileContentsManager(TestCase):
with TemporaryDirectory() as td:
root = td
fm = FileContentsManager(root_dir=root)
path = fm._get_os_path('test.ipynb', '/path/to/notebook/')
path = fm._get_os_path('/path/to/notebook/test.ipynb')
rel_path_list = '/path/to/notebook/test.ipynb'.split('/')
fs_path = os.path.join(fm.root_dir, *rel_path_list)
self.assertEqual(path, fs_path)
@ -53,7 +53,7 @@ class TestFileContentsManager(TestCase):
self.assertEqual(path, fs_path)
fm = FileContentsManager(root_dir=root)
path = fm._get_os_path('test.ipynb', '////')
path = fm._get_os_path('////test.ipynb')
fs_path = os.path.join(fm.root_dir, 'test.ipynb')
self.assertEqual(path, fs_path)
@ -64,8 +64,8 @@ class TestFileContentsManager(TestCase):
root = td
os.mkdir(os.path.join(td, subd))
fm = FileContentsManager(root_dir=root)
cp_dir = fm.get_checkpoint_path('cp', 'test.ipynb', '/')
cp_subdir = fm.get_checkpoint_path('cp', 'test.ipynb', '/%s/' % subd)
cp_dir = fm.get_checkpoint_path('cp', 'test.ipynb')
cp_subdir = fm.get_checkpoint_path('cp', '/%s/test.ipynb' % subd)
self.assertNotEqual(cp_dir, cp_subdir)
self.assertEqual(cp_dir, os.path.join(root, fm.checkpoint_dir, cp_name))
self.assertEqual(cp_subdir, os.path.join(root, subd, fm.checkpoint_dir, cp_name))
@ -95,71 +95,98 @@ class TestContentsManager(TestCase):
return os_path
def add_code_cell(self, nb):
output = current.new_output("display_data", output_javascript="alert('hi');")
cell = current.new_code_cell("print('hi')", outputs=[output])
if not nb.worksheets:
nb.worksheets.append(current.new_worksheet())
nb.worksheets[0].cells.append(cell)
output = nbformat.new_output("display_data", {'application/javascript': "alert('hi');"})
cell = nbformat.new_code_cell("print('hi')", outputs=[output])
nb.cells.append(cell)
def new_notebook(self):
cm = self.contents_manager
model = cm.create_file()
model = cm.new_untitled(type='notebook')
name = model['name']
path = model['path']
full_model = cm.get_model(name, path)
full_model = cm.get(path)
nb = full_model['content']
self.add_code_cell(nb)
cm.save(full_model, name, path)
cm.save(full_model, path)
return nb, name, path
def test_create_file(self):
def test_new_untitled(self):
cm = self.contents_manager
# Test in root directory
model = cm.create_file()
model = cm.new_untitled(type='notebook')
assert isinstance(model, dict)
self.assertIn('name', model)
self.assertIn('path', model)
self.assertEqual(model['name'], 'Untitled0.ipynb')
self.assertEqual(model['path'], '')
self.assertIn('type', model)
self.assertEqual(model['type'], 'notebook')
self.assertEqual(model['name'], 'Untitled.ipynb')
self.assertEqual(model['path'], 'Untitled.ipynb')
# Test in sub-directory
sub_dir = '/foo/'
self.make_dir(cm.root_dir, 'foo')
model = cm.create_file(None, sub_dir)
model = cm.new_untitled(type='directory')
assert isinstance(model, dict)
self.assertIn('name', model)
self.assertIn('path', model)
self.assertEqual(model['name'], 'Untitled0.ipynb')
self.assertEqual(model['path'], sub_dir.strip('/'))
self.assertIn('type', model)
self.assertEqual(model['type'], 'directory')
self.assertEqual(model['name'], 'Untitled Folder')
self.assertEqual(model['path'], 'Untitled Folder')
sub_dir = model['path']
model = cm.new_untitled(path=sub_dir)
assert isinstance(model, dict)
self.assertIn('name', model)
self.assertIn('path', model)
self.assertIn('type', model)
self.assertEqual(model['type'], 'file')
self.assertEqual(model['name'], 'untitled')
self.assertEqual(model['path'], '%s/untitled' % sub_dir)
def test_get(self):
cm = self.contents_manager
# Create a notebook
model = cm.create_file()
model = cm.new_untitled(type='notebook')
name = model['name']
path = model['path']
# Check that we 'get' on the notebook we just created
model2 = cm.get_model(name, path)
model2 = cm.get(path)
assert isinstance(model2, dict)
self.assertIn('name', model2)
self.assertIn('path', model2)
self.assertEqual(model['name'], name)
self.assertEqual(model['path'], path)
nb_as_file = cm.get(path, content=True, type_='file')
self.assertEqual(nb_as_file['path'], path)
self.assertEqual(nb_as_file['type'], 'file')
self.assertEqual(nb_as_file['format'], 'text')
self.assertNotIsInstance(nb_as_file['content'], dict)
nb_as_bin_file = cm.get(path, content=True, type_='file', format='base64')
self.assertEqual(nb_as_bin_file['format'], 'base64')
# Test in sub-directory
sub_dir = '/foo/'
self.make_dir(cm.root_dir, 'foo')
model = cm.create_file(None, sub_dir)
model2 = cm.get_model(name, sub_dir)
model = cm.new_untitled(path=sub_dir, ext='.ipynb')
model2 = cm.get(sub_dir + name)
assert isinstance(model2, dict)
self.assertIn('name', model2)
self.assertIn('path', model2)
self.assertIn('content', model2)
self.assertEqual(model2['name'], 'Untitled0.ipynb')
self.assertEqual(model2['path'], sub_dir.strip('/'))
self.assertEqual(model2['name'], 'Untitled.ipynb')
self.assertEqual(model2['path'], '{0}/{1}'.format(sub_dir.strip('/'), name))
# Test getting directory model
dirmodel = cm.get('foo')
self.assertEqual(dirmodel['type'], 'directory')
with self.assertRaises(HTTPError):
cm.get('foo', type_='file')
@dec.skip_win32
def test_bad_symlink(self):
@ -167,26 +194,27 @@ class TestContentsManager(TestCase):
path = 'test bad symlink'
os_path = self.make_dir(cm.root_dir, path)
file_model = cm.create_file(path=path, ext='.txt')
file_model = cm.new_untitled(path=path, ext='.txt')
# create a broken symlink
os.symlink("target", os.path.join(os_path, "bad symlink"))
model = cm.get_model(path)
model = cm.get(path)
self.assertEqual(model['content'], [file_model])
@dec.skip_win32
def test_good_symlink(self):
cm = self.contents_manager
path = 'test good symlink'
os_path = self.make_dir(cm.root_dir, path)
parent = 'test good symlink'
name = 'good symlink'
path = '{0}/{1}'.format(parent, name)
os_path = self.make_dir(cm.root_dir, parent)
file_model = cm.create_file(path=path, ext='.txt')
file_model = cm.new(path=parent + '/zfoo.txt')
# create a good symlink
os.symlink(file_model['name'], os.path.join(os_path, "good symlink"))
symlink_model = cm.get_model(name="good symlink", path=path, content=False)
dir_model = cm.get_model(path)
os.symlink(file_model['name'], os.path.join(os_path, name))
symlink_model = cm.get(path, content=False)
dir_model = cm.get(parent)
self.assertEqual(
sorted(dir_model['content'], key=lambda x: x['name']),
[symlink_model, file_model],
@ -195,53 +223,54 @@ class TestContentsManager(TestCase):
def test_update(self):
cm = self.contents_manager
# Create a notebook
model = cm.create_file()
model = cm.new_untitled(type='notebook')
name = model['name']
path = model['path']
# Change the name in the model for rename
model['name'] = 'test.ipynb'
model = cm.update(model, name, path)
model['path'] = 'test.ipynb'
model = cm.update(model, path)
assert isinstance(model, dict)
self.assertIn('name', model)
self.assertIn('path', model)
self.assertEqual(model['name'], 'test.ipynb')
# Make sure the old name is gone
self.assertRaises(HTTPError, cm.get_model, name, path)
self.assertRaises(HTTPError, cm.get, path)
# Test in sub-directory
# Create a directory and notebook in that directory
sub_dir = '/foo/'
self.make_dir(cm.root_dir, 'foo')
model = cm.create_file(None, sub_dir)
model = cm.new_untitled(path=sub_dir, type='notebook')
name = model['name']
path = model['path']
# Change the name in the model for rename
model['name'] = 'test_in_sub.ipynb'
model = cm.update(model, name, path)
d = path.rsplit('/', 1)[0]
new_path = model['path'] = d + '/test_in_sub.ipynb'
model = cm.update(model, path)
assert isinstance(model, dict)
self.assertIn('name', model)
self.assertIn('path', model)
self.assertEqual(model['name'], 'test_in_sub.ipynb')
self.assertEqual(model['path'], sub_dir.strip('/'))
self.assertEqual(model['path'], new_path)
# Make sure the old name is gone
self.assertRaises(HTTPError, cm.get_model, name, path)
self.assertRaises(HTTPError, cm.get, path)
def test_save(self):
cm = self.contents_manager
# Create a notebook
model = cm.create_file()
model = cm.new_untitled(type='notebook')
name = model['name']
path = model['path']
# Get the model with 'content'
full_model = cm.get_model(name, path)
full_model = cm.get(path)
# Save the notebook
model = cm.save(full_model, name, path)
model = cm.save(full_model, path)
assert isinstance(model, dict)
self.assertIn('name', model)
self.assertIn('path', model)
@ -252,18 +281,18 @@ class TestContentsManager(TestCase):
# Create a directory and notebook in that directory
sub_dir = '/foo/'
self.make_dir(cm.root_dir, 'foo')
model = cm.create_file(None, sub_dir)
model = cm.new_untitled(path=sub_dir, type='notebook')
name = model['name']
path = model['path']
model = cm.get_model(name, path)
model = cm.get(path)
# Change the name in the model for rename
model = cm.save(model, name, path)
model = cm.save(model, path)
assert isinstance(model, dict)
self.assertIn('name', model)
self.assertIn('path', model)
self.assertEqual(model['name'], 'Untitled0.ipynb')
self.assertEqual(model['path'], sub_dir.strip('/'))
self.assertEqual(model['name'], 'Untitled.ipynb')
self.assertEqual(model['path'], 'foo/Untitled.ipynb')
def test_delete(self):
cm = self.contents_manager
@ -271,36 +300,42 @@ class TestContentsManager(TestCase):
nb, name, path = self.new_notebook()
# Delete the notebook
cm.delete(name, path)
cm.delete(path)
# Check that a 'get' on the deleted notebook raises and error
self.assertRaises(HTTPError, cm.get_model, name, path)
self.assertRaises(HTTPError, cm.get, path)
def test_copy(self):
cm = self.contents_manager
path = u'å b'
parent = u'å b'
name = u'nb √.ipynb'
os.mkdir(os.path.join(cm.root_dir, path))
orig = cm.create_file({'name' : name}, path=path)
path = u'{0}/{1}'.format(parent, name)
os.mkdir(os.path.join(cm.root_dir, parent))
orig = cm.new(path=path)
# copy with unspecified name
copy = cm.copy(name, path=path)
self.assertEqual(copy['name'], orig['name'].replace('.ipynb', '-Copy0.ipynb'))
copy = cm.copy(path)
self.assertEqual(copy['name'], orig['name'].replace('.ipynb', '-Copy1.ipynb'))
# copy with specified name
copy2 = cm.copy(name, u'copy 2.ipynb', path=path)
copy2 = cm.copy(path, u'å b/copy 2.ipynb')
self.assertEqual(copy2['name'], u'copy 2.ipynb')
self.assertEqual(copy2['path'], u'å b/copy 2.ipynb')
# copy with specified path
copy2 = cm.copy(path, u'/')
self.assertEqual(copy2['name'], name)
self.assertEqual(copy2['path'], name)
def test_trust_notebook(self):
cm = self.contents_manager
nb, name, path = self.new_notebook()
untrusted = cm.get_model(name, path)['content']
untrusted = cm.get(path)['content']
assert not cm.notary.check_cells(untrusted)
# print(untrusted)
cm.trust_notebook(name, path)
trusted = cm.get_model(name, path)['content']
cm.trust_notebook(path)
trusted = cm.get(path)['content']
# print(trusted)
assert cm.notary.check_cells(trusted)
@ -308,27 +343,27 @@ class TestContentsManager(TestCase):
cm = self.contents_manager
nb, name, path = self.new_notebook()
cm.mark_trusted_cells(nb, name, path)
for cell in nb.worksheets[0].cells:
cm.mark_trusted_cells(nb, path)
for cell in nb.cells:
if cell.cell_type == 'code':
assert not cell.trusted
assert not cell.metadata.trusted
cm.trust_notebook(name, path)
nb = cm.get_model(name, path)['content']
for cell in nb.worksheets[0].cells:
cm.trust_notebook(path)
nb = cm.get(path)['content']
for cell in nb.cells:
if cell.cell_type == 'code':
assert cell.trusted
assert cell.metadata.trusted
def test_check_and_sign(self):
cm = self.contents_manager
nb, name, path = self.new_notebook()
cm.mark_trusted_cells(nb, name, path)
cm.check_and_sign(nb, name, path)
cm.mark_trusted_cells(nb, path)
cm.check_and_sign(nb, path)
assert not cm.notary.check_signature(nb)
cm.trust_notebook(name, path)
nb = cm.get_model(name, path)['content']
cm.mark_trusted_cells(nb, name, path)
cm.check_and_sign(nb, name, path)
cm.trust_notebook(path)
nb = cm.get(path)['content']
cm.mark_trusted_cells(nb, path)
cm.check_and_sign(nb, path)
assert cm.notary.check_signature(nb)

@ -5,14 +5,16 @@
import json
import logging
from tornado import web
from tornado import gen, web
from tornado.concurrent import Future
from tornado.ioloop import IOLoop
from IPython.utils.jsonutil import date_default
from IPython.utils.py3compat import string_types
from IPython.utils.py3compat import cast_unicode
from IPython.html.utils import url_path_join, url_escape
from ...base.handlers import IPythonHandler, json_errors
from ...base.zmqhandlers import AuthenticatedZMQStreamHandler
from ...base.zmqhandlers import AuthenticatedZMQStreamHandler, deserialize_binary_message
from IPython.core.release import kernel_protocol_version
@ -27,16 +29,16 @@ class MainKernelHandler(IPythonHandler):
@web.authenticated
@json_errors
def post(self):
km = self.kernel_manager
model = self.get_json_body()
if model is None:
raise web.HTTPError(400, "No JSON data provided")
try:
name = model['name']
except KeyError:
raise web.HTTPError(400, "Missing field in JSON data: name")
model = {
'name': km.default_kernel_name
}
else:
model.setdefault('name', km.default_kernel_name)
km = self.kernel_manager
kernel_id = km.start_kernel(kernel_name=name)
kernel_id = km.start_kernel(kernel_name=model['name'])
model = km.kernel_model(kernel_id)
location = url_path_join(self.base_url, 'api', 'kernels', kernel_id)
self.set_header('Location', url_escape(location))
@ -84,6 +86,10 @@ class KernelActionHandler(IPythonHandler):
class ZMQChannelHandler(AuthenticatedZMQStreamHandler):
@property
def kernel_info_timeout(self):
return self.settings.get('kernel_info_timeout', 10)
def __repr__(self):
return "%s(%s)" % (self.__class__.__name__, getattr(self, 'kernel_id', 'uninitialized'))
@ -91,17 +97,29 @@ class ZMQChannelHandler(AuthenticatedZMQStreamHandler):
km = self.kernel_manager
meth = getattr(km, 'connect_%s' % self.channel)
self.zmq_stream = meth(self.kernel_id, identity=self.session.bsession)
# Create a kernel_info channel to query the kernel protocol version.
# This channel will be closed after the kernel_info reply is received.
self.kernel_info_channel = None
self.kernel_info_channel = km.connect_shell(self.kernel_id)
self.kernel_info_channel.on_recv(self._handle_kernel_info_reply)
self._request_kernel_info()
def _request_kernel_info(self):
def request_kernel_info(self):
"""send a request for kernel_info"""
self.log.debug("requesting kernel info")
self.session.send(self.kernel_info_channel, "kernel_info_request")
km = self.kernel_manager
kernel = km.get_kernel(self.kernel_id)
try:
# check for previous request
future = kernel._kernel_info_future
except AttributeError:
self.log.debug("Requesting kernel info from %s", self.kernel_id)
# Create a kernel_info channel to query the kernel protocol version.
# This channel will be closed after the kernel_info reply is received.
if self.kernel_info_channel is None:
self.kernel_info_channel = km.connect_shell(self.kernel_id)
self.kernel_info_channel.on_recv(self._handle_kernel_info_reply)
self.session.send(self.kernel_info_channel, "kernel_info_request")
# store the future on the kernel, so only one request is sent
kernel._kernel_info_future = self._kernel_info_future
else:
if not future.done():
self.log.debug("Waiting for pending kernel_info request")
future.add_done_callback(lambda f: self._finish_kernel_info(f.result()))
return self._kernel_info_future
def _handle_kernel_info_reply(self, msg):
"""process the kernel_info_reply
@ -110,35 +128,75 @@ class ZMQChannelHandler(AuthenticatedZMQStreamHandler):
"""
idents,msg = self.session.feed_identities(msg)
try:
msg = self.session.unserialize(msg)
msg = self.session.deserialize(msg)
except:
self.log.error("Bad kernel_info reply", exc_info=True)
self._request_kernel_info()
self._kernel_info_future.set_result({})
return
else:
if msg['msg_type'] != 'kernel_info_reply' or 'protocol_version' not in msg['content']:
self.log.error("Kernel info request failed, assuming current %s", msg['content'])
else:
protocol_version = msg['content']['protocol_version']
if protocol_version != kernel_protocol_version:
self.session.adapt_version = int(protocol_version.split('.')[0])
self.log.info("adapting kernel to %s" % protocol_version)
self.kernel_info_channel.close()
info = msg['content']
self.log.debug("Received kernel info: %s", info)
if msg['msg_type'] != 'kernel_info_reply' or 'protocol_version' not in info:
self.log.error("Kernel info request failed, assuming current %s", info)
info = {}
self._finish_kernel_info(info)
# close the kernel_info channel, we don't need it anymore
if self.kernel_info_channel:
self.kernel_info_channel.close()
self.kernel_info_channel = None
def _finish_kernel_info(self, info):
"""Finish handling kernel_info reply
Set up protocol adaptation, if needed,
and signal that connection can continue.
"""
protocol_version = info.get('protocol_version', kernel_protocol_version)
if protocol_version != kernel_protocol_version:
self.session.adapt_version = int(protocol_version.split('.')[0])
self.log.info("Kernel %s speaks protocol %s", self.kernel_id, protocol_version)
if not self._kernel_info_future.done():
self._kernel_info_future.set_result(info)
def initialize(self, *args, **kwargs):
def initialize(self):
super(ZMQChannelHandler, self).initialize()
self.zmq_stream = None
self.kernel_id = None
self.kernel_info_channel = None
self._kernel_info_future = Future()
def on_first_message(self, msg):
try:
super(ZMQChannelHandler, self).on_first_message(msg)
except web.HTTPError:
self.close()
return
@gen.coroutine
def pre_get(self):
# authenticate first
super(ZMQChannelHandler, self).pre_get()
# then request kernel info, waiting up to a certain time before giving up.
# We don't want to wait forever, because browsers don't take it well when
# servers never respond to websocket connection requests.
future = self.request_kernel_info()
def give_up():
"""Don't wait forever for the kernel to reply"""
if future.done():
return
self.log.warn("Timeout waiting for kernel_info reply from %s", self.kernel_id)
future.set_result({})
loop = IOLoop.current()
loop.add_timeout(loop.time() + self.kernel_info_timeout, give_up)
# actually wait for it
yield future
@gen.coroutine
def get(self, kernel_id):
self.kernel_id = cast_unicode(kernel_id, 'ascii')
yield super(ZMQChannelHandler, self).get(kernel_id=kernel_id)
def open(self, kernel_id):
super(ZMQChannelHandler, self).open()
try:
self.create_stream()
except web.HTTPError:
except web.HTTPError as e:
self.log.error("Error opening stream: %s", e)
# WebSockets don't response to traditional error codes so we
# close the connection.
if not self.stream.closed():
@ -154,7 +212,10 @@ class ZMQChannelHandler(AuthenticatedZMQStreamHandler):
self.log.info("%s closed, closing websocket.", self)
self.close()
return
msg = json.loads(msg)
if isinstance(msg, bytes):
msg = deserialize_binary_message(msg)
else:
msg = json.loads(msg)
self.session.send(self.zmq_stream, msg)
def on_close(self):

@ -1,20 +1,11 @@
"""A kernel manager relating notebooks and kernels
"""A MultiKernelManager for use in the notebook webserver
Authors:
* Brian Granger
- raises HTTPErrors
- creates REST API models
"""
#-----------------------------------------------------------------------------
# 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
#-----------------------------------------------------------------------------
# Copyright (c) IPython Development Team.
# Distributed under the terms of the Modified BSD License.
import os
@ -26,10 +17,6 @@ from IPython.utils.traitlets import List, Unicode, TraitError
from IPython.html.utils import to_os_path
from IPython.utils.py3compat import getcwd
#-----------------------------------------------------------------------------
# Classes
#-----------------------------------------------------------------------------
class MappingKernelManager(MultiKernelManager):
"""A KernelManager that handles notebook mapping and HTTP error handling"""
@ -39,7 +26,13 @@ class MappingKernelManager(MultiKernelManager):
kernel_argv = List(Unicode)
root_dir = Unicode(getcwd(), config=True)
root_dir = Unicode(config=True)
def _root_dir_default(self):
try:
return self.parent.notebook_dir
except AttributeError:
return getcwd()
def _root_dir_changed(self, name, old, new):
"""Do a bit of validation of the root dir."""
@ -61,14 +54,10 @@ class MappingKernelManager(MultiKernelManager):
def cwd_for_path(self, path):
"""Turn API path into absolute OS path."""
# short circuit for NotebookManagers that pass in absolute paths
if os.path.exists(path):
return path
os_path = to_os_path(path, self.root_dir)
# in the case of notebooks and kernels not being on the same filesystem,
# walk up to root_dir if the paths don't exist
while not os.path.exists(os_path) and os_path != self.root_dir:
while not os.path.isdir(os_path) and os_path != self.root_dir:
os_path = os.path.dirname(os_path)
return os_path
@ -89,7 +78,6 @@ class MappingKernelManager(MultiKernelManager):
an existing kernel is returned, but it may be checked in the future.
"""
if kernel_id is None:
kwargs['extra_arguments'] = self.kernel_argv
if path is not None:
kwargs['cwd'] = self.cwd_for_path(path)
kernel_id = super(MappingKernelManager, self).start_kernel(

@ -57,6 +57,19 @@ class KernelAPITest(NotebookTestBase):
kernels = self.kern_api.list().json()
self.assertEqual(kernels, [])
def test_default_kernel(self):
# POST request
r = self.kern_api._req('POST', '')
kern1 = r.json()
self.assertEqual(r.headers['location'], '/api/kernels/' + kern1['id'])
self.assertEqual(r.status_code, 201)
self.assertIsInstance(kern1, dict)
self.assertEqual(r.headers['Content-Security-Policy'], (
"frame-ancestors 'self'; "
"report-uri /api/security/csp-report;"
))
def test_main_kernel_handler(self):
# POST request
r = self.kern_api.start()
@ -65,7 +78,10 @@ class KernelAPITest(NotebookTestBase):
self.assertEqual(r.status_code, 201)
self.assertIsInstance(kern1, dict)
self.assertEqual(r.headers['x-frame-options'], "SAMEORIGIN")
self.assertEqual(r.headers['Content-Security-Policy'], (
"frame-ancestors 'self'; "
"report-uri /api/security/csp-report;"
))
# GET request
r = self.kern_api.list()

@ -19,7 +19,11 @@ class MainKernelSpecHandler(IPythonHandler):
ksm = self.kernel_spec_manager
results = []
for kernel_name in sorted(ksm.find_kernel_specs(), key=_pythonfirst):
d = ksm.get_kernel_spec(kernel_name).to_dict()
try:
d = ksm.get_kernel_spec(kernel_name).to_dict()
except Exception:
self.log.error("Failed to load kernel spec: '%s'", kernel_name, exc_info=True)
continue
d['name'] = kernel_name
results.append(d)

@ -5,6 +5,7 @@ import errno
import io
import json
import os
import shutil
pjoin = os.path.join
@ -18,7 +19,6 @@ from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_erro
# break these tests
sample_kernel_json = {'argv':['cat', '{connection_file}'],
'display_name':'Test kernel',
'language':'bash',
}
some_resource = u"The very model of a modern major general"
@ -66,6 +66,25 @@ class APITest(NotebookTestBase):
self.ks_api = KernelSpecAPI(self.base_url())
def test_list_kernelspecs_bad(self):
"""Can list kernelspecs when one is invalid"""
bad_kernel_dir = pjoin(self.ipython_dir.name, 'kernels', 'bad')
try:
os.makedirs(bad_kernel_dir)
except OSError as e:
if e.errno != errno.EEXIST:
raise
with open(pjoin(bad_kernel_dir, 'kernel.json'), 'w') as f:
f.write("garbage")
specs = self.ks_api.list().json()
assert isinstance(specs, list)
# 2: the sample kernelspec created in setUp, and the native Python kernel
self.assertGreaterEqual(len(specs), 2)
shutil.rmtree(bad_kernel_dir)
def test_list_kernelspecs(self):
specs = self.ks_api.list().json()
assert isinstance(specs, list)
@ -84,7 +103,7 @@ class APITest(NotebookTestBase):
def test_get_kernelspec(self):
spec = self.ks_api.kernel_spec_info('Sample').json() # Case insensitive
self.assertEqual(spec['language'], 'bash')
self.assertEqual(spec['display_name'], 'Test kernel')
def test_get_nonexistant_kernelspec(self):
with assert_http_error(404):

@ -0,0 +1,4 @@
# URI for the CSP Report. Included here to prevent a cyclic dependency.
# csp_report_uri is needed both by the BaseHandler (for setting the report-uri)
# and by the CSPReportHandler (which depends on the BaseHandler).
csp_report_uri = r"/api/security/csp-report"

@ -0,0 +1,23 @@
"""Tornado handlers for security logging."""
# Copyright (c) IPython Development Team.
# Distributed under the terms of the Modified BSD License.
from tornado import gen, web
from ...base.handlers import IPythonHandler, json_errors
from . import csp_report_uri
class CSPReportHandler(IPythonHandler):
'''Accepts a content security policy violation report'''
@web.authenticated
@json_errors
def post(self):
'''Log a content security policy violation report'''
csp_report = self.get_json_body()
self.log.warn("Content security violation: %s",
self.request.body.decode('utf8', 'replace'))
default_handlers = [
(csp_report_uri, CSPReportHandler)
]

@ -10,6 +10,7 @@ from tornado import web
from ...base.handlers import IPythonHandler, json_errors
from IPython.utils.jsonutil import date_default
from IPython.html.utils import url_path_join, url_escape
from IPython.kernel.kernelspec import NoSuchKernel
class SessionRootHandler(IPythonHandler):
@ -34,10 +35,6 @@ class SessionRootHandler(IPythonHandler):
model = self.get_json_body()
if model is None:
raise web.HTTPError(400, "No JSON data provided")
try:
name = model['notebook']['name']
except KeyError:
raise web.HTTPError(400, "Missing field in JSON data: notebook.name")
try:
path = model['notebook']['path']
except KeyError:
@ -45,13 +42,24 @@ class SessionRootHandler(IPythonHandler):
try:
kernel_name = model['kernel']['name']
except KeyError:
raise web.HTTPError(400, "Missing field in JSON data: kernel.name")
self.log.debug("No kernel name specified, using default kernel")
kernel_name = None
# Check to see if session exists
if sm.session_exists(name=name, path=path):
model = sm.get_session(name=name, path=path)
if sm.session_exists(path=path):
model = sm.get_session(path=path)
else:
model = sm.create_session(name=name, path=path, kernel_name=kernel_name)
try:
model = sm.create_session(path=path, kernel_name=kernel_name)
except NoSuchKernel:
msg = ("The '%s' kernel is not available. Please pick another "
"suitable kernel instead, or install that kernel." % kernel_name)
status_msg = '%s not found' % kernel_name
self.log.warn('Kernel not found: %s' % kernel_name)
self.set_status(501)
self.finish(json.dumps(dict(message=msg, short_message=status_msg)))
return
location = url_path_join(self.base_url, 'api', 'sessions', model['id'])
self.set_header('Location', url_escape(location))
self.set_status(201)
@ -80,8 +88,6 @@ class SessionHandler(IPythonHandler):
changes = {}
if 'notebook' in model:
notebook = model['notebook']
if 'name' in notebook:
changes['name'] = notebook['name']
if 'path' in notebook:
changes['path'] = notebook['path']
@ -94,7 +100,11 @@ class SessionHandler(IPythonHandler):
def delete(self, session_id):
# Deletes the session with given session_id
sm = self.session_manager
sm.delete_session(session_id)
try:
sm.delete_session(session_id)
except KeyError:
# the kernel was deleted but the session wasn't!
raise web.HTTPError(410, "Kernel deleted before session")
self.set_status(204)
self.finish()

@ -1,20 +1,7 @@
"""A base class session manager.
"""A base class session manager."""
Authors:
* Zach Sailer
"""
#-----------------------------------------------------------------------------
# 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
#-----------------------------------------------------------------------------
# Copyright (c) IPython Development Team.
# Distributed under the terms of the Modified BSD License.
import uuid
import sqlite3
@ -25,9 +12,6 @@ from IPython.config.configurable import LoggingConfigurable
from IPython.utils.py3compat import unicode_type
from IPython.utils.traitlets import Instance
#-----------------------------------------------------------------------------
# Classes
#-----------------------------------------------------------------------------
class SessionManager(LoggingConfigurable):
@ -37,7 +21,7 @@ class SessionManager(LoggingConfigurable):
# Session database initialized below
_cursor = None
_connection = None
_columns = {'session_id', 'name', 'path', 'kernel_id'}
_columns = {'session_id', 'path', 'kernel_id'}
@property
def cursor(self):
@ -45,7 +29,7 @@ class SessionManager(LoggingConfigurable):
if self._cursor is None:
self._cursor = self.connection.cursor()
self._cursor.execute("""CREATE TABLE session
(session_id, name, path, kernel_id)""")
(session_id, path, kernel_id)""")
return self._cursor
@property
@ -60,9 +44,9 @@ class SessionManager(LoggingConfigurable):
"""Close connection once SessionManager closes"""
self.cursor.close()
def session_exists(self, name, path):
def session_exists(self, path):
"""Check to see if the session for a given notebook exists"""
self.cursor.execute("SELECT * FROM session WHERE name=? AND path=?", (name, path))
self.cursor.execute("SELECT * FROM session WHERE path=?", (path,))
reply = self.cursor.fetchone()
if reply is None:
return False
@ -73,17 +57,17 @@ class SessionManager(LoggingConfigurable):
"Create a uuid for a new session"
return unicode_type(uuid.uuid4())
def create_session(self, name=None, path=None, kernel_name='python'):
def create_session(self, path=None, kernel_name=None):
"""Creates a session and returns its model"""
session_id = self.new_session_id()
# allow nbm to specify kernels cwd
kernel_path = self.contents_manager.get_kernel_path(name=name, path=path)
kernel_path = self.contents_manager.get_kernel_path(path=path)
kernel_id = self.kernel_manager.start_kernel(path=kernel_path,
kernel_name=kernel_name)
return self.save_session(session_id, name=name, path=path,
return self.save_session(session_id, path=path,
kernel_id=kernel_id)
def save_session(self, session_id, name=None, path=None, kernel_id=None):
def save_session(self, session_id, path=None, kernel_id=None):
"""Saves the items for the session with the given session_id
Given a session_id (and any other of the arguments), this method
@ -94,10 +78,8 @@ class SessionManager(LoggingConfigurable):
----------
session_id : str
uuid for the session; this method must be given a session_id
name : str
the .ipynb notebook name that started the session
path : str
the path to the named notebook
the path for the given notebook
kernel_id : str
a uuid for the kernel associated with this session
@ -106,8 +88,8 @@ class SessionManager(LoggingConfigurable):
model : dict
a dictionary of the session model
"""
self.cursor.execute("INSERT INTO session VALUES (?,?,?,?)",
(session_id, name, path, kernel_id)
self.cursor.execute("INSERT INTO session VALUES (?,?,?)",
(session_id, path, kernel_id)
)
return self.get_session(session_id=session_id)
@ -121,7 +103,7 @@ class SessionManager(LoggingConfigurable):
----------
**kwargs : keyword argument
must be given one of the keywords and values from the session database
(i.e. session_id, name, path, kernel_id)
(i.e. session_id, path, kernel_id)
Returns
-------
@ -198,7 +180,6 @@ class SessionManager(LoggingConfigurable):
model = {
'id': row['session_id'],
'notebook': {
'name': row['name'],
'path': row['path']
},
'kernel': self.kernel_manager.kernel_model(row['kernel_id'])

@ -32,24 +32,24 @@ class TestSessionManager(TestCase):
def test_get_session(self):
sm = SessionManager(kernel_manager=DummyMKM())
session_id = sm.create_session(name='test.ipynb', path='/path/to/',
session_id = sm.create_session(path='/path/to/test.ipynb',
kernel_name='bar')['id']
model = sm.get_session(session_id=session_id)
expected = {'id':session_id,
'notebook':{'name':u'test.ipynb', 'path': u'/path/to/'},
'notebook':{'path': u'/path/to/test.ipynb'},
'kernel': {'id':u'A', 'name': 'bar'}}
self.assertEqual(model, expected)
def test_bad_get_session(self):
# Should raise error if a bad key is passed to the database.
sm = SessionManager(kernel_manager=DummyMKM())
session_id = sm.create_session(name='test.ipynb', path='/path/to/',
session_id = sm.create_session(path='/path/to/test.ipynb',
kernel_name='foo')['id']
self.assertRaises(TypeError, sm.get_session, bad_id=session_id) # Bad keyword
def test_get_session_dead_kernel(self):
sm = SessionManager(kernel_manager=DummyMKM())
session = sm.create_session(name='test1.ipynb', path='/path/to/1/', kernel_name='python')
session = sm.create_session(path='/path/to/1/test1.ipynb', kernel_name='python')
# kill the kernel
sm.kernel_manager.shutdown_kernel(session['kernel']['id'])
with self.assertRaises(KeyError):
@ -61,24 +61,33 @@ class TestSessionManager(TestCase):
def test_list_sessions(self):
sm = SessionManager(kernel_manager=DummyMKM())
sessions = [
sm.create_session(name='test1.ipynb', path='/path/to/1/', kernel_name='python'),
sm.create_session(name='test2.ipynb', path='/path/to/2/', kernel_name='python'),
sm.create_session(name='test3.ipynb', path='/path/to/3/', kernel_name='python'),
sm.create_session(path='/path/to/1/test1.ipynb', kernel_name='python'),
sm.create_session(path='/path/to/2/test2.ipynb', kernel_name='python'),
sm.create_session(path='/path/to/3/test3.ipynb', kernel_name='python'),
]
sessions = sm.list_sessions()
expected = [{'id':sessions[0]['id'], 'notebook':{'name':u'test1.ipynb',
'path': u'/path/to/1/'}, 'kernel':{'id':u'A', 'name':'python'}},
{'id':sessions[1]['id'], 'notebook': {'name':u'test2.ipynb',
'path': u'/path/to/2/'}, 'kernel':{'id':u'B', 'name':'python'}},
{'id':sessions[2]['id'], 'notebook':{'name':u'test3.ipynb',
'path': u'/path/to/3/'}, 'kernel':{'id':u'C', 'name':'python'}}]
expected = [
{
'id':sessions[0]['id'],
'notebook':{'path': u'/path/to/1/test1.ipynb'},
'kernel':{'id':u'A', 'name':'python'}
}, {
'id':sessions[1]['id'],
'notebook': {'path': u'/path/to/2/test2.ipynb'},
'kernel':{'id':u'B', 'name':'python'}
}, {
'id':sessions[2]['id'],
'notebook':{'path': u'/path/to/3/test3.ipynb'},
'kernel':{'id':u'C', 'name':'python'}
}
]
self.assertEqual(sessions, expected)
def test_list_sessions_dead_kernel(self):
sm = SessionManager(kernel_manager=DummyMKM())
sessions = [
sm.create_session(name='test1.ipynb', path='/path/to/1/', kernel_name='python'),
sm.create_session(name='test2.ipynb', path='/path/to/2/', kernel_name='python'),
sm.create_session(path='/path/to/1/test1.ipynb', kernel_name='python'),
sm.create_session(path='/path/to/2/test2.ipynb', kernel_name='python'),
]
# kill one of the kernels
sm.kernel_manager.shutdown_kernel(sessions[0]['kernel']['id'])
@ -87,8 +96,7 @@ class TestSessionManager(TestCase):
{
'id': sessions[1]['id'],
'notebook': {
'name': u'test2.ipynb',
'path': u'/path/to/2/',
'path': u'/path/to/2/test2.ipynb',
},
'kernel': {
'id': u'B',
@ -100,41 +108,47 @@ class TestSessionManager(TestCase):
def test_update_session(self):
sm = SessionManager(kernel_manager=DummyMKM())
session_id = sm.create_session(name='test.ipynb', path='/path/to/',
session_id = sm.create_session(path='/path/to/test.ipynb',
kernel_name='julia')['id']
sm.update_session(session_id, name='new_name.ipynb')
sm.update_session(session_id, path='/path/to/new_name.ipynb')
model = sm.get_session(session_id=session_id)
expected = {'id':session_id,
'notebook':{'name':u'new_name.ipynb', 'path': u'/path/to/'},
'notebook':{'path': u'/path/to/new_name.ipynb'},
'kernel':{'id':u'A', 'name':'julia'}}
self.assertEqual(model, expected)
def test_bad_update_session(self):
# try to update a session with a bad keyword ~ raise error
sm = SessionManager(kernel_manager=DummyMKM())
session_id = sm.create_session(name='test.ipynb', path='/path/to/',
session_id = sm.create_session(path='/path/to/test.ipynb',
kernel_name='ir')['id']
self.assertRaises(TypeError, sm.update_session, session_id=session_id, bad_kw='test.ipynb') # Bad keyword
def test_delete_session(self):
sm = SessionManager(kernel_manager=DummyMKM())
sessions = [
sm.create_session(name='test1.ipynb', path='/path/to/1/', kernel_name='python'),
sm.create_session(name='test2.ipynb', path='/path/to/2/', kernel_name='python'),
sm.create_session(name='test3.ipynb', path='/path/to/3/', kernel_name='python'),
sm.create_session(path='/path/to/1/test1.ipynb', kernel_name='python'),
sm.create_session(path='/path/to/2/test2.ipynb', kernel_name='python'),
sm.create_session(path='/path/to/3/test3.ipynb', kernel_name='python'),
]
sm.delete_session(sessions[1]['id'])
new_sessions = sm.list_sessions()
expected = [{'id':sessions[0]['id'], 'notebook':{'name':u'test1.ipynb',
'path': u'/path/to/1/'}, 'kernel':{'id':u'A', 'name':'python'}},
{'id':sessions[2]['id'], 'notebook':{'name':u'test3.ipynb',
'path': u'/path/to/3/'}, 'kernel':{'id':u'C', 'name':'python'}}]
expected = [{
'id': sessions[0]['id'],
'notebook': {'path': u'/path/to/1/test1.ipynb'},
'kernel': {'id':u'A', 'name':'python'}
}, {
'id': sessions[2]['id'],
'notebook': {'path': u'/path/to/3/test3.ipynb'},
'kernel': {'id':u'C', 'name':'python'}
}
]
self.assertEqual(new_sessions, expected)
def test_bad_delete_session(self):
# try to delete a session that doesn't exist ~ raise error
sm = SessionManager(kernel_manager=DummyMKM())
sm.create_session(name='test.ipynb', path='/path/to/', kernel_name='python')
sm.create_session(path='/path/to/test.ipynb', kernel_name='python')
self.assertRaises(TypeError, sm.delete_session, bad_kwarg='23424') # Bad keyword
self.assertRaises(web.HTTPError, sm.delete_session, session_id='23424') # nonexistant

@ -11,7 +11,8 @@ pjoin = os.path.join
from IPython.html.utils import url_path_join
from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
from IPython.nbformat.current import new_notebook, write
from IPython.nbformat.v4 import new_notebook
from IPython.nbformat import write
class SessionAPI(object):
"""Wrapper for notebook API calls."""
@ -37,13 +38,13 @@ class SessionAPI(object):
def get(self, id):
return self._req('GET', id)
def create(self, name, path, kernel_name='python'):
body = json.dumps({'notebook': {'name':name, 'path':path},
def create(self, path, kernel_name='python'):
body = json.dumps({'notebook': {'path':path},
'kernel': {'name': kernel_name}})
return self._req('POST', '', body)
def modify(self, id, name, path):
body = json.dumps({'notebook': {'name':name, 'path':path}})
def modify(self, id, path):
body = json.dumps({'notebook': {'path':path}})
return self._req('PATCH', id, body)
def delete(self, id):
@ -62,8 +63,8 @@ class SessionAPITest(NotebookTestBase):
with io.open(pjoin(nbdir, 'foo', 'nb1.ipynb'), 'w',
encoding='utf-8') as f:
nb = new_notebook(name='nb1')
write(nb, f, format='ipynb')
nb = new_notebook()
write(nb, f, version=4)
self.sess_api = SessionAPI(self.base_url())
@ -77,12 +78,11 @@ class SessionAPITest(NotebookTestBase):
sessions = self.sess_api.list().json()
self.assertEqual(len(sessions), 0)
resp = self.sess_api.create('nb1.ipynb', 'foo')
resp = self.sess_api.create('foo/nb1.ipynb')
self.assertEqual(resp.status_code, 201)
newsession = resp.json()
self.assertIn('id', newsession)
self.assertEqual(newsession['notebook']['name'], 'nb1.ipynb')
self.assertEqual(newsession['notebook']['path'], 'foo')
self.assertEqual(newsession['notebook']['path'], 'foo/nb1.ipynb')
self.assertEqual(resp.headers['Location'], '/api/sessions/{0}'.format(newsession['id']))
sessions = self.sess_api.list().json()
@ -94,7 +94,7 @@ class SessionAPITest(NotebookTestBase):
self.assertEqual(got, newsession)
def test_delete(self):
newsession = self.sess_api.create('nb1.ipynb', 'foo').json()
newsession = self.sess_api.create('foo/nb1.ipynb').json()
sid = newsession['id']
resp = self.sess_api.delete(sid)
@ -107,10 +107,9 @@ class SessionAPITest(NotebookTestBase):
self.sess_api.get(sid)
def test_modify(self):
newsession = self.sess_api.create('nb1.ipynb', 'foo').json()
newsession = self.sess_api.create('foo/nb1.ipynb').json()
sid = newsession['id']
changed = self.sess_api.modify(sid, 'nb2.ipynb', '').json()
changed = self.sess_api.modify(sid, 'nb2.ipynb').json()
self.assertEqual(changed['id'], sid)
self.assertEqual(changed['notebook']['name'], 'nb2.ipynb')
self.assertEqual(changed['notebook']['path'], '')
self.assertEqual(changed['notebook']['path'], 'nb2.ipynb')

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

@ -4,7 +4,8 @@
define([
'base/js/namespace',
'jquery',
], function(IPython, $) {
'codemirror/lib/codemirror',
], function(IPython, $, CodeMirror) {
"use strict";
var modal = function (options) {
@ -90,6 +91,17 @@ define([
return modal.modal(options);
};
var kernel_modal = function (options) {
/**
* only one kernel dialog should be open at a time -- but
* other modal dialogs can still be open
*/
$('.kernel-modal').modal('hide');
var dialog = modal(options);
dialog.addClass('kernel-modal');
return dialog;
};
var edit_metadata = function (options) {
options.name = options.name || "Cell";
var error_div = $('<div/>').css('color', 'red');
@ -130,7 +142,9 @@ define([
buttons: {
OK: { class : "btn-primary",
click: function() {
// validate json and set it
/**
* validate json and set it
*/
var new_md;
try {
new_md = JSON.parse(editor.getValue());
@ -153,6 +167,7 @@ define([
var dialog = {
modal : modal,
kernel_modal : kernel_modal,
edit_metadata : edit_metadata,
};

@ -1,22 +1,33 @@
// Copyright (c) IPython Development Team.
// Distributed under the terms of the Modified BSD License.
/**
*
*
* @module keyboard
* @namespace keyboard
* @class ShortcutManager
*/
define([
'base/js/namespace',
'jquery',
'base/js/utils',
], function(IPython, $, utils) {
'underscore',
], function(IPython, $, utils, _) {
"use strict";
// Setup global keycodes and inverse keycodes.
/**
* Setup global keycodes and inverse keycodes.
*
* See http://unixpapa.com/js/key.html for a complete description. The short of
* it is that there are different keycode sets. Firefox uses the "Mozilla keycodes"
* and Webkit/IE use the "IE keycodes". These keycode sets are mostly the same
* but have minor differences.
**/
// See http://unixpapa.com/js/key.html for a complete description. The short of
// it is that there are different keycode sets. Firefox uses the "Mozilla keycodes"
// and Webkit/IE use the "IE keycodes". These keycode sets are mostly the same
// but have minor differences.
// These apply to Firefox, (Webkit and IE)
// These apply to Firefox, (Webkit and IE)
// This does work **only** on US keyboard.
var _keycodes = {
'a': 65, 'b': 66, 'c': 67, 'd': 68, 'e': 69, 'f': 70, 'g': 71, 'h': 72, 'i': 73,
'j': 74, 'k': 75, 'l': 76, 'm': 77, 'n': 78, 'o': 79, 'p': 80, 'q': 81, 'r': 82,
@ -77,13 +88,32 @@ define([
};
var normalize_shortcut = function (shortcut) {
// Put a shortcut into normalized form:
// 1. Make lowercase
// 2. Replace cmd by meta
// 3. Sort '-' separated modifiers into the order alt-ctrl-meta-shift
// 4. Normalize keys
/**
* @function _normalize_shortcut
* @private
* return a dict containing the normalized shortcut and the number of time it should be pressed:
*
* Put a shortcut into normalized form:
* 1. Make lowercase
* 2. Replace cmd by meta
* 3. Sort '-' separated modifiers into the order alt-ctrl-meta-shift
* 4. Normalize keys
**/
if (platform === 'MacOS') {
shortcut = shortcut.toLowerCase().replace('cmdtrl-', 'cmd-');
} else {
shortcut = shortcut.toLowerCase().replace('cmdtrl-', 'ctrl-');
}
shortcut = shortcut.toLowerCase().replace('cmd', 'meta');
shortcut = shortcut.replace(/-$/, '_'); // catch shortcuts using '-' key
shortcut = shortcut.replace(/,$/, 'comma'); // catch shortcuts using '-' key
if(shortcut.indexOf(',') !== -1){
var sht = shortcut.split(',');
sht = _.map(sht, normalize_shortcut);
return shortcut;
}
shortcut = shortcut.replace(/comma/g, ','); // catch shortcuts using '-' key
var values = shortcut.split("-");
if (values.length === 1) {
return normalize_key(values[0]);
@ -96,7 +126,9 @@ define([
};
var shortcut_to_event = function (shortcut, type) {
// Convert a shortcut (shift-r) to a jQuery Event object
/**
* Convert a shortcut (shift-r) to a jQuery Event object
**/
type = type || 'keydown';
shortcut = normalize_shortcut(shortcut);
shortcut = shortcut.replace(/-$/, '_'); // catch shortcuts using '-' key
@ -111,8 +143,21 @@ define([
return $.Event(type, opts);
};
var only_modifier_event = function(event){
/**
* Return `true` if the event only contains modifiers keys.
* false otherwise
**/
var key = inv_keycodes[event.which];
return ((event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) &&
(key === 'alt'|| key === 'ctrl'|| key === 'meta'|| key === 'shift'));
};
var event_to_shortcut = function (event) {
// Convert a jQuery Event object to a shortcut (shift-r)
/**
* Convert a jQuery Event object to a normalized shortcut string (shift-r)
**/
var shortcut = '';
var key = inv_keycodes[event.which];
if (event.altKey && key !== 'alt') {shortcut += 'alt-';}
@ -125,35 +170,86 @@ define([
// Shortcut manager class
var ShortcutManager = function (delay, events) {
var ShortcutManager = function (delay, events, actions, env) {
/**
* A class to deal with keyboard event and shortcut
*
* @class ShortcutManager
* @constructor
*/
this._shortcuts = {};
this._counts = {};
this._timers = {};
this.delay = delay || 800; // delay in milliseconds
this.events = events;
this.actions = actions;
this.actions.extend_env(env);
this._queue = [];
this._cleartimeout = null;
Object.seal(this);
};
ShortcutManager.prototype.clearsoon = function(){
/**
* Clear the pending shortcut soon, and cancel previous clearing
* that might be registered.
**/
var that = this;
clearTimeout(this._cleartimeout);
this._cleartimeout = setTimeout(function(){that.clearqueue();}, this.delay);
};
ShortcutManager.prototype.clearqueue = function(){
/**
* clear the pending shortcut sequence now.
**/
this._queue = [];
clearTimeout(this._cleartimeout);
};
var flatten_shorttree = function(tree){
/**
* Flatten a tree of shortcut sequences.
* use full to iterate over all the key/values of available shortcuts.
**/
var dct = {};
for(var key in tree){
var value = tree[key];
if(typeof(value) === 'string'){
dct[key] = value;
} else {
var ftree=flatten_shorttree(value);
for(var subkey in ftree){
dct[key+','+subkey] = ftree[subkey];
}
}
}
return dct;
};
ShortcutManager.prototype.help = function () {
var help = [];
for (var shortcut in this._shortcuts) {
var help_string = this._shortcuts[shortcut].help;
var help_index = this._shortcuts[shortcut].help_index;
var ftree = flatten_shorttree(this._shortcuts);
for (var shortcut in ftree) {
var action = this.actions.get(ftree[shortcut]);
var help_string = action.help||'== no help ==';
var help_index = action.help_index;
if (help_string) {
if (platform === 'MacOS') {
shortcut = shortcut.replace('meta', 'cmd');
}
var shortstring = (action.shortstring||shortcut);
help.push({
shortcut: shortcut,
shortcut: shortstring,
help: help_string,
help_index: help_index}
);
}
}
help.sort(function (a, b) {
if (a.help_index > b.help_index)
if (a.help_index > b.help_index){
return 1;
if (a.help_index < b.help_index)
}
if (a.help_index < b.help_index){
return -1;
}
return 0;
});
return help;
@ -163,19 +259,105 @@ define([
this._shortcuts = {};
};
ShortcutManager.prototype.add_shortcut = function (shortcut, data, suppress_help_update) {
if (typeof(data) === 'function') {
data = {help: '', help_index: '', handler: data};
ShortcutManager.prototype.get_shortcut = function (shortcut){
/**
* return a node of the shortcut tree which an action name (string) if leaf,
* and an object with `object.subtree===true`
**/
if(typeof(shortcut) === 'string'){
shortcut = shortcut.split(',');
}
return this._get_leaf(shortcut, this._shortcuts);
};
ShortcutManager.prototype._get_leaf = function(shortcut_array, tree){
/**
* @private
* find a leaf/node in a subtree of the keyboard shortcut
*
**/
if(shortcut_array.length === 1){
return tree[shortcut_array[0]];
} else if( typeof(tree[shortcut_array[0]]) !== 'string'){
return this._get_leaf(shortcut_array.slice(1), tree[shortcut_array[0]]);
}
return null;
};
ShortcutManager.prototype.set_shortcut = function( shortcut, action_name){
if( typeof(action_name) !== 'string'){ throw('action is not a string', action_name);}
if( typeof(shortcut) === 'string'){
shortcut = shortcut.split(',');
}
return this._set_leaf(shortcut, action_name, this._shortcuts);
};
ShortcutManager.prototype._is_leaf = function(shortcut_array, tree){
if(shortcut_array.length === 1){
return(typeof(tree[shortcut_array[0]]) === 'string');
} else {
var subtree = tree[shortcut_array[0]];
return this._is_leaf(shortcut_array.slice(1), subtree );
}
data.help_index = data.help_index || '';
data.help = data.help || '';
data.count = data.count || 1;
if (data.help_index === '') {
data.help_index = 'zz';
};
ShortcutManager.prototype._remove_leaf = function(shortcut_array, tree, allow_node){
if(shortcut_array.length === 1){
var current_node = tree[shortcut_array[0]];
if(typeof(current_node) === 'string'){
delete tree[shortcut_array[0]];
} else {
throw('try to delete non-leaf');
}
} else {
this._remove_leaf(shortcut_array.slice(1), tree[shortcut_array[0]], allow_node);
if(_.keys(tree[shortcut_array[0]]).length === 0){
delete tree[shortcut_array[0]];
}
}
};
ShortcutManager.prototype._set_leaf = function(shortcut_array, action_name, tree){
var current_node = tree[shortcut_array[0]];
if(shortcut_array.length === 1){
if(current_node !== undefined && typeof(current_node) !== 'string'){
console.warn('[warning], you are overriting a long shortcut with a shorter one');
}
tree[shortcut_array[0]] = action_name;
return true;
} else {
if(typeof(current_node) === 'string'){
console.warn('you are trying to set a shortcut that will be shadowed'+
'by a more specific one. Aborting for :', action_name, 'the follwing '+
'will take precedence', current_node);
return false;
} else {
tree[shortcut_array[0]] = tree[shortcut_array[0]]||{};
}
this._set_leaf(shortcut_array.slice(1), action_name, tree[shortcut_array[0]]);
return true;
}
};
ShortcutManager.prototype.add_shortcut = function (shortcut, data, suppress_help_update) {
/**
* Add a action to be handled by shortcut manager.
*
* - `shortcut` should be a `Shortcut Sequence` of the for `Ctrl-Alt-C,Meta-X`...
* - `data` could be an `action name`, an `action` or a `function`.
* if a `function` is passed it will be converted to an anonymous `action`.
*
**/
var action_name = this.actions.get_name(data);
if (! action_name){
throw('does nto know how to deal with ', data);
}
shortcut = normalize_shortcut(shortcut);
this._counts[shortcut] = 0;
this._shortcuts[shortcut] = data;
this.set_shortcut(shortcut, action_name);
if (!suppress_help_update) {
// update the keyboard shortcuts notebook help
this.events.trigger('rebuild.QuickHelp');
@ -183,6 +365,11 @@ define([
};
ShortcutManager.prototype.add_shortcuts = function (data) {
/**
* Convenient methods to call `add_shortcut(key, value)` on several items
*
* data : Dict of the form {key:value, ...}
**/
for (var shortcut in data) {
this.add_shortcut(shortcut, data[shortcut], true);
}
@ -191,55 +378,63 @@ define([
};
ShortcutManager.prototype.remove_shortcut = function (shortcut, suppress_help_update) {
/**
* Remove the binding of shortcut `sortcut` with its action.
* throw an error if trying to remove a non-exiting shortcut
**/
shortcut = normalize_shortcut(shortcut);
delete this._counts[shortcut];
delete this._shortcuts[shortcut];
if( typeof(shortcut) === 'string'){
shortcut = shortcut.split(',');
}
this._remove_leaf(shortcut, this._shortcuts);
if (!suppress_help_update) {
// update the keyboard shortcuts notebook help
this.events.trigger('rebuild.QuickHelp');
}
};
ShortcutManager.prototype.count_handler = function (shortcut, event, data) {
var that = this;
var c = this._counts;
var t = this._timers;
var timer = null;
if (c[shortcut] === data.count-1) {
c[shortcut] = 0;
timer = t[shortcut];
if (timer) {clearTimeout(timer); delete t[shortcut];}
return data.handler(event);
} else {
c[shortcut] = c[shortcut] + 1;
timer = setTimeout(function () {
c[shortcut] = 0;
}, that.delay);
t[shortcut] = timer;
}
return false;
};
ShortcutManager.prototype.call_handler = function (event) {
/**
* Call the corresponding shortcut handler for a keyboard event
* @method call_handler
* @return {Boolean} `true|false`, `false` if no handler was found, otherwise the value return by the handler.
* @param event {event}
*
* given an event, call the corresponding shortcut.
* return false is event wan handled, true otherwise
* in any case returning false stop event propagation
**/
this.clearsoon();
if(only_modifier_event(event)){
return true;
}
var shortcut = event_to_shortcut(event);
var data = this._shortcuts[shortcut];
if (data) {
var handler = data.handler;
if (handler) {
if (data.count === 1) {
return handler(event);
} else if (data.count > 1) {
return this.count_handler(shortcut, event, data);
}
}
this._queue.push(shortcut);
var action_name = this.get_shortcut(this._queue);
if (typeof(action_name) === 'undefined'|| action_name === null){
this.clearqueue();
return true;
}
return true;
if (this.actions.exists(action_name)) {
event.preventDefault();
this.clearqueue();
return this.actions.call(action_name, event);
}
return false;
};
ShortcutManager.prototype.handles = function (event) {
var shortcut = event_to_shortcut(event);
var data = this._shortcuts[shortcut];
return !( data === undefined || data.handler === undefined );
var action_name = this.get_shortcut(this._queue.concat(shortcut));
return (typeof(action_name) !== 'undefined');
};
var keyboard = {
@ -249,10 +444,10 @@ define([
normalize_key : normalize_key,
normalize_shortcut : normalize_shortcut,
shortcut_to_event : shortcut_to_event,
event_to_shortcut : event_to_shortcut
event_to_shortcut : event_to_shortcut,
};
// For backwards compatability.
// For backwards compatibility.
IPython.keyboard = keyboard;
return keyboard;

@ -3,6 +3,7 @@
var IPython = IPython || {};
define([], function(){
"use strict";
IPython.version = "3.0.0-dev";
return IPython;
});

@ -0,0 +1,83 @@
// Copyright (c) IPython Development Team.
// Distributed under the terms of the Modified BSD License.
define([
'jquery',
'base/js/notificationwidget',
], function($, notificationwidget) {
"use strict";
// store reference to the NotificationWidget class
var NotificationWidget = notificationwidget.NotificationWidget;
/**
* Construct the NotificationArea object. Options are:
* events: $(Events) instance
* save_widget: SaveWidget instance
* notebook: Notebook instance
* keyboard_manager: KeyboardManager instance
*
* @constructor
* @param {string} selector - a jQuery selector string for the
* notification area element
* @param {Object} [options] - a dictionary of keyword arguments.
*/
var NotificationArea = function (selector, options) {
this.selector = selector;
this.events = options.events;
if (this.selector !== undefined) {
this.element = $(selector);
}
this.widget_dict = {};
};
/**
* Get a widget by name, creating it if it doesn't exist.
*
* @method widget
* @param {string} name - the widget name
*/
NotificationArea.prototype.widget = function (name) {
if (this.widget_dict[name] === undefined) {
return this.new_notification_widget(name);
}
return this.get_widget(name);
};
/**
* Get a widget by name, throwing an error if it doesn't exist.
*
* @method get_widget
* @param {string} name - the widget name
*/
NotificationArea.prototype.get_widget = function (name) {
if(this.widget_dict[name] === undefined) {
throw('no widgets with this name');
}
return this.widget_dict[name];
};
/**
* Create a new notification widget with the given name. The
* widget must not already exist.
*
* @method new_notification_widget
* @param {string} name - the widget name
*/
NotificationArea.prototype.new_notification_widget = function (name) {
if (this.widget_dict[name] !== undefined) {
throw('widget with that name already exists!');
}
// create the element for the notification widget and add it
// to the notification aread element
var div = $('<div/>').attr('id', 'notification_' + name);
$(this.selector).append(div);
// create the widget object and return it
this.widget_dict[name] = new NotificationWidget('#notification_' + name);
return this.widget_dict[name];
};
return {'NotificationArea': NotificationArea};
});

@ -0,0 +1,160 @@
// Copyright (c) IPython Development Team.
// Distributed under the terms of the Modified BSD License.
define([
'base/js/namespace',
'jquery',
], function(IPython, $) {
"use strict";
/**
* Construct a NotificationWidget object.
*
* @constructor
* @param {string} selector - a jQuery selector string for the
* notification widget element
*/
var NotificationWidget = function (selector) {
this.selector = selector;
this.timeout = null;
this.busy = false;
if (this.selector !== undefined) {
this.element = $(selector);
this.style();
}
this.element.hide();
this.inner = $('<span/>');
this.element.append(this.inner);
};
/**
* Add the 'notification_widget' CSS class to the widget element.
*
* @method style
*/
NotificationWidget.prototype.style = function () {
this.element.addClass('notification_widget');
};
/**
* Set the notification widget message to display for a certain
* amount of time (timeout). The widget will be shown forever if
* timeout is <= 0 or undefined. If the widget is clicked while it
* is still displayed, execute an optional callback
* (click_callback). If the callback returns false, it will
* prevent the notification from being dismissed.
*
* Options:
* class - CSS class name for styling
* icon - CSS class name for the widget icon
* title - HTML title attribute for the widget
*
* @method set_message
* @param {string} msg - The notification to display
* @param {integer} [timeout] - The amount of time in milliseconds to display the widget
* @param {function} [click_callback] - The function to run when the widget is clicked
* @param {Object} [options] - Additional options
*/
NotificationWidget.prototype.set_message = function (msg, timeout, click_callback, options) {
options = options || {};
// unbind potential previous callback
this.element.unbind('click');
this.inner.attr('class', options.icon);
this.inner.attr('title', options.title);
this.inner.text(msg);
this.element.fadeIn(100);
// reset previous set style
this.element.removeClass();
this.style();
if (options.class) {
this.element.addClass(options.class);
}
// clear previous timer
if (this.timeout !== null) {
clearTimeout(this.timeout);
this.timeout = null;
}
// set the timer if a timeout is given
var that = this;
if (timeout !== undefined && timeout >= 0) {
this.timeout = setTimeout(function () {
that.element.fadeOut(100, function () {that.inner.text('');});
that.element.unbind('click');
that.timeout = null;
}, timeout);
}
// bind the click callback if it is given
if (click_callback !== undefined) {
this.element.click(function () {
if (click_callback() !== false) {
that.element.fadeOut(100, function () {that.inner.text('');});
}
that.element.unbind('click');
if (that.timeout !== null) {
clearTimeout(that.timeout);
that.timeout = null;
}
});
}
};
/**
* Display an information message (styled with the 'info'
* class). Arguments are the same as in set_message. Default
* timeout is 3500 milliseconds.
*
* @method info
*/
NotificationWidget.prototype.info = function (msg, timeout, click_callback, options) {
options = options || {};
options.class = options.class + ' info';
timeout = timeout || 3500;
this.set_message(msg, timeout, click_callback, options);
};
/**
* Display a warning message (styled with the 'warning'
* class). Arguments are the same as in set_message. Messages are
* sticky by default.
*
* @method warning
*/
NotificationWidget.prototype.warning = function (msg, timeout, click_callback, options) {
options = options || {};
options.class = options.class + ' warning';
this.set_message(msg, timeout, click_callback, options);
};
/**
* Display a danger message (styled with the 'danger'
* class). Arguments are the same as in set_message. Messages are
* sticky by default.
*
* @method danger
*/
NotificationWidget.prototype.danger = function (msg, timeout, click_callback, options) {
options = options || {};
options.class = options.class + ' danger';
this.set_message(msg, timeout, click_callback, options);
};
/**
* Get the text of the widget message.
*
* @method get_message
* @return {string} - the message text
*/
NotificationWidget.prototype.get_message = function () {
return this.inner.html();
};
// For backwards compatibility.
IPython.NotificationWidget = NotificationWidget;
return {'NotificationWidget': NotificationWidget};
});

@ -15,23 +15,29 @@ define([
};
Page.prototype.show = function () {
// The header and site divs start out hidden to prevent FLOUC.
// Main scripts should call this method after styling everything.
/**
* The header and site divs start out hidden to prevent FLOUC.
* Main scripts should call this method after styling everything.
*/
this.show_header();
this.show_site();
};
Page.prototype.show_header = 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
/**
* 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');
};
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
/**
* 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');
};

@ -18,8 +18,10 @@ define([
}
var sanitizeAttribs = function (tagName, attribs, opt_naiveUriRewriter, opt_nmTokenPolicy, opt_logger) {
// add trusting data-attributes to the default sanitizeAttribs from caja
// this function is mostly copied from the caja source
/**
* add trusting data-attributes to the default sanitizeAttribs from caja
* this function is mostly copied from the caja source
*/
var ATTRIBS = caja.html4.ATTRIBS;
for (var i = 0; i < attribs.length; i += 2) {
var attribName = attribs[i];
@ -34,9 +36,11 @@ define([
};
var sanitize_css = function (css, tagPolicy) {
// sanitize CSS
// like sanitize_html, but for CSS
// called by sanitize_stylesheets
/**
* sanitize CSS
* like sanitize_html, but for CSS
* called by sanitize_stylesheets
*/
return caja.sanitizeStylesheet(
window.location.pathname,
css,
@ -51,8 +55,10 @@ define([
};
var sanitize_stylesheets = function (html, tagPolicy) {
// sanitize just the css in style tags in a block of html
// called by sanitize_html, if allow_css is true
/**
* sanitize just the css in style tags in a block of html
* called by sanitize_html, if allow_css is true
*/
var h = $("<div/>").append(html);
var style_tags = h.find("style");
if (!style_tags.length) {
@ -66,9 +72,11 @@ define([
};
var sanitize_html = function (html, allow_css) {
// sanitize HTML
// if allow_css is true (default: false), CSS is sanitized as well.
// otherwise, CSS elements and attributes are simply removed.
/**
* sanitize HTML
* if allow_css is true (default: false), CSS is sanitized as well.
* otherwise, CSS elements and attributes are simply removed.
*/
var html4 = caja.html4;
if (allow_css) {

@ -4,7 +4,8 @@
define([
'base/js/namespace',
'jquery',
], function(IPython, $){
'codemirror/lib/codemirror',
], function(IPython, $, CodeMirror){
"use strict";
IPython.load_extensions = function () {
@ -153,7 +154,9 @@ define([
var uuid = function () {
// http://www.ietf.org/rfc/rfc4122.txt
/**
* http://www.ietf.org/rfc/rfc4122.txt
*/
var s = [];
var hexDigits = "0123456789ABCDEF";
for (var i = 0; i < 32; i++) {
@ -271,11 +274,11 @@ define([
} else {
line = "background-color: ";
}
line = line + "rgb(" + r + "," + g + "," + b + ");"
if ( !attrs["style"] ) {
attrs["style"] = line;
line = line + "rgb(" + r + "," + g + "," + b + ");";
if ( !attrs.style ) {
attrs.style = line;
} else {
attrs["style"] += " " + line;
attrs.style += " " + line;
}
}
}
@ -284,27 +287,36 @@ define([
function ansispan(str) {
// ansispan function adapted from github.com/mmalecki/ansispan (MIT License)
// regular ansi escapes (using the table above)
var is_open = false;
return str.replace(/\033\[(0?[01]|22|39)?([;\d]+)?m/g, function(match, prefix, pattern) {
if (!pattern) {
// [(01|22|39|)m close spans
return "</span>";
}
// consume sequence of color escapes
var numbers = pattern.match(/\d+/g);
var attrs = {};
while (numbers.length > 0) {
_process_numbers(attrs, numbers);
}
var span = "<span ";
for (var attr in attrs) {
var value = attrs[attr];
span = span + " " + attr + '="' + attrs[attr] + '"';
if (is_open) {
is_open = false;
return "</span>";
} else {
return "";
}
} else {
is_open = true;
// consume sequence of color escapes
var numbers = pattern.match(/\d+/g);
var attrs = {};
while (numbers.length > 0) {
_process_numbers(attrs, numbers);
}
var span = "<span ";
for (var attr in attrs) {
var value = attrs[attr];
span = span + " " + attr + '="' + attrs[attr] + '"';
}
return span + ">";
}
return span + ">";
});
};
}
// Transform ANSI color escape codes into HTML <span> tags with css
// classes listed in the above ansi_colormap object. The actual color used
// are set in the css file.
@ -345,7 +357,9 @@ define([
}
var points_to_pixels = function (points) {
// A reasonably good way of converting between points and pixels.
/**
* A reasonably good way of converting between points and pixels.
*/
var test = $('<div style="display: none; width: 10000pt; padding:0; border:0;"></div>');
$(body).append(test);
var pixel_per_point = test.width()/10000;
@ -354,10 +368,12 @@ define([
};
var always_new = function (constructor) {
// wrapper around contructor to avoid requiring `var a = new constructor()`
// useful for passing constructors as callbacks,
// not for programmer laziness.
// from http://programmers.stackexchange.com/questions/118798
/**
* wrapper around contructor to avoid requiring `var a = new constructor()`
* useful for passing constructors as callbacks,
* not for programmer laziness.
* from http://programmers.stackexchange.com/questions/118798
*/
return function () {
var obj = Object.create(constructor.prototype);
constructor.apply(obj, arguments);
@ -366,7 +382,9 @@ define([
};
var url_path_join = function () {
// join a sequence of url components with '/'
/**
* join a sequence of url components with '/'
*/
var url = '';
for (var i = 0; i < arguments.length; i++) {
if (arguments[i] === '') {
@ -382,36 +400,58 @@ define([
return url;
};
var url_path_split = function (path) {
/**
* Like os.path.split for URLs.
* Always returns two strings, the directory path and the base filename
*/
var idx = path.lastIndexOf('/');
if (idx === -1) {
return ['', path];
} else {
return [ path.slice(0, idx), path.slice(idx + 1) ];
}
};
var parse_url = function (url) {
// an `a` element with an href allows attr-access to the parsed segments of a URL
// a = parse_url("http://localhost:8888/path/name#hash")
// a.protocol = "http:"
// a.host = "localhost:8888"
// a.hostname = "localhost"
// a.port = 8888
// a.pathname = "/path/name"
// a.hash = "#hash"
/**
* an `a` element with an href allows attr-access to the parsed segments of a URL
* a = parse_url("http://localhost:8888/path/name#hash")
* a.protocol = "http:"
* a.host = "localhost:8888"
* a.hostname = "localhost"
* a.port = 8888
* a.pathname = "/path/name"
* a.hash = "#hash"
*/
var a = document.createElement("a");
a.href = url;
return a;
};
var encode_uri_components = function (uri) {
// encode just the components of a multi-segment uri,
// leaving '/' separators
/**
* encode just the components of a multi-segment uri,
* leaving '/' separators
*/
return uri.split('/').map(encodeURIComponent).join('/');
};
var url_join_encode = function () {
// join a sequence of url components with '/',
// encoding each component with encodeURIComponent
/**
* join a sequence of url components with '/',
* encoding each component with encodeURIComponent
*/
return encode_uri_components(url_path_join.apply(null, arguments));
};
var splitext = function (filename) {
// mimic Python os.path.splitext
// Returns ['base', '.ext']
/**
* mimic Python os.path.splitext
* Returns ['base', '.ext']
*/
var idx = filename.lastIndexOf('.');
if (idx > 0) {
return [filename.slice(0, idx), filename.slice(idx)];
@ -422,20 +462,26 @@ define([
var escape_html = function (text) {
// escape text to HTML
/**
* escape text to HTML
*/
return $("<div/>").text(text).html();
};
var get_body_data = function(key) {
// get a url-encoded item from body.data and decode it
// we should never have any encoded URLs anywhere else in code
// until we are building an actual request
/**
* get a url-encoded item from body.data and decode it
* we should never have any encoded URLs anywhere else in code
* until we are building an actual request
*/
return decodeURIComponent($('body').data(key));
};
var to_absolute_cursor_pos = function (cm, cursor) {
// get the absolute cursor position from CodeMirror's col, ch
/**
* get the absolute cursor position from CodeMirror's col, ch
*/
if (!cursor) {
cursor = cm.getCursor();
}
@ -447,7 +493,9 @@ define([
};
var from_absolute_cursor_pos = function (cm, cursor_pos) {
// turn absolute cursor postion into CodeMirror col, ch cursor
/**
* turn absolute cursor postion into CodeMirror col, ch cursor
*/
var i, line;
var offset = 0;
for (i = 0, line=cm.getLine(i); line !== undefined; i++, line=cm.getLine(i)) {
@ -495,12 +543,16 @@ define([
})();
var is_or_has = function (a, b) {
// Is b a child of a or a itself?
/**
* Is b a child of a or a itself?
*/
return a.has(b).length !==0 || a.is(b);
};
var is_focused = function (e) {
// Is element e, or one of its children focused?
/**
* Is element e, or one of its children focused?
*/
e = $(e);
var target = $(document.activeElement);
if (target.length > 0) {
@ -521,21 +573,198 @@ define([
};
var ajax_error_msg = function (jqXHR) {
// Return a JSON error message if there is one,
// otherwise the basic HTTP status text.
if (jqXHR.responseJSON && jqXHR.responseJSON.message) {
/**
* Return a JSON error message if there is one,
* otherwise the basic HTTP status text.
*/
if (jqXHR.responseJSON && jqXHR.responseJSON.traceback) {
return jqXHR.responseJSON.traceback;
} else if (jqXHR.responseJSON && jqXHR.responseJSON.message) {
return jqXHR.responseJSON.message;
} else {
return jqXHR.statusText;
}
}
};
var log_ajax_error = function (jqXHR, status, error) {
// log ajax failures with informative messages
/**
* log ajax failures with informative messages
*/
var msg = "API request failed (" + jqXHR.status + "): ";
console.log(jqXHR);
msg += ajax_error_msg(jqXHR);
console.log(msg);
};
var requireCodeMirrorMode = function (mode, callback, errback) {
/**
* load a mode with requirejs
*/
if (typeof mode != "string") mode = mode.name;
if (CodeMirror.modes.hasOwnProperty(mode)) {
callback(CodeMirror.modes.mode);
return;
}
require([
// might want to use CodeMirror.modeURL here
['codemirror/mode', mode, mode].join('/'),
], callback, errback
);
};
/** Error type for wrapped XHR errors. */
var XHR_ERROR = 'XhrError';
/**
* Wraps an AJAX error as an Error object.
*/
var wrap_ajax_error = function (jqXHR, status, error) {
var wrapped_error = new Error(ajax_error_msg(jqXHR));
wrapped_error.name = XHR_ERROR;
// provide xhr response
wrapped_error.xhr = jqXHR;
wrapped_error.xhr_status = status;
wrapped_error.xhr_error = error;
return wrapped_error;
};
var promising_ajax = function(url, settings) {
/**
* Like $.ajax, but returning an ES6 promise. success and error settings
* will be ignored.
*/
return new Promise(function(resolve, reject) {
settings.success = function(data, status, jqXHR) {
resolve(data);
};
settings.error = function(jqXHR, status, error) {
log_ajax_error(jqXHR, status, error);
reject(wrap_ajax_error(jqXHR, status, error));
};
$.ajax(url, settings);
});
};
var WrappedError = function(message, error){
/**
* Wrappable Error class
*
* The Error class doesn't actually act on `this`. Instead it always
* returns a new instance of Error. Here we capture that instance so we
* can apply it's properties to `this`.
*/
var tmp = Error.apply(this, [message]);
// Copy the properties of the error over to this.
var properties = Object.getOwnPropertyNames(tmp);
for (var i = 0; i < properties.length; i++) {
this[properties[i]] = tmp[properties[i]];
}
// Keep a stack of the original error messages.
if (error instanceof WrappedError) {
this.error_stack = error.error_stack;
} else {
this.error_stack = [error];
}
this.error_stack.push(tmp);
return this;
};
WrappedError.prototype = Object.create(Error.prototype, {});
var load_class = function(class_name, module_name, registry) {
/**
* Tries to load a class
*
* Tries to load a class from a module using require.js, if a module
* is specified, otherwise tries to load a class from the global
* registry, if the global registry is provided.
*/
return new Promise(function(resolve, reject) {
// Try loading the view module using require.js
if (module_name) {
require([module_name], function(module) {
if (module[class_name] === undefined) {
reject(new Error('Class '+class_name+' not found in module '+module_name));
} else {
resolve(module[class_name]);
}
}, reject);
} else {
if (registry && registry[class_name]) {
resolve(registry[class_name]);
} else {
reject(new Error('Class '+class_name+' not found in registry '));
}
}
});
};
var resolve_promises_dict = function(d) {
/**
* Resolve a promiseful dictionary.
* Returns a single Promise.
*/
var keys = Object.keys(d);
var values = [];
keys.forEach(function(key) {
values.push(d[key]);
});
return Promise.all(values).then(function(v) {
d = {};
for(var i=0; i<keys.length; i++) {
d[keys[i]] = v[i];
}
return d;
});
};
var WrappedError = function(message, error){
/**
* Wrappable Error class
*
* The Error class doesn't actually act on `this`. Instead it always
* returns a new instance of Error. Here we capture that instance so we
* can apply it's properties to `this`.
*/
var tmp = Error.apply(this, [message]);
// Copy the properties of the error over to this.
var properties = Object.getOwnPropertyNames(tmp);
for (var i = 0; i < properties.length; i++) {
this[properties[i]] = tmp[properties[i]];
}
// Keep a stack of the original error messages.
if (error instanceof WrappedError) {
this.error_stack = error.error_stack;
} else {
this.error_stack = [error];
}
this.error_stack.push(tmp);
return this;
};
WrappedError.prototype = Object.create(Error.prototype, {});
var reject = function(message, log) {
/**
* Creates a wrappable Promise rejection function.
*
* Creates a function that returns a Promise.reject with a new WrappedError
* that has the provided message and wraps the original error that
* caused the promise to reject.
*/
return function(error) {
var wrapped_error = new WrappedError(message, error);
if (log) console.error(wrapped_error);
return Promise.reject(wrapped_error);
};
};
var utils = {
regex_split : regex_split,
@ -546,6 +775,7 @@ define([
points_to_pixels : points_to_pixels,
get_body_data : get_body_data,
parse_url : parse_url,
url_path_split : url_path_split,
url_path_join : url_path_join,
url_join_encode : url_join_encode,
encode_uri_components : encode_uri_components,
@ -561,6 +791,14 @@ define([
mergeopt: mergeopt,
ajax_error_msg : ajax_error_msg,
log_ajax_error : log_ajax_error,
requireCodeMirrorMode : requireCodeMirrorMode,
XHR_ERROR : XHR_ERROR,
wrap_ajax_error : wrap_ajax_error,
promising_ajax : promising_ajax,
WrappedError: WrappedError,
load_class: load_class,
resolve_promises_dict: resolve_promises_dict,
reject: reject,
};
// Backwards compatability.

@ -8,6 +8,7 @@
@breadcrumb-color: darken(@border_color, 30%);
@blockquote-font-size: inherit;
@modal-inner-padding: 15px;
@grid-float-breakpoint: 540px;
// Disable modal slide-in from top animation.
.modal {

@ -0,0 +1,78 @@
// Copyright (c) IPython Development Team.
// Distributed under the terms of the Modified BSD License.
define([
'jquery',
'base/js/utils',
'codemirror/lib/codemirror',
'codemirror/mode/meta',
'codemirror/addon/search/search'
],
function($,
utils,
CodeMirror
) {
var Editor = function(selector, options) {
this.selector = selector;
this.contents = options.contents;
this.events = options.events;
this.base_url = options.base_url;
this.file_path = options.file_path;
this.codemirror = CodeMirror($(this.selector)[0]);
// It appears we have to set commands on the CodeMirror class, not the
// instance. I'd like to be wrong, but since there should only be one CM
// instance on the page, this is good enough for now.
CodeMirror.commands.save = $.proxy(this.save, this);
this.save_enabled = false;
};
Editor.prototype.load = function() {
var that = this;
var cm = this.codemirror;
this.contents.get(this.file_path, {type: 'file', format: 'text'})
.then(function(model) {
cm.setValue(model.content);
// Setting the file's initial value creates a history entry,
// which we don't want.
cm.clearHistory();
// Find and load the highlighting mode
var modeinfo = CodeMirror.findModeByMIME(model.mimetype);
if (modeinfo) {
utils.requireCodeMirrorMode(modeinfo.mode, function() {
cm.setOption('mode', modeinfo.mode);
});
}
that.save_enabled = true;
},
function(error) {
cm.setValue("Error! " + error.message +
"\nSaving disabled.");
that.save_enabled = false;
}
);
};
Editor.prototype.save = function() {
if (!this.save_enabled) {
console.log("Not saving, save disabled");
return;
}
var model = {
path: this.file_path,
type: 'file',
format: 'text',
content: this.codemirror.getValue(),
};
var that = this;
this.contents.save(this.file_path, model).then(function() {
that.events.trigger("save_succeeded.TextEditor");
});
};
return {Editor: Editor};
});

@ -0,0 +1,64 @@
// Copyright (c) IPython Development Team.
// Distributed under the terms of the Modified BSD License.
require([
'base/js/namespace',
'base/js/utils',
'base/js/page',
'base/js/events',
'contents',
'services/config',
'edit/js/editor',
'edit/js/menubar',
'edit/js/notificationarea',
'custom/custom',
], function(
IPython,
utils,
page,
events,
contents,
configmod,
editor,
menubar,
notificationarea
){
page = new page.Page();
var base_url = utils.get_body_data('baseUrl');
var file_path = utils.get_body_data('filePath');
contents = new contents.Contents({base_url: base_url});
var config = new configmod.ConfigSection('edit', {base_url: base_url})
config.load();
var editor = new editor.Editor('#texteditor-container', {
base_url: base_url,
events: events,
contents: contents,
file_path: file_path,
});
// Make it available for debugging
IPython.editor = editor;
var menus = new menubar.MenuBar('#menubar', {
base_url: base_url,
editor: editor,
});
var notification_area = new notificationarea.EditorNotificationArea(
'#notification_area', {
events: events,
});
notification_area.init_notification_widgets();
config.loaded.then(function() {
if (config.data.load_extensions) {
var nbextension_paths = Object.getOwnPropertyNames(
config.data.load_extensions);
IPython.load_extensions.apply(this, nbextension_paths);
}
});
editor.load();
page.show();
});

@ -0,0 +1,50 @@
// Copyright (c) IPython Development Team.
// Distributed under the terms of the Modified BSD License.
define([
'base/js/namespace',
'jquery',
'base/js/utils',
'bootstrap',
], function(IPython, $, utils, bootstrap) {
"use strict";
var MenuBar = function (selector, options) {
/**
* Constructor
*
* A MenuBar Class to generate the menubar of IPython notebook
*
* Parameters:
* selector: string
* options: dictionary
* Dictionary of keyword arguments.
* codemirror: CodeMirror instance
* contents: ContentManager instance
* events: $(Events) instance
* base_url : string
* file_path : string
*/
options = options || {};
this.base_url = options.base_url || utils.get_body_data("baseUrl");
this.selector = selector;
this.editor = options.editor;
if (this.selector !== undefined) {
this.element = $(selector);
this.bind_events();
}
};
MenuBar.prototype.bind_events = function () {
/**
* File
*/
var that = this;
this.element.find('#save_file').click(function () {
that.editor.save();
});
};
return {'MenuBar': MenuBar};
});

@ -0,0 +1,29 @@
define([
'base/js/notificationarea'
], function(notificationarea) {
"use strict";
var NotificationArea = notificationarea.NotificationArea;
var EditorNotificationArea = function(selector, options) {
NotificationArea.apply(this, [selector, options]);
}
EditorNotificationArea.prototype = Object.create(NotificationArea.prototype);
/**
* Initialize the default set of notification widgets.
*
* @method init_notification_widgets
*/
EditorNotificationArea.prototype.init_notification_widgets = function () {
var that = this;
var enw = this.new_notification_widget('editor');
this.events.on("save_succeeded.TextEditor", function() {
enw.set_message("File saved", 2000);
});
};
return {EditorNotificationArea: EditorNotificationArea};
});

@ -0,0 +1,38 @@
// Copyright (c) IPython Development Team.
// Distributed under the terms of the Modified BSD License.
require([
'jquery',
'base/js/dialog',
'underscore',
'base/js/namespace'
], function ($, dialog, _, IPython) {
'use strict';
$('#notebook_about').click(function () {
// use underscore template to auto html escape
var text = 'You are using IPython notebook.<br/><br/>';
text = text + 'The version of the notebook server is ';
text = text + _.template('<b><%- version %></b>')({ version: sys_info.ipython_version });
if (sys_info.commit_hash) {
text = text + _.template('-<%- hash %>')({ hash: sys_info.commit_hash });
}
text = text + _.template(' and is running on:<br/><pre>Python <%- pyver %></pre>')({ pyver: sys_info.sys_version });
var kinfo = $('<div/>').attr('id', '#about-kinfo').text('Waiting for kernel to be available...');
var body = $('<div/>');
body.append($('<h4/>').text('Server Information:'));
body.append($('<p/>').html(text));
body.append($('<h4/>').text('Current Kernel Information:'));
body.append(kinfo);
dialog.modal({
title: 'About IPython Notebook',
body: body,
buttons: { 'OK': {} }
});
try {
IPython.notebook.session.kernel.kernel_info(function (data) {
kinfo.html($('<pre/>').text(data.content.banner));
});
} catch (e) {
kinfo.html($('<p/>').text('unable to contact kernel'));
}
});
});

@ -0,0 +1,503 @@
// Copyright (c) IPython Development Team.
// Distributed under the terms of the Modified BSD License.
define(['require'
], function(require) {
"use strict";
var ActionHandler = function (env) {
this.env = env || {};
Object.seal(this);
};
/**
* A bunch of predefined `Simple Actions` used by IPython.
* `Simple Actions` have the following keys:
* help (optional): a short string the describe the action.
* will be used in various context, like as menu name, tool tips on buttons,
* and short description in help menu.
* help_index (optional): a string used to sort action in help menu.
* icon (optional): a short string that represent the icon that have to be used with this
* action. this should mainly correspond to a Font_awesome class.
* handler : a function which is called when the action is activated. It will receive at first parameter
* a dictionary containing various handle to element of the notebook.
*
* action need to be registered with a **name** that can be use to refer to this action.
*
*
* if `help` is not provided it will be derived by replacing any dash by space
* in the **name** of the action. It is advised to provide a prefix to action name to
* avoid conflict the prefix should be all lowercase and end with a dot `.`
* in the absence of a prefix the behavior of the action is undefined.
*
* All action provided by IPython are prefixed with `ipython.`.
*
* One can register extra actions or replace an existing action with another one is possible
* but is considered undefined behavior.
*
**/
var _action = {
'run-select-next': {
icon: 'fa-play',
help : 'run cell, select below',
help_index : 'ba',
handler : function (env) {
env.notebook.execute_cell_and_select_below();
}
},
'execute-in-place':{
help : 'run cell',
help_index : 'bb',
handler : function (env) {
env.notebook.execute_cell();
}
},
'execute-and-insert-after':{
help : 'run cell, insert below',
help_index : 'bc',
handler : function (env) {
env.notebook.execute_cell_and_insert_below();
}
},
'go-to-command-mode': {
help : 'command mode',
help_index : 'aa',
handler : function (env) {
env.notebook.command_mode();
}
},
'split-cell-at-cursor': {
help : 'split cell',
help_index : 'ea',
handler : function (env) {
env.notebook.split_cell();
}
},
'enter-edit-mode' : {
help_index : 'aa',
handler : function (env) {
env.notebook.edit_mode();
}
},
'select-previous-cell' : {
help_index : 'da',
handler : function (env) {
var index = env.notebook.get_selected_index();
if (index !== 0 && index !== null) {
env.notebook.select_prev();
env.notebook.focus_cell();
}
}
},
'select-next-cell' : {
help_index : 'db',
handler : function (env) {
var index = env.notebook.get_selected_index();
if (index !== (env.notebook.ncells()-1) && index !== null) {
env.notebook.select_next();
env.notebook.focus_cell();
}
}
},
'cut-selected-cell' : {
icon: 'fa-cut',
help_index : 'ee',
handler : function (env) {
env.notebook.cut_cell();
}
},
'copy-selected-cell' : {
icon: 'fa-copy',
help_index : 'ef',
handler : function (env) {
env.notebook.copy_cell();
}
},
'paste-cell-before' : {
help_index : 'eg',
handler : function (env) {
env.notebook.paste_cell_above();
}
},
'paste-cell-after' : {
icon: 'fa-paste',
help_index : 'eh',
handler : function (env) {
env.notebook.paste_cell_below();
}
},
'insert-cell-before' : {
help_index : 'ec',
handler : function (env) {
env.notebook.insert_cell_above();
env.notebook.select_prev();
env.notebook.focus_cell();
}
},
'insert-cell-after' : {
icon : 'fa-plus',
help_index : 'ed',
handler : function (env) {
env.notebook.insert_cell_below();
env.notebook.select_next();
env.notebook.focus_cell();
}
},
'change-selected-cell-to-code-cell' : {
help : 'to code',
help_index : 'ca',
handler : function (env) {
env.notebook.to_code();
}
},
'change-selected-cell-to-markdown-cell' : {
help : 'to markdown',
help_index : 'cb',
handler : function (env) {
env.notebook.to_markdown();
}
},
'change-selected-cell-to-raw-cell' : {
help : 'to raw',
help_index : 'cc',
handler : function (env) {
env.notebook.to_raw();
}
},
'change-selected-cell-to-heading-1' : {
help : 'to heading 1',
help_index : 'cd',
handler : function (env) {
env.notebook.to_heading(undefined, 1);
}
},
'change-selected-cell-to-heading-2' : {
help : 'to heading 2',
help_index : 'ce',
handler : function (env) {
env.notebook.to_heading(undefined, 2);
}
},
'change-selected-cell-to-heading-3' : {
help : 'to heading 3',
help_index : 'cf',
handler : function (env) {
env.notebook.to_heading(undefined, 3);
}
},
'change-selected-cell-to-heading-4' : {
help : 'to heading 4',
help_index : 'cg',
handler : function (env) {
env.notebook.to_heading(undefined, 4);
}
},
'change-selected-cell-to-heading-5' : {
help : 'to heading 5',
help_index : 'ch',
handler : function (env) {
env.notebook.to_heading(undefined, 5);
}
},
'change-selected-cell-to-heading-6' : {
help : 'to heading 6',
help_index : 'ci',
handler : function (env) {
env.notebook.to_heading(undefined, 6);
}
},
'toggle-output-visibility-selected-cell' : {
help : 'toggle output',
help_index : 'gb',
handler : function (env) {
env.notebook.toggle_output();
}
},
'toggle-output-scrolling-selected-cell' : {
help : 'toggle output scrolling',
help_index : 'gc',
handler : function (env) {
env.notebook.toggle_output_scroll();
}
},
'move-selected-cell-down' : {
icon: 'fa-arrow-down',
help_index : 'eb',
handler : function (env) {
env.notebook.move_cell_down();
}
},
'move-selected-cell-up' : {
icon: 'fa-arrow-up',
help_index : 'ea',
handler : function (env) {
env.notebook.move_cell_up();
}
},
'toggle-line-number-selected-cell' : {
help : 'toggle line numbers',
help_index : 'ga',
handler : function (env) {
env.notebook.cell_toggle_line_numbers();
}
},
'show-keyboard-shortcut-help-dialog' : {
help_index : 'ge',
handler : function (env) {
env.quick_help.show_keyboard_shortcuts();
}
},
'delete-cell': {
help_index : 'ej',
handler : function (env) {
env.notebook.delete_cell();
}
},
'interrupt-kernel':{
icon: 'fa-stop',
help_index : 'ha',
handler : function (env) {
env.notebook.kernel.interrupt();
}
},
'restart-kernel':{
icon: 'fa-repeat',
help_index : 'hb',
handler : function (env) {
env.notebook.restart_kernel();
}
},
'undo-last-cell-deletion' : {
help_index : 'ei',
handler : function (env) {
env.notebook.undelete_cell();
}
},
'merge-selected-cell-with-cell-after' : {
help : 'merge cell below',
help_index : 'ek',
handler : function (env) {
env.notebook.merge_cell_below();
}
},
'close-pager' : {
help_index : 'gd',
handler : function (env) {
env.pager.collapse();
}
}
};
/**
* A bunch of `Advance actions` for IPython.
* Cf `Simple Action` plus the following properties.
*
* handler: first argument of the handler is the event that triggerd the action
* (typically keypress). The handler is responsible for any modification of the
* event and event propagation.
* Is also responsible for returning false if the event have to be further ignored,
* true, to tell keyboard manager that it ignored the event.
*
* the second parameter of the handler is the environemnt passed to Simple Actions
*
**/
var custom_ignore = {
'ignore':{
handler : function () {
return true;
}
},
'move-cursor-up-or-previous-cell':{
handler : function (env, event) {
var index = env.notebook.get_selected_index();
var cell = env.notebook.get_cell(index);
var cm = env.notebook.get_selected_cell().code_mirror;
var cur = cm.getCursor();
if (cell && cell.at_top() && index !== 0 && cur.ch === 0) {
if(event){
event.preventDefault();
}
env.notebook.command_mode();
env.notebook.select_prev();
env.notebook.edit_mode();
cm = env.notebook.get_selected_cell().code_mirror;
cm.setCursor(cm.lastLine(), 0);
}
return false;
}
},
'move-cursor-down-or-next-cell':{
handler : function (env, event) {
var index = env.notebook.get_selected_index();
var cell = env.notebook.get_cell(index);
if (cell.at_bottom() && index !== (env.notebook.ncells()-1)) {
if(event){
event.preventDefault();
}
env.notebook.command_mode();
env.notebook.select_next();
env.notebook.edit_mode();
var cm = env.notebook.get_selected_cell().code_mirror;
cm.setCursor(0, 0);
}
return false;
}
},
'scroll-down': {
handler: function(env, event) {
if(event){
event.preventDefault();
}
return env.notebook.scroll_manager.scroll(1);
},
},
'scroll-up': {
handler: function(env, event) {
if(event){
event.preventDefault();
}
return env.notebook.scroll_manager.scroll(-1);
},
},
'save-notebook':{
help: "Save and Checkpoint",
help_index : 'fb',
icon: 'fa-save',
handler : function (env, event) {
env.notebook.save_checkpoint();
if(event){
event.preventDefault();
}
return false;
}
},
};
// private stuff that prepend `.ipython` to actions names
// and uniformize/fill in missing pieces in of an action.
var _prepare_handler = function(registry, subkey, source){
registry['ipython.'+subkey] = {};
registry['ipython.'+subkey].help = source[subkey].help||subkey.replace(/-/g,' ');
registry['ipython.'+subkey].help_index = source[subkey].help_index;
registry['ipython.'+subkey].icon = source[subkey].icon;
return source[subkey].handler;
};
// Will actually generate/register all the IPython actions
var fun = function(){
var final_actions = {};
for(var k in _action){
// Js closure are function level not block level need to wrap in a IIFE
// and append ipython to event name these things do intercept event so are wrapped
// in a function that return false.
var handler = _prepare_handler(final_actions, k, _action);
(function(key, handler){
final_actions['ipython.'+key].handler = function(env, event){
handler(env);
if(event){
event.preventDefault();
}
return false;
};
})(k, handler);
}
for(var k in custom_ignore){
// Js closure are function level not block level need to wrap in a IIFE
// same as above, but decide for themselves wether or not they intercept events.
var handler = _prepare_handler(final_actions, k, custom_ignore);
(function(key, handler){
final_actions['ipython.'+key].handler = function(env, event){
return handler(env, event);
};
})(k, handler);
}
return final_actions;
};
ActionHandler.prototype._actions = fun();
/**
* extend the environment variable that will be pass to handlers
**/
ActionHandler.prototype.extend_env = function(env){
for(var k in env){
this.env[k] = env[k];
}
};
ActionHandler.prototype.register = function(action, name, prefix){
/**
* Register an `action` with an optional name and prefix.
*
* if name and prefix are not given they will be determined automatically.
* if action if just a `function` it will be wrapped in an anonymous action.
*
* @return the full name to access this action .
**/
action = this.normalise(action);
if( !name ){
name = 'autogenerated-'+String(action.handler);
}
prefix = prefix || 'auto';
var full_name = prefix+'.'+name;
this._actions[full_name] = action;
return full_name;
};
ActionHandler.prototype.normalise = function(data){
/**
* given an `action` or `function`, return a normalised `action`
* by setting all known attributes and removing unknown attributes;
**/
if(typeof(data) === 'function'){
data = {handler:data};
}
if(typeof(data.handler) !== 'function'){
throw('unknown datatype, cannot register');
}
var _data = data;
data = {};
data.handler = _data.handler;
data.help = data.help || '';
data.icon = data.icon || '';
data.help_index = data.help_index || '';
return data;
};
ActionHandler.prototype.get_name = function(name_or_data){
/**
* given an `action` or `name` of a action, return the name attached to this action.
* if given the name of and corresponding actions does not exist in registry, return `null`.
**/
if(typeof(name_or_data) === 'string'){
if(this.exists(name_or_data)){
return name_or_data;
} else {
return null;
}
} else {
return this.register(name_or_data);
}
};
ActionHandler.prototype.get = function(name){
return this._actions[name];
};
ActionHandler.prototype.call = function(name, event, env){
return this._actions[name].handler(env|| this.env, event);
};
ActionHandler.prototype.exists = function(name){
return (typeof(this._actions[name]) !== 'undefined');
};
return {init:ActionHandler};
});

@ -1,44 +1,39 @@
// Copyright (c) IPython Development Team.
// Distributed under the terms of the Modified BSD License.
/**
*
*
* @module cell
* @namespace cell
* @class Cell
*/
define([
'base/js/namespace',
'jquery',
'base/js/utils',
], function(IPython, $, utils) {
'codemirror/lib/codemirror',
'codemirror/addon/edit/matchbrackets',
'codemirror/addon/edit/closebrackets',
'codemirror/addon/comment/comment'
], function(IPython, $, utils, CodeMirror, cm_match, cm_closeb, cm_comment) {
// TODO: remove IPython dependency here
"use strict";
// monkey patch CM to be able to syntax highlight cell magics
// bug reported upstream,
// see https://github.com/codemirror/CodeMirror/issues/670
if(CodeMirror.getMode(1,'text/plain').indent === undefined ){
CodeMirror.modes.null = function() {
return {token: function(stream) {stream.skipToEnd();},indent : function(){return 0;}};
};
}
CodeMirror.patchedGetMode = function(config, mode){
var cmmode = CodeMirror.getMode(config, mode);
if(cmmode.indent === null) {
console.log('patch mode "' , mode, '" on the fly');
cmmode.indent = function(){return 0;};
}
return cmmode;
};
// end monkey patching CodeMirror
var Cell = function (options) {
// Constructor
//
// The Base `Cell` class from which to inherit.
//
// Parameters:
// options: dictionary
// Dictionary of keyword arguments.
// events: $(Events) instance
// config: dictionary
// keyboard_manager: KeyboardManager instance
/* Constructor
*
* The Base `Cell` class from which to inherit.
* @constructor
* @param:
* options: dictionary
* Dictionary of keyword arguments.
* events: $(Events) instance
* config: dictionary
* keyboard_manager: KeyboardManager instance
*/
options = options || {};
this.keyboard_manager = options.keyboard_manager;
this.events = options.events;
@ -50,7 +45,20 @@ define([
this.selected = false;
this.rendered = false;
this.mode = 'command';
this.metadata = {};
// Metadata property
var that = this;
this._metadata = {};
Object.defineProperty(this, 'metadata', {
get: function() { return that._metadata; },
set: function(value) {
that._metadata = value;
if (that.celltoolbar) {
that.celltoolbar.rebuild();
}
}
});
// load this from metadata later ?
this.user_highlight = 'auto';
this.cm_config = config.cm_config;
@ -104,8 +112,10 @@ define([
};
Cell.prototype.init_classes = function () {
// Call after this.element exists to initialize the css classes
// related to selected, rendered and mode.
/**
* Call after this.element exists to initialize the css classes
* related to selected, rendered and mode.
*/
if (this.selected) {
this.element.addClass('selected');
} else {
@ -157,6 +167,16 @@ define([
that.events.trigger('command_mode.Cell', {cell: that});
});
}
this.element.dblclick(function () {
if (that.selected === false) {
this.events.trigger('select.Cell', {'cell':that});
}
var cont = that.unrender();
if (cont) {
that.focus_editor();
}
});
};
/**
@ -174,9 +194,22 @@ define([
Cell.prototype.handle_codemirror_keyevent = function (editor, event) {
var shortcuts = this.keyboard_manager.edit_shortcuts;
var cur = editor.getCursor();
if((cur.line !== 0 || cur.ch !==0) && event.keyCode === 38){
event._ipkmIgnore = true;
}
var nLastLine = editor.lastLine();
if ((event.keyCode === 40) &&
((cur.line !== nLastLine) ||
(cur.ch !== editor.getLineHandle(nLastLine).text.length))
) {
event._ipkmIgnore = true;
}
// if this is an edit_shortcuts shortcut, the global keyboard/shortcut
// manager will handle it
if (shortcuts.handles(event)) { return true; }
if (shortcuts.handles(event)) {
return true;
}
return false;
};
@ -225,6 +258,14 @@ define([
}
};
/**
* should be overritten by subclass
* @method execute
*/
Cell.prototype.execute = function () {
return;
};
/**
* handle cell level logic when a cell is rendered
* @method render
@ -267,9 +308,6 @@ define([
* @return {Boolean} `true` if CodeMirror should ignore the event, `false` Otherwise
*/
Cell.prototype.handle_keyevent = function (editor, event) {
// console.log('CM', this.mode, event.which, event.type)
if (this.mode === 'command') {
return true;
} else if (this.mode === 'edit') {
@ -360,7 +398,9 @@ define([
* @method refresh
*/
Cell.prototype.refresh = function () {
this.code_mirror.refresh();
if (this.code_mirror) {
this.code_mirror.refresh();
}
};
/**
@ -385,12 +425,12 @@ define([
**/
Cell.prototype.toJSON = function () {
var data = {};
data.metadata = this.metadata;
// deepcopy the metadata so copied cells don't share the same object
data.metadata = JSON.parse(JSON.stringify(this.metadata));
data.cell_type = this.cell_type;
return data;
};
/**
* should be overritten by subclass
* @method fromJSON
@ -399,27 +439,39 @@ define([
if (data.metadata !== undefined) {
this.metadata = data.metadata;
}
this.celltoolbar.rebuild();
};
/**
* can the cell be split into two cells
* can the cell be split into two cells (false if not deletable)
* @method is_splittable
**/
Cell.prototype.is_splittable = function () {
return true;
return this.is_deletable();
};
/**
* can the cell be merged with other cells
* can the cell be merged with other cells (false if not deletable)
* @method is_mergeable
**/
Cell.prototype.is_mergeable = function () {
return true;
return this.is_deletable();
};
/**
* is the cell deletable? only false (undeletable) if
* metadata.deletable is explicitly false -- everything else
* counts as true
*
* @method is_deletable
**/
Cell.prototype.is_deletable = function () {
if (this.metadata.deletable === false) {
return false;
}
return true;
};
/**
* @return {String} - the text before the cursor
@ -484,7 +536,10 @@ define([
* @param {String|object|undefined} - CodeMirror mode | 'auto'
**/
Cell.prototype._auto_highlight = function (modes) {
//Here we handle manually selected modes
/**
*Here we handle manually selected modes
*/
var that = this;
var mode;
if( this.user_highlight !== undefined && this.user_highlight != 'auto' )
{
@ -506,33 +561,34 @@ define([
return;
}
if (mode.search('magic_') !== 0) {
this.code_mirror.setOption('mode', mode);
CodeMirror.autoLoadMode(this.code_mirror, mode);
utils.requireCodeMirrorMode(mode, function () {
that.code_mirror.setOption('mode', mode);
});
return;
}
var open = modes[mode].open || "%%";
var close = modes[mode].close || "%%end";
var mmode = mode;
mode = mmode.substr(6);
if(current_mode == mode){
var magic_mode = mode;
mode = magic_mode.substr(6);
if(current_mode == magic_mode){
return;
}
CodeMirror.autoLoadMode(this.code_mirror, mode);
// create on the fly a mode that swhitch between
// plain/text and smth else otherwise `%%` is
// source of some highlight issues.
// we use patchedGetMode to circumvent a bug in CM
CodeMirror.defineMode(mmode , function(config) {
return CodeMirror.multiplexingMode(
CodeMirror.patchedGetMode(config, 'text/plain'),
// always set someting on close
{open: open, close: close,
mode: CodeMirror.patchedGetMode(config, mode),
delimStyle: "delimit"
}
);
utils.requireCodeMirrorMode(mode, function () {
// create on the fly a mode that switch between
// plain/text and something else, otherwise `%%` is
// source of some highlight issues.
CodeMirror.defineMode(magic_mode, function(config) {
return CodeMirror.multiplexingMode(
CodeMirror.getMode(config, 'text/plain'),
// always set something on close
{open: open, close: close,
mode: CodeMirror.getMode(config, mode),
delimStyle: "delimit"
}
);
});
that.code_mirror.setOption('mode', magic_mode);
});
this.code_mirror.setOption('mode', mmode);
return;
}
}
@ -550,8 +606,76 @@ define([
this.code_mirror.setOption('mode', default_mode);
};
var UnrecognizedCell = function (options) {
/** Constructor for unrecognized cells */
Cell.apply(this, arguments);
this.cell_type = 'unrecognized';
this.celltoolbar = null;
this.data = {};
Object.seal(this);
};
UnrecognizedCell.prototype = Object.create(Cell.prototype);
// cannot merge or split unrecognized cells
UnrecognizedCell.prototype.is_mergeable = function () {
return false;
};
UnrecognizedCell.prototype.is_splittable = function () {
return false;
};
UnrecognizedCell.prototype.toJSON = function () {
/**
* deepcopy the metadata so copied cells don't share the same object
*/
return JSON.parse(JSON.stringify(this.data));
};
UnrecognizedCell.prototype.fromJSON = function (data) {
this.data = data;
if (data.metadata !== undefined) {
this.metadata = data.metadata;
} else {
data.metadata = this.metadata;
}
this.element.find('.inner_cell').find("a").text("Unrecognized cell type: " + data.cell_type);
};
UnrecognizedCell.prototype.create_element = function () {
Cell.prototype.create_element.apply(this, arguments);
var cell = this.element = $("<div>").addClass('cell unrecognized_cell');
cell.attr('tabindex','2');
var prompt = $('<div/>').addClass('prompt input_prompt');
cell.append(prompt);
var inner_cell = $('<div/>').addClass('inner_cell');
inner_cell.append(
$("<a>")
.attr("href", "#")
.text("Unrecognized cell type")
);
cell.append(inner_cell);
this.element = cell;
};
UnrecognizedCell.prototype.bind_events = function () {
Cell.prototype.bind_events.apply(this, arguments);
var cell = this;
this.element.find('.inner_cell').find("a").click(function () {
cell.events.trigger('unrecognized_cell.Cell', {cell: cell})
});
};
// Backwards compatibility.
IPython.Cell = Cell;
return {'Cell': Cell};
return {
Cell: Cell,
UnrecognizedCell: UnrecognizedCell
};
});

@ -9,17 +9,19 @@ define([
"use strict";
var CellToolbar = function (options) {
// Constructor
//
// Parameters:
// options: dictionary
// Dictionary of keyword arguments.
// events: $(Events) instance
// cell: Cell instance
// notebook: Notebook instance
//
// TODO: This leaks, when cell are deleted
// There is still a reference to each celltoolbars.
/**
* Constructor
*
* Parameters:
* options: dictionary
* Dictionary of keyword arguments.
* events: $(Events) instance
* cell: Cell instance
* notebook: Notebook instance
*
* TODO: This leaks, when cell are deleted
* There is still a reference to each celltoolbars.
*/
CellToolbar._instances.push(this);
this.notebook = options.notebook;
this.cell = options.cell;
@ -114,7 +116,7 @@ define([
* @param name {String} name to use to refer to the callback. It is advised to use a prefix with the name
* for easier sorting and avoid collision
* @param callback {function(div, cell)} callback that will be called to generate the ui element
* @param [cell_types] {List of String|undefined} optional list of cell types. If present the UI element
* @param [cell_types] {List_of_String|undefined} optional list of cell types. If present the UI element
* will be added only to cells of types in the list.
*
*
@ -163,7 +165,7 @@ define([
* @method register_preset
* @param name {String} name to use to refer to the preset. It is advised to use a prefix with the name
* for easier sorting and avoid collision
* @param preset_list {List of String} reverse order of the button in the toolbar. Each String of the list
* @param preset_list {List_of_String} reverse order of the button in the toolbar. Each String of the list
* should correspond to a name of a registerd callback.
*
* @private
@ -248,9 +250,11 @@ define([
* @method rebuild
*/
CellToolbar.prototype.rebuild = function(){
// strip evrything from the div
// which is probably inner_element
// or this.element.
/**
* strip evrything from the div
* which is probably inner_element
* or this.element.
*/
this.inner_element.empty();
this.ui_controls_list = [];
@ -288,8 +292,6 @@ define([
};
/**
*/
CellToolbar.utils = {};
@ -385,7 +387,7 @@ define([
* @method utils.select_ui_generator
* @static
*
* @param list_list {list of sublist} List of sublist of metadata value and name in the dropdown list.
* @param list_list {list_of_sublist} List of sublist of metadata value and name in the dropdown list.
* subslit shoud contain 2 element each, first a string that woul be displayed in the dropdown list,
* and second the corresponding value to be passed to setter/return by getter. the corresponding value
* should not be "undefined" or behavior can be unexpected.

@ -119,7 +119,9 @@ define([
width: 650,
modal: true,
close: function() {
//cleanup on close
/**
*cleanup on close
*/
$(this).remove();
}
});

@ -1,5 +1,13 @@
// Copyright (c) IPython Development Team.
// Distributed under the terms of the Modified BSD License.
/**
*
*
* @module codecell
* @namespace codecell
* @class CodeCell
*/
define([
'base/js/namespace',
@ -10,8 +18,12 @@ define([
'notebook/js/outputarea',
'notebook/js/completer',
'notebook/js/celltoolbar',
], function(IPython, $, utils, keyboard, cell, outputarea, completer, celltoolbar) {
'codemirror/lib/codemirror',
'codemirror/mode/python/python',
'notebook/js/codemirror-ipython'
], function(IPython, $, utils, keyboard, cell, outputarea, completer, celltoolbar, CodeMirror, cmpython, cmip) {
"use strict";
var Cell = cell.Cell;
/* local util for codemirror */
@ -41,21 +53,23 @@ define([
var keycodes = keyboard.keycodes;
var CodeCell = function (kernel, options) {
// Constructor
//
// A Cell conceived to write code.
//
// Parameters:
// kernel: Kernel instance
// The kernel doesn't have to be set at creation time, in that case
// it will be null and set_kernel has to be called later.
// options: dictionary
// Dictionary of keyword arguments.
// events: $(Events) instance
// config: dictionary
// keyboard_manager: KeyboardManager instance
// notebook: Notebook instance
// tooltip: Tooltip instance
/**
* Constructor
*
* A Cell conceived to write code.
*
* Parameters:
* kernel: Kernel instance
* The kernel doesn't have to be set at creation time, in that case
* it will be null and set_kernel has to be called later.
* options: dictionary
* Dictionary of keyword arguments.
* events: $(Events) instance
* config: dictionary
* keyboard_manager: KeyboardManager instance
* notebook: Notebook instance
* tooltip: Tooltip instance
*/
this.kernel = kernel || null;
this.notebook = options.notebook;
this.collapsed = false;
@ -68,15 +82,28 @@ define([
this.input_prompt_number = null;
this.celltoolbar = null;
this.output_area = null;
// Keep a stack of the 'active' output areas (where active means the
// output area that recieves output). When a user activates an output
// area, it gets pushed to the stack. Then, when the output area is
// deactivated, it's popped from the stack. When the stack is empty,
// the cell's output area is used.
this.active_output_areas = [];
var that = this;
Object.defineProperty(this, 'active_output_area', {
get: function() {
if (that.active_output_areas && that.active_output_areas.length > 0) {
return that.active_output_areas[that.active_output_areas.length-1];
} else {
return that.output_area;
}
},
});
this.last_msg_id = null;
this.completer = null;
var cm_overwrite_options = {
onKeyEvent: $.proxy(this.handle_keyevent,this)
};
var config = utils.mergeopt(CodeCell, this.config, {cm_config: cm_overwrite_options});
var config = utils.mergeopt(CodeCell, this.config);
Cell.apply(this,[{
config: config,
keyboard_manager: options.keyboard_manager,
@ -84,8 +111,6 @@ define([
// Attributes we want to override in this subclass.
this.cell_type = "code";
var that = this;
this.element.focusout(
function() { that.auto_highlight(); }
);
@ -102,15 +127,30 @@ define([
},
mode: 'ipython',
theme: 'ipython',
matchBrackets: true,
// don't auto-close strings because of CodeMirror #2385
autoCloseBrackets: "()[]{}"
matchBrackets: true
}
};
CodeCell.msg_cells = {};
CodeCell.prototype = new Cell();
CodeCell.prototype = Object.create(Cell.prototype);
/**
* @method push_output_area
*/
CodeCell.prototype.push_output_area = function (output_area) {
this.active_output_areas.push(output_area);
};
/**
* @method pop_output_area
*/
CodeCell.prototype.pop_output_area = function (output_area) {
var index = this.active_output_areas.lastIndexOf(output_area);
if (index > -1) {
this.active_output_areas.splice(index, 1);
}
};
/**
* @method auto_highlight
@ -135,6 +175,7 @@ define([
inner_cell.append(this.celltoolbar.element);
var input_area = $('<div/>').addClass('input_area');
this.code_mirror = new CodeMirror(input_area.get(0), this.cm_config);
this.code_mirror.on('keydown', $.proxy(this.handle_keyevent,this))
$(this.code_mirror.getInputField()).attr("spellcheck", "false");
inner_cell.append(input_area);
input.append(prompt).append(inner_cell);
@ -187,6 +228,7 @@ define([
* true = ignore, false = don't ignore.
* @method handle_codemirror_keyevent
*/
CodeCell.prototype.handle_codemirror_keyevent = function (editor, event) {
var that = this;
@ -220,10 +262,11 @@ define([
}
// If we closed the tooltip, don't let CM or the global handlers
// handle this event.
event.stop();
event.codemirrorIgnore = true;
event.preventDefault();
return true;
} else if (event.keyCode === keycodes.tab && event.type === 'keydown' && event.shiftKey) {
if (editor.somethingSelected()){
if (editor.somethingSelected() || editor.getSelections().length !== 1){
var anchor = editor.getCursor("anchor");
var head = editor.getCursor("head");
if( anchor.line != head.line){
@ -231,12 +274,15 @@ define([
}
}
this.tooltip.request(that);
event.stop();
event.codemirrorIgnore = true;
event.preventDefault();
return true;
} else if (event.keyCode === keycodes.tab && event.type == 'keydown') {
// Tab completion.
this.tooltip.remove_and_cancel_tooltip();
if (editor.somethingSelected()) {
// completion does not work on multicursor, it might be possible though in some cases
if (editor.somethingSelected() || editor.getSelections().length > 1) {
return false;
}
var pre_cursor = editor.getRange({line:cur.line,ch:0},cur);
@ -245,7 +291,8 @@ define([
// is empty. In this case, let CodeMirror handle indentation.
return false;
} else {
event.stop();
event.codemirrorIgnore = true;
event.preventDefault();
this.completer.startCompletion();
return true;
}
@ -267,7 +314,12 @@ define([
* @method execute
*/
CodeCell.prototype.execute = function () {
this.output_area.clear_output();
if (!this.kernel || !this.kernel.is_connected()) {
console.log("Can't execute, kernel is not connected.");
return;
}
this.active_output_area.clear_output();
// Clear widget area
this.widget_subarea.html('');
@ -288,6 +340,8 @@ define([
delete CodeCell.msg_cells[old_msg_id];
}
CodeCell.msg_cells[this.last_msg_id] = this;
this.render();
this.events.trigger('execute.CodeCell', {cell: this});
};
/**
@ -295,6 +349,7 @@ define([
* @method get_callbacks
*/
CodeCell.prototype.get_callbacks = function () {
var that = this;
return {
shell : {
reply : $.proxy(this._handle_execute_reply, this),
@ -304,8 +359,12 @@ define([
}
},
iopub : {
output : $.proxy(this.output_area.handle_output, this.output_area),
clear_output : $.proxy(this.output_area.handle_clear_output, this.output_area),
output : function() {
that.active_output_area.handle_output.apply(that.active_output_area, arguments);
},
clear_output : function() {
that.active_output_area.handle_clear_output.apply(that.active_output_area, arguments);
},
},
input : $.proxy(this._handle_input_request, this)
};
@ -339,7 +398,7 @@ define([
* @private
*/
CodeCell.prototype._handle_input_request = function (msg) {
this.output_area.append_raw_input(msg);
this.active_output_area.append_raw_input(msg);
};
@ -360,11 +419,6 @@ define([
return cont;
};
CodeCell.prototype.unrender = function () {
// CodeCell is always rendered
return false;
};
CodeCell.prototype.select_all = function () {
var start = {line: 0, ch: 0};
var nlines = this.code_mirror.lineCount();
@ -375,13 +429,11 @@ define([
CodeCell.prototype.collapse_output = function () {
this.collapsed = true;
this.output_area.collapse();
};
CodeCell.prototype.expand_output = function () {
this.collapsed = false;
this.output_area.expand();
this.output_area.unscroll_area();
};
@ -392,7 +444,6 @@ define([
};
CodeCell.prototype.toggle_output = function () {
this.collapsed = Boolean(1 - this.collapsed);
this.output_area.toggle_output();
};
@ -403,7 +454,7 @@ define([
CodeCell.input_prompt_classical = function (prompt_value, lines_number) {
var ns;
if (prompt_value === undefined) {
if (prompt_value === undefined || prompt_value === null) {
ns = "&nbsp;";
} else {
ns = encodeURIComponent(prompt_value);
@ -450,7 +501,7 @@ define([
CodeCell.prototype.clear_output = function (wait) {
this.output_area.clear_output(wait);
this.active_output_area.clear_output(wait);
this.set_input_prompt();
};
@ -460,22 +511,18 @@ define([
CodeCell.prototype.fromJSON = function (data) {
Cell.prototype.fromJSON.apply(this, arguments);
if (data.cell_type === 'code') {
if (data.input !== undefined) {
this.set_text(data.input);
if (data.source !== undefined) {
this.set_text(data.source);
// make this value the starting point, so that we can only undo
// to this state, instead of a blank cell
this.code_mirror.clearHistory();
this.auto_highlight();
}
if (data.prompt_number !== undefined) {
this.set_input_prompt(data.prompt_number);
} else {
this.set_input_prompt();
}
this.output_area.trusted = data.trusted || false;
this.set_input_prompt(data.execution_count);
this.output_area.trusted = data.metadata.trusted || false;
this.output_area.fromJSON(data.outputs);
if (data.collapsed !== undefined) {
if (data.collapsed) {
if (data.metadata.collapsed !== undefined) {
if (data.metadata.collapsed) {
this.collapse_output();
} else {
this.expand_output();
@ -487,16 +534,17 @@ define([
CodeCell.prototype.toJSON = function () {
var data = Cell.prototype.toJSON.apply(this);
data.input = this.get_text();
data.source = this.get_text();
// is finite protect against undefined and '*' value
if (isFinite(this.input_prompt_number)) {
data.prompt_number = this.input_prompt_number;
data.execution_count = this.input_prompt_number;
} else {
data.execution_count = null;
}
var outputs = this.output_area.toJSON();
data.outputs = outputs;
data.language = 'python';
data.trusted = this.output_area.trusted;
data.collapsed = this.collapsed;
data.metadata.trusted = this.output_area.trusted;
data.metadata.collapsed = this.output_area.collapsed;
return data;
};

@ -3,7 +3,18 @@
// callback to auto-load python mode, which is more likely not the best things
// to do, but at least the simple one for now.
CodeMirror.requireMode('python',function(){
(function(mod) {
if (typeof exports == "object" && typeof module == "object"){ // CommonJS
mod(require("codemirror/lib/codemirror"),
require("codemirror/mode/python/python")
);
} else if (typeof define == "function" && define.amd){ // AMD
define(["codemirror/lib/codemirror",
"codemirror/mode/python/python"], mod);
} else {// Plain browser env
mod(CodeMirror);
}
})(function(CodeMirror) {
"use strict";
CodeMirror.defineMode("ipython", function(conf, parserConf) {

@ -1,44 +1,62 @@
// IPython GFM (GitHub Flavored Markdown) mode is just a slightly altered GFM
// Mode with support for latex.
// IPython GFM (GitHub Flavored Markdown) mode is just a slightly altered GFM
// Mode with support for latex.
//
// Latex support was supported by Codemirror GFM as of
// Latex support was supported by Codemirror GFM as of
// https://github.com/codemirror/CodeMirror/pull/567
// But was later removed in
// https://github.com/codemirror/CodeMirror/commit/d9c9f1b1ffe984aee41307f3e927f80d1f23590c
CodeMirror.requireMode('gfm', function(){
CodeMirror.requireMode('stex', function(){
CodeMirror.defineMode("ipythongfm", function(config, parserConfig) {
var gfm_mode = CodeMirror.getMode(config, "gfm");
var tex_mode = CodeMirror.getMode(config, "stex");
return CodeMirror.multiplexingMode(
gfm_mode,
{
open: "$", close: "$",
mode: tex_mode,
delimStyle: "delimit"
},
{
open: "$$", close: "$$",
mode: tex_mode,
delimStyle: "delimit"
},
{
open: "\\(", close: "\\)",
mode: tex_mode,
delimStyle: "delimit"
},
{
open: "\\[", close: "\\]",
mode: tex_mode,
delimStyle: "delimit"
}
// .. more multiplexed styles can follow here
);
}, 'gfm');
CodeMirror.defineMIME("text/x-ipythongfm", "ipythongfm");
});
});
(function(mod) {
if (typeof exports == "object" && typeof module == "object"){ // CommonJS
mod(require("codemirror/lib/codemirror")
,require("codemirror/addon/mode/multiplex")
,require("codemirror/mode/gfm/gfm")
,require("codemirror/mode/stex/stex")
);
} else if (typeof define == "function" && define.amd){ // AMD
define(["codemirror/lib/codemirror"
,"codemirror/addon/mode/multiplex"
,"codemirror/mode/python/python"
,"codemirror/mode/stex/stex"
], mod);
} else {// Plain browser env
mod(CodeMirror);
}
})( function(CodeMirror){
"use strict";
CodeMirror.defineMode("ipythongfm", function(config, parserConfig) {
var gfm_mode = CodeMirror.getMode(config, "gfm");
var tex_mode = CodeMirror.getMode(config, "stex");
return CodeMirror.multiplexingMode(
gfm_mode,
{
open: "$", close: "$",
mode: tex_mode,
delimStyle: "delimit"
},
{
// not sure this works as $$ is interpreted at (opening $, closing $, as defined just above)
open: "$$", close: "$$",
mode: tex_mode,
delimStyle: "delimit"
},
{
open: "\\(", close: "\\)",
mode: tex_mode,
delimStyle: "delimit"
},
{
open: "\\[", close: "\\]",
mode: tex_mode,
delimStyle: "delimit"
}
// .. more multiplexed styles can follow here
);
}, 'gfm');
CodeMirror.defineMIME("text/x-ipythongfm", "ipythongfm");
})

@ -7,7 +7,8 @@ define([
'base/js/utils',
'base/js/keyboard',
'notebook/js/contexthint',
], function(IPython, $, utils, keyboard) {
'codemirror/lib/codemirror',
], function(IPython, $, utils, keyboard, CodeMirror) {
"use strict";
// easier key mapping
@ -82,18 +83,20 @@ define([
this.cell = cell;
this.editor = cell.code_mirror;
var that = this;
events.on('status_busy.Kernel', function () {
events.on('kernel_busy.Kernel', function () {
that.skip_kernel_completion = true;
});
events.on('status_idle.Kernel', function () {
events.on('kernel_idle.Kernel', function () {
that.skip_kernel_completion = false;
});
};
Completer.prototype.startCompletion = function () {
// call for a 'first' completion, that will set the editor and do some
// special behavior like autopicking if only one completion available.
if (this.editor.somethingSelected()) return;
/**
* call for a 'first' completion, that will set the editor and do some
* special behavior like autopicking if only one completion available.
*/
if (this.editor.somethingSelected()|| this.editor.getSelections().length > 1) return;
this.done = false;
// use to get focus back on opera
this.carry_on_completion(true);
@ -118,9 +121,11 @@ define([
* shared start
**/
Completer.prototype.carry_on_completion = function (first_invocation) {
// Pass true as parameter if you want the completer to autopick when
// only one completion. This function is automatically reinvoked at
// each keystroke with first_invocation = false
/**
* Pass true as parameter if you want the completer to autopick when
* only one completion. This function is automatically reinvoked at
* each keystroke with first_invocation = false
*/
var cur = this.editor.getCursor();
var line = this.editor.getLine(cur.line);
var pre_cursor = this.editor.getRange({
@ -142,7 +147,7 @@ define([
}
// We want a single cursor position.
if (this.editor.somethingSelected()) {
if (this.editor.somethingSelected()|| this.editor.getSelections().length > 1) {
return;
}
@ -163,8 +168,10 @@ define([
};
Completer.prototype.finish_completing = function (msg) {
// let's build a function that wrap all that stuff into what is needed
// for the new completer:
/**
* let's build a function that wrap all that stuff into what is needed
* for the new completer:
*/
var content = msg.content;
var start = content.cursor_start;
var end = content.cursor_end;
@ -316,11 +323,15 @@ define([
// Enter
if (code == keycodes.enter) {
CodeMirror.e_stop(event);
event.codemirrorIgnore = true;
event._ipkmIgnore = true;
event.preventDefault();
this.pick();
// Escape or backspace
} else if (code == keycodes.esc || code == keycodes.backspace) {
CodeMirror.e_stop(event);
event.codemirrorIgnore = true;
event._ipkmIgnore = true;
event.preventDefault();
this.close();
} else if (code == keycodes.tab) {
//all the fastforwarding operation,
@ -339,7 +350,9 @@ define([
} else if (code == keycodes.up || code == keycodes.down) {
// need to do that to be able to move the arrow
// when on the first or last line ofo a code cell
CodeMirror.e_stop(event);
event.codemirrorIgnore = true;
event._ipkmIgnore = true;
event.preventDefault();
var options = this.sel.find('option');
var index = this.sel[0].selectedIndex;
@ -352,7 +365,7 @@ define([
index = Math.min(Math.max(index, 0), options.length-1);
this.sel[0].selectedIndex = index;
} else if (code == keycodes.pageup || code == keycodes.pagedown) {
CodeMirror.e_stop(event);
event._ipkmIgnore = true;
var options = this.sel.find('option');
var index = this.sel[0].selectedIndex;
@ -369,11 +382,13 @@ define([
};
Completer.prototype.keypress = function (event) {
// FIXME: This is a band-aid.
// on keypress, trigger insertion of a single character.
// This simulates the old behavior of completion as you type,
// before events were disconnected and CodeMirror stopped
// receiving events while the completer is focused.
/**
* FIXME: This is a band-aid.
* on keypress, trigger insertion of a single character.
* This simulates the old behavior of completion as you type,
* before events were disconnected and CodeMirror stopped
* receiving events while the completer is focused.
*/
var that = this;
var code = event.keyCode;

@ -2,7 +2,7 @@
// Distributed under the terms of the Modified BSD License.
// highly adapted for codemiror jshint
define([], function() {
define(['codemirror/lib/codemirror'], function(CodeMirror) {
"use strict";
var forEach = function(arr, f) {

@ -12,7 +12,7 @@ define([
this.selector = selector;
this.notebook = notebook;
this.events = notebook.events;
this.current_selection = notebook.default_kernel_name;
this.current_selection = null;
this.kernelspecs = {};
if (this.selector !== undefined) {
this.element = $(selector);
@ -76,12 +76,12 @@ define([
that.element.find("#current_kernel_spec").find('.kernel_name').text(data.display_name);
});
this.events.on('started.Session', function(events, session) {
if (session.kernel_name !== that.current_selection) {
this.events.on('kernel_created.Session', function(event, data) {
if (data.kernel.name !== that.current_selection) {
// If we created a 'python' session, we only know if it's Python
// 3 or 2 on the server's reply, so we fire the event again to
// set things up.
var ks = that.kernelspecs[session.kernel_name];
var ks = that.kernelspecs[data.kernel.name];
that.events.trigger('spec_changed.Kernel', ks);
}
});

@ -1,5 +1,12 @@
// Copyright (c) IPython Development Team.
// Distributed under the terms of the Modified BSD License.
/**
*
*
* @module keyboardmanager
* @namespace keyboardmanager
* @class KeyboardManager
*/
define([
'base/js/namespace',
@ -9,491 +16,138 @@ define([
], function(IPython, $, utils, keyboard) {
"use strict";
var browser = utils.browser[0];
var platform = utils.platform;
// Main keyboard manager for the notebook
var keycodes = keyboard.keycodes;
var KeyboardManager = function (options) {
// Constructor
//
// Parameters:
// options: dictionary
// Dictionary of keyword arguments.
// events: $(Events) instance
// pager: Pager instance
/**
* A class to deal with keyboard event and shortcut
*
* @class KeyboardManager
* @constructor
* @param options {dict} Dictionary of keyword arguments :
* @param options.events {$(Events)} instance
* @param options.pager: {Pager} pager instance
*/
this.mode = 'command';
this.enabled = true;
this.pager = options.pager;
this.quick_help = undefined;
this.notebook = undefined;
this.last_mode = undefined;
this.bind_events();
this.command_shortcuts = new keyboard.ShortcutManager(undefined, options.events);
this.env = {pager:this.pager};
this.actions = options.actions;
this.command_shortcuts = new keyboard.ShortcutManager(undefined, options.events, this.actions, this.env );
this.command_shortcuts.add_shortcuts(this.get_default_common_shortcuts());
this.command_shortcuts.add_shortcuts(this.get_default_command_shortcuts());
this.edit_shortcuts = new keyboard.ShortcutManager(undefined, options.events);
this.edit_shortcuts = new keyboard.ShortcutManager(undefined, options.events, this.actions, this.env);
this.edit_shortcuts.add_shortcuts(this.get_default_common_shortcuts());
this.edit_shortcuts.add_shortcuts(this.get_default_edit_shortcuts());
Object.seal(this);
};
/**
* Return a dict of common shortcut
* @method get_default_common_shortcuts
*
* @example Example of returned shortcut
* ```
* 'shortcut-key': 'action-name'
* // a string representing the shortcut as dash separated value.
* // e.g. 'shift' , 'shift-enter', 'cmd-t'
*```
*/
KeyboardManager.prototype.get_default_common_shortcuts = function() {
var that = this;
var shortcuts = {
'shift' : {
help : '',
help_index : '',
handler : function (event) {
// ignore shift keydown
return true;
}
},
'shift-enter' : {
help : 'run cell, select below',
help_index : 'ba',
handler : function (event) {
that.notebook.execute_cell_and_select_below();
return false;
}
},
'ctrl-enter' : {
help : 'run cell',
help_index : 'bb',
handler : function (event) {
that.notebook.execute_cell();
return false;
}
},
'alt-enter' : {
help : 'run cell, insert below',
help_index : 'bc',
handler : function (event) {
that.notebook.execute_cell_and_insert_below();
return false;
}
}
return {
'shift' : 'ipython.ignore',
'shift-enter' : 'ipython.run-select-next',
'ctrl-enter' : 'ipython.execute-in-place',
'alt-enter' : 'ipython.execute-and-insert-after',
// cmd on mac, ctrl otherwise
'cmdtrl-s' : 'ipython.save-notebook',
};
if (platform === 'MacOS') {
shortcuts['cmd-s'] =
{
help : 'save notebook',
help_index : 'fb',
handler : function (event) {
that.notebook.save_checkpoint();
event.preventDefault();
return false;
}
};
} else {
shortcuts['ctrl-s'] =
{
help : 'save notebook',
help_index : 'fb',
handler : function (event) {
that.notebook.save_checkpoint();
event.preventDefault();
return false;
}
};
}
return shortcuts;
};
KeyboardManager.prototype.get_default_edit_shortcuts = function() {
var that = this;
return {
'esc' : {
help : 'command mode',
help_index : 'aa',
handler : function (event) {
that.notebook.command_mode();
return false;
}
},
'ctrl-m' : {
help : 'command mode',
help_index : 'ab',
handler : function (event) {
that.notebook.command_mode();
return false;
}
},
'up' : {
help : '',
help_index : '',
handler : function (event) {
var index = that.notebook.get_selected_index();
var cell = that.notebook.get_cell(index);
if (cell && cell.at_top() && index !== 0) {
event.preventDefault();
that.notebook.command_mode();
that.notebook.select_prev();
that.notebook.edit_mode();
var cm = that.notebook.get_selected_cell().code_mirror;
cm.setCursor(cm.lastLine(), 0);
return false;
} else if (cell) {
var cm = cell.code_mirror;
cm.execCommand('goLineUp');
return false;
}
}
},
'down' : {
help : '',
help_index : '',
handler : function (event) {
var index = that.notebook.get_selected_index();
var cell = that.notebook.get_cell(index);
if (cell.at_bottom() && index !== (that.notebook.ncells()-1)) {
event.preventDefault();
that.notebook.command_mode();
that.notebook.select_next();
that.notebook.edit_mode();
var cm = that.notebook.get_selected_cell().code_mirror;
cm.setCursor(0, 0);
return false;
} else {
var cm = cell.code_mirror;
cm.execCommand('goLineDown');
return false;
}
}
},
'ctrl-shift--' : {
help : 'split cell',
help_index : 'ea',
handler : function (event) {
that.notebook.split_cell();
return false;
}
},
'ctrl-shift-subtract' : {
help : '',
help_index : 'eb',
handler : function (event) {
that.notebook.split_cell();
return false;
}
},
'esc' : 'ipython.go-to-command-mode',
'ctrl-m' : 'ipython.go-to-command-mode',
'up' : 'ipython.move-cursor-up-or-previous-cell',
'down' : 'ipython.move-cursor-down-or-next-cell',
'ctrl-shift--' : 'ipython.split-cell-at-cursor',
'ctrl-shift-subtract' : 'ipython.split-cell-at-cursor'
};
};
KeyboardManager.prototype.get_default_command_shortcuts = function() {
var that = this;
return {
'space': {
help: "Scroll down",
handler: function(event) {
return that.notebook.scroll_manager.scroll(1);
},
},
'shift-space': {
help: "Scroll up",
handler: function(event) {
return that.notebook.scroll_manager.scroll(-1);
},
},
'enter' : {
help : 'edit mode',
help_index : 'aa',
handler : function (event) {
that.notebook.edit_mode();
return false;
}
},
'up' : {
help : 'select previous cell',
help_index : 'da',
handler : function (event) {
var index = that.notebook.get_selected_index();
if (index !== 0 && index !== null) {
that.notebook.select_prev();
that.notebook.focus_cell();
}
return false;
}
},
'down' : {
help : 'select next cell',
help_index : 'db',
handler : function (event) {
var index = that.notebook.get_selected_index();
if (index !== (that.notebook.ncells()-1) && index !== null) {
that.notebook.select_next();
that.notebook.focus_cell();
}
return false;
}
},
'k' : {
help : 'select previous cell',
help_index : 'dc',
handler : function (event) {
var index = that.notebook.get_selected_index();
if (index !== 0 && index !== null) {
that.notebook.select_prev();
that.notebook.focus_cell();
}
return false;
}
},
'j' : {
help : 'select next cell',
help_index : 'dd',
handler : function (event) {
var index = that.notebook.get_selected_index();
if (index !== (that.notebook.ncells()-1) && index !== null) {
that.notebook.select_next();
that.notebook.focus_cell();
}
return false;
}
},
'x' : {
help : 'cut cell',
help_index : 'ee',
handler : function (event) {
that.notebook.cut_cell();
return false;
}
},
'c' : {
help : 'copy cell',
help_index : 'ef',
handler : function (event) {
that.notebook.copy_cell();
return false;
}
},
'shift-v' : {
help : 'paste cell above',
help_index : 'eg',
handler : function (event) {
that.notebook.paste_cell_above();
return false;
}
},
'v' : {
help : 'paste cell below',
help_index : 'eh',
handler : function (event) {
that.notebook.paste_cell_below();
return false;
}
},
'd' : {
help : 'delete cell (press twice)',
help_index : 'ej',
count: 2,
handler : function (event) {
that.notebook.delete_cell();
return false;
}
},
'a' : {
help : 'insert cell above',
help_index : 'ec',
handler : function (event) {
that.notebook.insert_cell_above();
that.notebook.select_prev();
that.notebook.focus_cell();
return false;
}
},
'b' : {
help : 'insert cell below',
help_index : 'ed',
handler : function (event) {
that.notebook.insert_cell_below();
that.notebook.select_next();
that.notebook.focus_cell();
return false;
}
},
'y' : {
help : 'to code',
help_index : 'ca',
handler : function (event) {
that.notebook.to_code();
return false;
}
},
'm' : {
help : 'to markdown',
help_index : 'cb',
handler : function (event) {
that.notebook.to_markdown();
return false;
}
},
'r' : {
help : 'to raw',
help_index : 'cc',
handler : function (event) {
that.notebook.to_raw();
return false;
}
},
'1' : {
help : 'to heading 1',
help_index : 'cd',
handler : function (event) {
that.notebook.to_heading(undefined, 1);
return false;
}
},
'2' : {
help : 'to heading 2',
help_index : 'ce',
handler : function (event) {
that.notebook.to_heading(undefined, 2);
return false;
}
},
'3' : {
help : 'to heading 3',
help_index : 'cf',
handler : function (event) {
that.notebook.to_heading(undefined, 3);
return false;
}
},
'4' : {
help : 'to heading 4',
help_index : 'cg',
handler : function (event) {
that.notebook.to_heading(undefined, 4);
return false;
}
},
'5' : {
help : 'to heading 5',
help_index : 'ch',
handler : function (event) {
that.notebook.to_heading(undefined, 5);
return false;
}
},
'6' : {
help : 'to heading 6',
help_index : 'ci',
handler : function (event) {
that.notebook.to_heading(undefined, 6);
return false;
}
},
'o' : {
help : 'toggle output',
help_index : 'gb',
handler : function (event) {
that.notebook.toggle_output();
return false;
}
},
'shift-o' : {
help : 'toggle output scrolling',
help_index : 'gc',
handler : function (event) {
that.notebook.toggle_output_scroll();
return false;
}
},
's' : {
help : 'save notebook',
help_index : 'fa',
handler : function (event) {
that.notebook.save_checkpoint();
return false;
}
},
'ctrl-j' : {
help : 'move cell down',
help_index : 'eb',
handler : function (event) {
that.notebook.move_cell_down();
return false;
}
},
'ctrl-k' : {
help : 'move cell up',
help_index : 'ea',
handler : function (event) {
that.notebook.move_cell_up();
return false;
}
},
'l' : {
help : 'toggle line numbers',
help_index : 'ga',
handler : function (event) {
that.notebook.cell_toggle_line_numbers();
return false;
}
},
'i' : {
help : 'interrupt kernel (press twice)',
help_index : 'ha',
count: 2,
handler : function (event) {
that.notebook.kernel.interrupt();
return false;
}
},
'0' : {
help : 'restart kernel (press twice)',
help_index : 'hb',
count: 2,
handler : function (event) {
that.notebook.restart_kernel();
return false;
}
},
'h' : {
help : 'keyboard shortcuts',
help_index : 'ge',
handler : function (event) {
that.quick_help.show_keyboard_shortcuts();
return false;
}
},
'z' : {
help : 'undo last delete',
help_index : 'ei',
handler : function (event) {
that.notebook.undelete_cell();
return false;
}
},
'shift-m' : {
help : 'merge cell below',
help_index : 'ek',
handler : function (event) {
that.notebook.merge_cell_below();
return false;
}
},
'q' : {
help : 'close pager',
help_index : 'gd',
handler : function (event) {
that.pager.collapse();
return false;
}
},
'shift-space': 'ipython.scroll-up',
'shift-v' : 'ipython.paste-cell-before',
'shift-m' : 'ipython.merge-selected-cell-with-cell-after',
'shift-o' : 'ipython.toggle-output-scrolling-selected-cell',
'ctrl-j' : 'ipython.move-selected-cell-down',
'ctrl-k' : 'ipython.move-selected-cell-up',
'enter' : 'ipython.enter-edit-mode',
'space' : 'ipython.scroll-down',
'down' : 'ipython.select-next-cell',
'i,i' : 'ipython.interrupt-kernel',
'0,0' : 'ipython.restart-kernel',
'd,d' : 'ipython.delete-cell',
'esc': 'ipython.close-pager',
'up' : 'ipython.select-previous-cell',
'k' : 'ipython.select-previous-cell',
'j' : 'ipython.select-next-cell',
'x' : 'ipython.cut-selected-cell',
'c' : 'ipython.copy-selected-cell',
'v' : 'ipython.paste-cell-after',
'a' : 'ipython.insert-cell-before',
'b' : 'ipython.insert-cell-after',
'y' : 'ipython.change-selected-cell-to-code-cell',
'm' : 'ipython.change-selected-cell-to-markdown-cell',
'r' : 'ipython.change-selected-cell-to-raw-cell',
'1' : 'ipython.change-selected-cell-to-heading-1',
'2' : 'ipython.change-selected-cell-to-heading-2',
'3' : 'ipython.change-selected-cell-to-heading-3',
'4' : 'ipython.change-selected-cell-to-heading-4',
'5' : 'ipython.change-selected-cell-to-heading-5',
'6' : 'ipython.change-selected-cell-to-heading-6',
'o' : 'ipython.toggle-output-visibility-selected-cell',
's' : 'ipython.save-notebook',
'l' : 'ipython.toggle-line-number-selected-cell',
'h' : 'ipython.show-keyboard-shortcut-help-dialog',
'z' : 'ipython.undo-last-cell-deletion',
'q' : 'ipython.close-pager',
};
};
KeyboardManager.prototype.bind_events = function () {
var that = this;
$(document).keydown(function (event) {
if(event._ipkmIgnore===true||(event.originalEvent||{})._ipkmIgnore===true){
return false;
}
return that.handle_keydown(event);
});
};
KeyboardManager.prototype.set_notebook = function (notebook) {
this.notebook = notebook;
this.actions.extend_env({notebook:notebook});
};
KeyboardManager.prototype.set_quickhelp = function (notebook) {
this.actions.extend_env({quick_help:notebook});
};
KeyboardManager.prototype.handle_keydown = function (event) {
var notebook = this.notebook;
/**
* returning false from this will stop event propagation
**/
if (event.which === keycodes.esc) {
// Intercept escape at highest level to avoid closing
@ -503,8 +157,7 @@ define([
if (!this.enabled) {
if (event.which === keycodes.esc) {
// ESC
notebook.command_mode();
this.notebook.command_mode();
return false;
}
return true;
@ -571,7 +224,8 @@ define([
});
};
// For backwards compatability.
// For backwards compatibility.
IPython.KeyboardManager = KeyboardManager;
return {'KeyboardManager': KeyboardManager};

@ -5,6 +5,8 @@ require([
'base/js/namespace',
'jquery',
'notebook/js/notebook',
'contents',
'services/config',
'base/js/utils',
'base/js/page',
'notebook/js/layoutmanager',
@ -16,15 +18,20 @@ require([
'notebook/js/menubar',
'notebook/js/notificationarea',
'notebook/js/savewidget',
'notebook/js/actions',
'notebook/js/keyboardmanager',
'notebook/js/config',
'notebook/js/kernelselector',
// only loaded, not used:
'custom/custom',
'codemirror/lib/codemirror',
'notebook/js/about',
// only loaded, not used, please keep sure this is loaded last
'custom/custom'
], function(
IPython,
$,
notebook,
contents,
configmod,
utils,
page,
layoutmanager,
@ -35,16 +42,24 @@ require([
quickhelp,
menubar,
notificationarea,
savewidget,
savewidget,
actions,
keyboardmanager,
config,
kernelselector
kernelselector,
CodeMirror,
about,
// please keep sure that even if not used, this is loaded last
custom
) {
"use strict";
// compat with old IPython, remove for IPython > 3.0
window.CodeMirror = CodeMirror;
var common_options = {
ws_url : utils.get_body_data("wsUrl"),
base_url : utils.get_body_data("baseUrl"),
ws_url : IPython.utils.get_body_data("wsUrl"),
notebook_path : utils.get_body_data("notebookPath"),
notebook_name : utils.get_body_data('notebookName')
};
@ -55,34 +70,46 @@ require([
var pager = new pager.Pager('div#pager', 'div#pager_splitter', {
layout_manager: layout_manager,
events: events});
var acts = new actions.init();
var keyboard_manager = new keyboardmanager.KeyboardManager({
pager: pager,
events: events});
events: events,
actions: acts });
var save_widget = new savewidget.SaveWidget('span#save_widget', {
events: events,
keyboard_manager: keyboard_manager});
var contents = new contents.Contents($.extend({
events: events},
common_options));
var config_section = new configmod.ConfigSection('notebook', common_options);
config_section.load();
var notebook = new notebook.Notebook('div#notebook', $.extend({
events: events,
keyboard_manager: keyboard_manager,
save_widget: save_widget,
contents: contents,
config: user_config},
common_options));
var login_widget = new loginwidget.LoginWidget('span#login_widget', common_options);
var toolbar = new maintoolbar.MainToolBar('#maintoolbar-container', {
notebook: notebook,
events: events});
events: events,
actions: acts});
var quick_help = new quickhelp.QuickHelp({
keyboard_manager: keyboard_manager,
events: events,
notebook: notebook});
keyboard_manager.set_notebook(notebook);
keyboard_manager.set_quickhelp(quick_help);
var menubar = new menubar.MenuBar('#menubar', $.extend({
notebook: notebook,
contents: contents,
layout_manager: layout_manager,
events: events,
save_widget: save_widget,
quick_help: quick_help},
common_options));
var notification_area = new notificationarea.NotificationArea(
var notification_area = new notificationarea.NotebookNotificationArea(
'#notification_area', {
events: events,
save_widget: save_widget,
@ -122,6 +149,7 @@ require([
IPython.page = page;
IPython.layout_manager = layout_manager;
IPython.notebook = notebook;
IPython.contents = contents;
IPython.pager = pager;
IPython.quick_help = quick_help;
IPython.login_widget = login_widget;
@ -134,6 +162,13 @@ require([
IPython.tooltip = notebook.tooltip;
events.trigger('app_initialized.NotebookApp');
notebook.load_notebook(common_options.notebook_name, common_options.notebook_path);
config_section.loaded.then(function() {
if (config_section.data.load_extensions) {
var nbextension_paths = Object.getOwnPropertyNames(
config_section.data.load_extensions);
IPython.load_extensions.apply(this, nbextension_paths);
}
});
notebook.load_notebook(common_options.notebook_path);
});

@ -10,14 +10,16 @@ define([
"use strict";
var MainToolBar = function (selector, options) {
// Constructor
//
// Parameters:
// selector: string
// options: dictionary
// Dictionary of keyword arguments.
// events: $(Events) instance
// notebook: Notebook instance
/**
* Constructor
*
* Parameters:
* selector: string
* options: dictionary
* Dictionary of keyword arguments.
* events: $(Events) instance
* notebook: Notebook instance
*/
toolbar.ToolBar.apply(this, arguments);
this.events = options.events;
this.notebook = options.notebook;
@ -27,7 +29,7 @@ define([
this.bind_events();
};
MainToolBar.prototype = new toolbar.ToolBar();
MainToolBar.prototype = Object.create(toolbar.ToolBar.prototype);
MainToolBar.prototype.construct = function () {
var that = this;
@ -108,7 +110,9 @@ define([
label : 'Run Cell',
icon : 'fa-play',
callback : function () {
// emulate default shift-enter behavior
/**
* emulate default shift-enter behavior
*/
that.notebook.execute_cell_and_select_below();
}
},
@ -117,7 +121,7 @@ define([
label : 'Interrupt',
icon : 'fa-stop',
callback : function () {
that.notebook.session.interrupt_kernel();
that.notebook.kernel.interrupt();
}
},
{
@ -139,12 +143,7 @@ define([
.append($('<option/>').attr('value','code').text('Code'))
.append($('<option/>').attr('value','markdown').text('Markdown'))
.append($('<option/>').attr('value','raw').text('Raw NBConvert'))
.append($('<option/>').attr('value','heading1').text('Heading 1'))
.append($('<option/>').attr('value','heading2').text('Heading 2'))
.append($('<option/>').attr('value','heading3').text('Heading 3'))
.append($('<option/>').attr('value','heading4').text('Heading 4'))
.append($('<option/>').attr('value','heading5').text('Heading 5'))
.append($('<option/>').attr('value','heading6').text('Heading 6'))
.append($('<option/>').attr('value','heading').text('Heading'))
);
};
@ -190,24 +189,23 @@ define([
this.element.find('#cell_type').change(function () {
var cell_type = $(this).val();
if (cell_type === 'code') {
switch (cell_type) {
case 'code':
that.notebook.to_code();
} else if (cell_type === 'markdown') {
break;
case 'markdown':
that.notebook.to_markdown();
} else if (cell_type === 'raw') {
break;
case 'raw':
that.notebook.to_raw();
} else if (cell_type === 'heading1') {
that.notebook.to_heading(undefined, 1);
} else if (cell_type === 'heading2') {
that.notebook.to_heading(undefined, 2);
} else if (cell_type === 'heading3') {
that.notebook.to_heading(undefined, 3);
} else if (cell_type === 'heading4') {
that.notebook.to_heading(undefined, 4);
} else if (cell_type === 'heading5') {
that.notebook.to_heading(undefined, 5);
} else if (cell_type === 'heading6') {
that.notebook.to_heading(undefined, 6);
break;
case 'heading':
that.notebook._warn_heading();
that.notebook.to_heading();
that.element.find('#cell_type').val("markdown");
break;
default:
console.log("unrecognized cell type:", cell_type);
}
});
this.events.on('selected_cell_type_changed.Notebook', function (event, data) {

@ -2,36 +2,41 @@
// Distributed under the terms of the Modified BSD License.
define([
'base/js/namespace',
'jquery',
'base/js/namespace',
'base/js/dialog',
'base/js/utils',
'notebook/js/tour',
'bootstrap',
'moment',
], function(IPython, $, utils, tour, bootstrap, moment) {
], function($, IPython, dialog, utils, tour, bootstrap, moment) {
"use strict";
var MenuBar = function (selector, options) {
// Constructor
//
// A MenuBar Class to generate the menubar of IPython notebook
//
// Parameters:
// selector: string
// options: dictionary
// Dictionary of keyword arguments.
// notebook: Notebook instance
// layout_manager: LayoutManager instance
// events: $(Events) instance
// save_widget: SaveWidget instance
// quick_help: QuickHelp instance
// base_url : string
// notebook_path : string
// notebook_name : string
/**
* Constructor
*
* A MenuBar Class to generate the menubar of IPython notebook
*
* Parameters:
* selector: string
* options: dictionary
* Dictionary of keyword arguments.
* notebook: Notebook instance
* contents: ContentManager instance
* layout_manager: LayoutManager instance
* events: $(Events) instance
* save_widget: SaveWidget instance
* quick_help: QuickHelp instance
* base_url : string
* notebook_path : string
* notebook_name : string
*/
options = options || {};
this.base_url = options.base_url || utils.get_body_data("baseUrl");
this.selector = selector;
this.notebook = options.notebook;
this.contents = options.contents;
this.layout_manager = options.layout_manager;
this.events = options.events;
this.save_widget = options.save_widget;
@ -66,33 +71,52 @@ define([
MenuBar.prototype._nbconvert = function (format, download) {
download = download || false;
var notebook_path = this.notebook.notebook_path;
var notebook_name = this.notebook.notebook_name;
if (this.notebook.dirty) {
this.notebook.save_notebook({async : false});
}
var url = utils.url_join_encode(
this.base_url,
'nbconvert',
format,
notebook_path,
notebook_name
notebook_path
) + "?download=" + download.toString();
window.open(url);
var w = window.open()
if (this.notebook.dirty) {
this.notebook.save_notebook().then(function() {
w.location = url;
});
} else {
w.location = url;
}
};
MenuBar.prototype.bind_events = function () {
// File
/**
* File
*/
var that = this;
this.element.find('#new_notebook').click(function () {
that.notebook.new_notebook();
var w = window.open();
// Create a new notebook in the same path as the current
// notebook's path.
var parent = utils.url_path_split(that.notebook.notebook_path)[0];
that.contents.new_untitled(parent, {type: "notebook"}).then(
function (data) {
w.location = utils.url_join_encode(
that.base_url, 'notebooks', data.path
);
},
function(error) {
w.close();
dialog.modal({
title : 'Creating Notebook Failed',
body : "The error was: " + error.message,
buttons : {'OK' : {'class' : 'btn-primary'}}
});
}
);
});
this.element.find('#open_notebook').click(function () {
window.open(utils.url_join_encode(
that.notebook.base_url,
'tree',
that.notebook.notebook_path
));
var parent = utils.url_path_split(that.notebook.notebook_path)[0];
window.open(utils.url_join_encode(that.base_url, 'tree', parent));
});
this.element.find('#copy_notebook').click(function () {
that.notebook.copy_notebook();
@ -101,28 +125,18 @@ define([
this.element.find('#download_ipynb').click(function () {
var base_url = that.notebook.base_url;
var notebook_path = that.notebook.notebook_path;
var notebook_name = that.notebook.notebook_name;
if (that.notebook.dirty) {
that.notebook.save_notebook({async : false});
}
var url = utils.url_join_encode(
base_url,
'files',
notebook_path,
notebook_name
);
window.location.assign(url);
var url = utils.url_join_encode(base_url, 'files', notebook_path);
window.open(url + '?download=1');
});
this.element.find('#print_preview').click(function () {
that._nbconvert('html', false);
});
this.element.find('#download_py').click(function () {
that._nbconvert('python', true);
});
this.element.find('#download_html').click(function () {
that._nbconvert('html', true);
});
@ -159,7 +173,9 @@ define([
});
this.element.find('#kill_and_exit').click(function () {
var close_window = function () {
// allow closing of new tabs in Chromium, impossible in FF
/**
* allow closing of new tabs in Chromium, impossible in FF
*/
window.open('', '_self', '');
window.close();
};
@ -246,24 +262,6 @@ define([
this.element.find('#to_raw').click(function () {
that.notebook.to_raw();
});
this.element.find('#to_heading1').click(function () {
that.notebook.to_heading(undefined, 1);
});
this.element.find('#to_heading2').click(function () {
that.notebook.to_heading(undefined, 2);
});
this.element.find('#to_heading3').click(function () {
that.notebook.to_heading(undefined, 3);
});
this.element.find('#to_heading4').click(function () {
that.notebook.to_heading(undefined, 4);
});
this.element.find('#to_heading5').click(function () {
that.notebook.to_heading(undefined, 5);
});
this.element.find('#to_heading6').click(function () {
that.notebook.to_heading(undefined, 6);
});
this.element.find('#toggle_current_output').click(function () {
that.notebook.toggle_output();
@ -287,11 +285,14 @@ define([
// Kernel
this.element.find('#int_kernel').click(function () {
that.notebook.session.interrupt_kernel();
that.notebook.kernel.interrupt();
});
this.element.find('#restart_kernel').click(function () {
that.notebook.restart_kernel();
});
this.element.find('#reconnect_kernel').click(function () {
that.notebook.kernel.reconnect();
});
// Help
if (this.tour) {
this.element.find('#notebook_tour').click(function () {
@ -313,6 +314,16 @@ define([
this.events.on('checkpoint_created.Notebook', function (event, data) {
that.update_restore_checkpoint(that.notebook.checkpoints);
});
this.events.on('notebook_loaded.Notebook', function() {
var langinfo = that.notebook.metadata.language_info || {};
that.update_nbconvert_script(langinfo);
});
this.events.on('kernel_ready.Kernel', function(event, data) {
var langinfo = data.kernel.info_reply.language_info || {};
that.update_nbconvert_script(langinfo);
});
};
MenuBar.prototype.update_restore_checkpoint = function(checkpoints) {
@ -345,6 +356,33 @@ define([
);
});
};
MenuBar.prototype.update_nbconvert_script = function(langinfo) {
/**
* Set the 'Download as foo' menu option for the relevant language.
*/
var el = this.element.find('#download_script');
var that = this;
// Set menu entry text to e.g. "Python (.py)"
var langname = (langinfo.name || 'Script')
langname = langname.charAt(0).toUpperCase()+langname.substr(1) // Capitalise
el.find('a').text(langname + ' ('+(langinfo.file_extension || 'txt')+')');
// Unregister any previously registered handlers
el.off('click');
if (langinfo.nbconvert_exporter) {
// Metadata specifies a specific exporter, e.g. 'python'
el.click(function() {
that._nbconvert(langinfo.nbconvert_exporter, true);
});
} else {
// Use generic 'script' exporter
el.click(function() {
that._nbconvert('script', true);
});
}
};
// Backwards compatability.
IPython.MenuBar = MenuBar;

File diff suppressed because it is too large Load Diff

@ -1,90 +1,51 @@
// Copyright (c) IPython Development Team.
// Distributed under the terms of the Modified BSD License.
define([
'base/js/namespace',
'jquery',
'base/js/utils',
'base/js/dialog',
'notebook/js/notificationwidget',
'base/js/notificationarea',
'moment'
], function(IPython, $, utils, dialog, notificationwidget, moment) {
], function(IPython, $, utils, dialog, notificationarea, moment) {
"use strict";
var NotificationArea = function (selector, options) {
// Constructor
//
// Parameters:
// selector: string
// options: dictionary
// Dictionary of keyword arguments.
// notebook: Notebook instance
// events: $(Events) instance
// save_widget: SaveWidget instance
this.selector = selector;
this.events = options.events;
var NotificationArea = notificationarea.NotificationArea;
var NotebookNotificationArea = function(selector, options) {
NotificationArea.apply(this, [selector, options]);
this.save_widget = options.save_widget;
this.notebook = options.notebook;
this.keyboard_manager = options.keyboard_manager;
if (this.selector !== undefined) {
this.element = $(selector);
}
this.widget_dict = {};
};
NotificationArea.prototype.temp_message = function (msg, timeout, css_class) {
var tdiv = $('<div>')
.addClass('notification_widget')
.addClass(css_class)
.hide()
.text(msg);
$(this.selector).append(tdiv);
var tmout = Math.max(1500,(timeout||1500));
tdiv.fadeIn(100);
setTimeout(function () {
tdiv.fadeOut(100, function () {tdiv.remove();});
}, tmout);
};
NotificationArea.prototype.widget = function(name) {
if(this.widget_dict[name] === undefined) {
return this.new_notification_widget(name);
}
return this.get_widget(name);
};
NotificationArea.prototype.get_widget = function(name) {
if(this.widget_dict[name] === undefined) {
throw('no widgets with this name');
}
return this.widget_dict[name];
};
NotificationArea.prototype.new_notification_widget = function(name) {
if(this.widget_dict[name] !== undefined) {
throw('widget with that name already exists ! ');
}
var div = $('<div/>').attr('id','notification_'+name);
$(this.selector).append(div);
this.widget_dict[name] = new notificationwidget.NotificationWidget('#notification_'+name);
return this.widget_dict[name];
}
NotebookNotificationArea.prototype = Object.create(NotificationArea.prototype);
/**
* Initialize the default set of notification widgets.
*
* @method init_notification_widgets
*/
NotebookNotificationArea.prototype.init_notification_widgets = function () {
this.init_kernel_notification_widget();
this.init_notebook_notification_widget();
};
NotificationArea.prototype.init_notification_widgets = function() {
/**
* Initialize the notification widget for kernel status messages.
*
* @method init_kernel_notification_widget
*/
NotebookNotificationArea.prototype.init_kernel_notification_widget = function () {
var that = this;
var knw = this.new_notification_widget('kernel');
var $kernel_ind_icon = $("#kernel_indicator_icon");
var $modal_ind_icon = $("#modal_indicator_icon");
// Command/Edit mode
this.events.on('edit_mode.Notebook',function () {
this.events.on('edit_mode.Notebook', function () {
that.save_widget.update_document_title();
$modal_ind_icon.attr('class','edit_mode_icon').attr('title','Edit Mode');
});
this.events.on('command_mode.Notebook',function () {
this.events.on('command_mode.Notebook', function () {
that.save_widget.update_document_title();
$modal_ind_icon.attr('class','command_mode_icon').attr('title','Command Mode');
});
@ -92,110 +53,210 @@ define([
// Implicitly start off in Command mode, switching to Edit mode will trigger event
$modal_ind_icon.attr('class','command_mode_icon').attr('title','Command Mode');
// Kernel events
this.events.on('status_idle.Kernel',function () {
that.save_widget.update_document_title();
$kernel_ind_icon.attr('class','kernel_idle_icon').attr('title','Kernel Idle');
// Kernel events
// this can be either kernel_created.Kernel or kernel_created.Session
this.events.on('kernel_created.Kernel kernel_created.Session', function () {
knw.info("Kernel Created", 500);
});
this.events.on('status_busy.Kernel',function () {
window.document.title='(Busy) '+window.document.title;
$kernel_ind_icon.attr('class','kernel_busy_icon').attr('title','Kernel Busy');
this.events.on('kernel_reconnecting.Kernel', function () {
knw.warning("Connecting to kernel");
});
this.events.on('status_restarting.Kernel',function () {
this.events.on('kernel_connection_dead.Kernel', function (evt, info) {
knw.danger("Not Connected", undefined, function () {
// schedule reconnect a short time in the future, don't reconnect immediately
setTimeout($.proxy(info.kernel.reconnect, info.kernel), 500);
}, {title: 'click to reconnect'});
});
this.events.on('kernel_connected.Kernel', function () {
knw.info("Connected", 500);
});
this.events.on('kernel_restarting.Kernel', function () {
that.save_widget.update_document_title();
knw.set_message("Restarting kernel", 2000);
});
this.events.on('status_dead.Kernel',function () {
this.events.on('kernel_autorestarting.Kernel', function (evt, info) {
// Only show the dialog on the first restart attempt. This
// number gets tracked by the `Kernel` object and passed
// along here, because we don't want to show the user 5
// dialogs saying the same thing (which is the number of
// times it tries restarting).
if (info.attempt === 1) {
dialog.kernel_modal({
notebook: that.notebook,
keyboard_manager: that.keyboard_manager,
title: "Kernel Restarting",
body: "The kernel appears to have died. It will restart automatically.",
buttons: {
OK : {
class : "btn-primary"
}
}
});
};
that.save_widget.update_document_title();
knw.danger("Dead kernel");
$kernel_ind_icon.attr('class','kernel_dead_icon').attr('title','Kernel Dead');
});
this.events.on('status_interrupting.Kernel',function () {
this.events.on('kernel_interrupting.Kernel', function () {
knw.set_message("Interrupting kernel", 2000);
});
// Start the kernel indicator in the busy state, and send a kernel_info request.
// When the kernel_info reply arrives, the kernel is idle.
$kernel_ind_icon.attr('class','kernel_busy_icon').attr('title','Kernel Busy');
this.events.on('status_started.Kernel', function (evt, data) {
knw.info("Websockets Connected", 500);
that.events.trigger('status_busy.Kernel');
data.kernel.kernel_info(function () {
that.events.trigger('status_idle.Kernel');
});
});
this.events.on('status_dead.Kernel',function () {
var msg = 'The kernel has died, and the automatic restart has failed.' +
' It is possible the kernel cannot be restarted.' +
' If you are not able to restart the kernel, you will still be able to save' +
' the notebook, but running code will no longer work until the notebook' +
' is reopened.';
dialog.modal({
title: "Dead kernel",
body : msg,
keyboard_manager: that.keyboard_manager,
notebook: that.notebook,
buttons : {
"Manual Restart": {
class: "btn-danger",
click: function () {
that.events.trigger('status_restarting.Kernel');
that.notebook.start_kernel();
}
},
"Don't restart": {}
}
});
});
this.events.on('websocket_closed.Kernel', function (event, data) {
var kernel = data.kernel;
var ws_url = data.ws_url;
var early = data.early;
var msg;
this.events.on('kernel_disconnected.Kernel', function () {
$kernel_ind_icon
.attr('class', 'kernel_disconnected_icon')
.attr('title', 'No Connection to Kernel');
if (!early) {
knw.warning('Reconnecting');
setTimeout(function () {
kernel.start_channels();
}, 5000);
return;
});
this.events.on('kernel_connection_failed.Kernel', function (evt, info) {
// only show the dialog if this is the first failed
// connect attempt, because the kernel will continue
// trying to reconnect and we don't want to spam the user
// with messages
if (info.attempt === 1) {
var msg = "A connection to the notebook server could not be established." +
" The notebook will continue trying to reconnect, but" +
" until it does, you will NOT be able to run code. Check your" +
" network connection or notebook server configuration.";
dialog.kernel_modal({
title: "Connection failed",
body: msg,
keyboard_manager: that.keyboard_manager,
notebook: that.notebook,
buttons : {
"OK": {}
}
});
}
console.log('WebSocket connection failed: ', ws_url);
msg = "A WebSocket connection could not be established." +
" You will NOT be able to run code. Check your" +
" network connection or notebook server configuration.";
dialog.modal({
title: "WebSocket connection failed",
body: msg,
keyboard_manager: that.keyboard_manager,
notebook: that.notebook,
buttons : {
"OK": {},
"Reconnect": {
click: function () {
knw.warning('Reconnecting');
setTimeout(function () {
kernel.start_channels();
}, 5000);
}
});
this.events.on('kernel_killed.Kernel kernel_killed.Session', function () {
that.save_widget.update_document_title();
knw.danger("Dead kernel");
$kernel_ind_icon.attr('class','kernel_dead_icon').attr('title','Kernel Dead');
});
this.events.on('kernel_dead.Kernel', function () {
var showMsg = function () {
var msg = 'The kernel has died, and the automatic restart has failed.' +
' It is possible the kernel cannot be restarted.' +
' If you are not able to restart the kernel, you will still be able to save' +
' the notebook, but running code will no longer work until the notebook' +
' is reopened.';
dialog.kernel_modal({
title: "Dead kernel",
body : msg,
keyboard_manager: that.keyboard_manager,
notebook: that.notebook,
buttons : {
"Manual Restart": {
class: "btn-danger",
click: function () {
that.notebook.start_session();
}
},
"Don't restart": {}
}
});
return false;
};
that.save_widget.update_document_title();
knw.danger("Dead kernel", undefined, showMsg);
$kernel_ind_icon.attr('class','kernel_dead_icon').attr('title','Kernel Dead');
showMsg();
});
this.events.on('kernel_dead.Session', function (evt, info) {
var full = info.xhr.responseJSON.message;
var short = info.xhr.responseJSON.short_message || 'Kernel error';
var traceback = info.xhr.responseJSON.traceback;
var showMsg = function () {
var msg = $('<div/>').append($('<p/>').text(full));
var cm, cm_elem, cm_open;
if (traceback) {
cm_elem = $('<div/>')
.css('margin-top', '1em')
.css('padding', '1em')
.addClass('output_scroll');
msg.append(cm_elem);
cm = CodeMirror(cm_elem.get(0), {
mode: "python",
readOnly : true
});
cm.setValue(traceback);
cm_open = $.proxy(cm.refresh, cm);
}
});
dialog.kernel_modal({
title: "Failed to start the kernel",
body : msg,
keyboard_manager: that.keyboard_manager,
notebook: that.notebook,
open: cm_open,
buttons : {
"Ok": { class: 'btn-primary' }
}
});
return false;
};
that.save_widget.update_document_title();
$kernel_ind_icon.attr('class','kernel_dead_icon').attr('title','Kernel Dead');
knw.danger(short, undefined, showMsg);
});
this.events.on('kernel_starting.Kernel', function () {
window.document.title='(Starting) '+window.document.title;
$kernel_ind_icon.attr('class','kernel_busy_icon').attr('title','Kernel Busy');
knw.set_message("Kernel starting, please wait...");
});
this.events.on('kernel_ready.Kernel', function () {
that.save_widget.update_document_title();
$kernel_ind_icon.attr('class','kernel_idle_icon').attr('title','Kernel Idle');
knw.info("Kernel ready", 500);
});
this.events.on('kernel_idle.Kernel', function () {
that.save_widget.update_document_title();
$kernel_ind_icon.attr('class','kernel_idle_icon').attr('title','Kernel Idle');
});
this.events.on('kernel_busy.Kernel', function () {
window.document.title='(Busy) '+window.document.title;
$kernel_ind_icon.attr('class','kernel_busy_icon').attr('title','Kernel Busy');
});
// Start the kernel indicator in the busy state, and send a kernel_info request.
// When the kernel_info reply arrives, the kernel is idle.
$kernel_ind_icon.attr('class','kernel_busy_icon').attr('title','Kernel Busy');
};
/**
* Initialize the notification widget for notebook status messages.
*
* @method init_notebook_notification_widget
*/
NotebookNotificationArea.prototype.init_notebook_notification_widget = function () {
var nnw = this.new_notification_widget('notebook');
// Notebook events
@ -211,8 +272,11 @@ define([
this.events.on('notebook_saved.Notebook', function () {
nnw.set_message("Notebook saved",2000);
});
this.events.on('notebook_save_failed.Notebook', function (evt, xhr, status, data) {
nnw.warning(data || "Notebook save failed");
this.events.on('notebook_save_failed.Notebook', function (evt, error) {
nnw.warning(error.message || "Notebook save failed");
});
this.events.on('notebook_copy_failed.Notebook', function (evt, error) {
nnw.warning(error.message || "Notebook copy failed");
});
// Checkpoint events
@ -247,10 +311,10 @@ define([
this.events.on('autosave_enabled.Notebook', function (evt, interval) {
nnw.set_message("Saving every " + interval / 1000 + "s", 1000);
});
};
IPython.NotificationArea = NotificationArea;
return {'NotificationArea': NotificationArea};
// Backwards compatibility.
IPython.NotificationArea = NotebookNotificationArea;
return {'NotebookNotificationArea': NotebookNotificationArea};
});

@ -1,104 +0,0 @@
// Copyright (c) IPython Development Team.
// Distributed under the terms of the Modified BSD License.
define([
'base/js/namespace',
'jquery',
], function(IPython, $) {
"use strict";
var NotificationWidget = function (selector) {
this.selector = selector;
this.timeout = null;
this.busy = false;
if (this.selector !== undefined) {
this.element = $(selector);
this.style();
}
this.element.hide();
var that = this;
this.inner = $('<span/>');
this.element.append(this.inner);
};
NotificationWidget.prototype.style = function () {
this.element.addClass('notification_widget');
};
// msg : message to display
// timeout : time in ms before diseapearing
//
// if timeout <= 0
// click_callback : function called if user click on notification
// could return false to prevent the notification to be dismissed
NotificationWidget.prototype.set_message = function (msg, timeout, click_callback, options) {
var options = options || {};
var callback = click_callback || function() {return true;};
var that = this;
// unbind potential previous callback
this.element.unbind('click');
this.inner.attr('class', options.icon);
this.inner.attr('title', options.title);
this.inner.text(msg);
this.element.fadeIn(100);
// reset previous set style
this.element.removeClass();
this.style();
if (options.class){
this.element.addClass(options.class)
}
if (this.timeout !== null) {
clearTimeout(this.timeout);
this.timeout = null;
}
if (timeout !== undefined && timeout >=0) {
this.timeout = setTimeout(function () {
that.element.fadeOut(100, function () {that.inner.text('');});
that.timeout = null;
}, timeout);
} else {
this.element.click(function() {
if( callback() !== false ) {
that.element.fadeOut(100, function () {that.inner.text('');});
that.element.unbind('click');
}
if (that.timeout !== undefined) {
that.timeout = undefined;
clearTimeout(that.timeout);
}
});
}
};
NotificationWidget.prototype.info = function (msg, timeout, click_callback, options) {
var options = options || {};
options.class = options.class +' info';
var timeout = timeout || 3500;
this.set_message(msg, timeout, click_callback, options);
}
NotificationWidget.prototype.warning = function (msg, timeout, click_callback, options) {
var options = options || {};
options.class = options.class +' warning';
this.set_message(msg, timeout, click_callback, options);
}
NotificationWidget.prototype.danger = function (msg, timeout, click_callback, options) {
var options = options || {};
options.class = options.class +' danger';
this.set_message(msg, timeout, click_callback, options);
}
NotificationWidget.prototype.get_message = function () {
return this.inner.html();
};
// For backwards compatibility.
IPython.NotificationWidget = NotificationWidget;
return {'NotificationWidget': NotificationWidget};
});

@ -81,7 +81,7 @@ define([
*
*/
OutputArea.prototype._should_scroll = function (lines) {
if (lines <=0 ){ return }
if (lines <=0 ){ return; }
if (!lines) {
lines = 100;
}
@ -177,7 +177,7 @@ define([
OutputArea.prototype.scroll_if_long = function (lines) {
var n = lines | OutputArea.minimum_scroll_threshold;
if(n <= 0){
return
return;
}
if (this._should_scroll(n)) {
@ -210,17 +210,17 @@ define([
var msg_type = json.output_type = msg.header.msg_type;
var content = msg.content;
if (msg_type === "stream") {
json.text = content.data;
json.stream = content.name;
json.text = content.text;
json.name = content.name;
} else if (msg_type === "display_data") {
json = content.data;
json.data = content.data;
json.output_type = msg_type;
json.metadata = content.metadata;
} else if (msg_type === "execute_result") {
json = content.data;
json.data = content.data;
json.output_type = msg_type;
json.metadata = content.metadata;
json.prompt_number = content.execution_count;
json.execution_count = content.execution_count;
} else if (msg_type === "error") {
json.ename = content.ename;
json.evalue = content.evalue;
@ -233,16 +233,6 @@ define([
};
OutputArea.prototype.rename_keys = function (data, key_map) {
var remapped = {};
for (var key in data) {
var new_key = key_map[key] || key;
remapped[new_key] = data[key];
}
return remapped;
};
OutputArea.output_types = [
'application/javascript',
'text/html',
@ -255,14 +245,18 @@ define([
'text/plain'
];
OutputArea.prototype.validate_output = function (json) {
// scrub invalid outputs
// TODO: right now everything is a string, but JSON really shouldn't be.
// nbformat 4 will fix that.
OutputArea.prototype.validate_mimebundle = function (json) {
/**
* scrub invalid outputs
*/
var data = json.data;
$.map(OutputArea.output_types, function(key){
if (json[key] !== undefined && typeof json[key] !== 'string') {
console.log("Invalid type for " + key, json[key]);
delete json[key];
if (key !== 'application/json' &&
data[key] !== undefined &&
typeof data[key] !== 'string'
) {
console.log("Invalid type for " + key, data[key]);
delete data[key];
}
});
return json;
@ -271,9 +265,6 @@ define([
OutputArea.prototype.append_output = function (json) {
this.expand();
// validate output data types
json = this.validate_output(json);
// Clear the output if clear is queued.
var needs_height_reset = false;
if (this.clear_queued) {
@ -282,14 +273,25 @@ define([
}
var record_output = true;
if (json.output_type === 'execute_result') {
this.append_execute_result(json);
} else if (json.output_type === 'error') {
this.append_error(json);
} else if (json.output_type === 'stream') {
// append_stream might have merged the output with earlier stream output
record_output = this.append_stream(json);
switch(json.output_type) {
case 'execute_result':
json = this.validate_mimebundle(json);
this.append_execute_result(json);
break;
case 'stream':
// append_stream might have merged the output with earlier stream output
record_output = this.append_stream(json);
break;
case 'error':
this.append_error(json);
break;
case 'display_data':
// append handled below
json = this.validate_mimebundle(json);
break;
default:
console.log("unrecognized output type: " + json.output_type);
this.append_unrecognized(json);
}
// We must release the animation fixed height in a callback since Gecko
@ -297,8 +299,10 @@ define([
// available.
var that = this;
var handle_appended = function ($el) {
// Only reset the height to automatic if the height is currently
// fixed (done by wait=True flag on clear_output).
/**
* Only reset the height to automatic if the height is currently
* fixed (done by wait=True flag on clear_output).
*/
if (needs_height_reset) {
that.element.height('');
}
@ -376,12 +380,14 @@ define([
} else {
return subarea;
}
}
};
OutputArea.prototype._append_javascript_error = function (err, element) {
// display a message when a javascript error occurs in display output
var msg = "Javascript error adding output!"
/**
* display a message when a javascript error occurs in display output
*/
var msg = "Javascript error adding output!";
if ( element === undefined ) return;
element
.append($('<div/>').text(msg).addClass('js-error'))
@ -390,10 +396,12 @@ define([
};
OutputArea.prototype._safe_append = function (toinsert) {
// safely append an item to the document
// this is an object created by user code,
// and may have errors, which should not be raised
// under any circumstances.
/**
* safely append an item to the document
* this is an object created by user code,
* and may have errors, which should not be raised
* under any circumstances.
*/
try {
this.element.append(toinsert);
} catch(err) {
@ -406,11 +414,14 @@ define([
this._append_javascript_error(err, subarea);
this.element.append(toinsert);
}
// Notify others of changes.
this.element.trigger('changed');
};
OutputArea.prototype.append_execute_result = function (json) {
var n = json.prompt_number || ' ';
var n = json.execution_count || ' ';
var toinsert = this.create_output_area();
if (this.prompt_area) {
toinsert.find('div.prompt').addClass('output_prompt').text('Out[' + n + ']:');
@ -421,9 +432,9 @@ define([
}
this._safe_append(toinsert);
// If we just output latex, typeset it.
if ((json['text/latex'] !== undefined) ||
(json['text/html'] !== undefined) ||
(json['text/markdown'] !== undefined)) {
if ((json.data['text/latex'] !== undefined) ||
(json.data['text/html'] !== undefined) ||
(json.data['text/markdown'] !== undefined)) {
this.typeset();
}
};
@ -449,17 +460,12 @@ define([
OutputArea.prototype.append_stream = function (json) {
// temporary fix: if stream undefined (json file written prior to this patch),
// default to most likely stdout:
if (json.stream === undefined){
json.stream = 'stdout';
}
var text = json.text;
var subclass = "output_"+json.stream;
var subclass = "output_"+json.name;
if (this.outputs.length > 0){
// have at least one output to consider
var last = this.outputs[this.outputs.length-1];
if (last.output_type == 'stream' && json.stream == last.stream){
if (last.output_type == 'stream' && json.name == last.name){
// latest output was in the same stream,
// so append directly into its pre tag
// escape ANSI & HTML specials:
@ -493,14 +499,31 @@ define([
};
OutputArea.prototype.append_unrecognized = function (json) {
var that = this;
var toinsert = this.create_output_area();
var subarea = $('<div/>').addClass('output_subarea output_unrecognized');
toinsert.append(subarea);
subarea.append(
$("<a>")
.attr("href", "#")
.text("Unrecognized output: " + json.output_type)
.click(function () {
that.events.trigger('unrecognized_output.OutputArea', {output: json})
})
);
this._safe_append(toinsert);
};
OutputArea.prototype.append_display_data = function (json, handle_inserted) {
var toinsert = this.create_output_area();
if (this.append_mime_type(json, toinsert, handle_inserted)) {
this._safe_append(toinsert);
// If we just output latex, typeset it.
if ((json['text/latex'] !== undefined) ||
(json['text/html'] !== undefined) ||
(json['text/markdown'] !== undefined)) {
if ((json.data['text/latex'] !== undefined) ||
(json.data['text/html'] !== undefined) ||
(json.data['text/markdown'] !== undefined)) {
this.typeset();
}
}
@ -518,8 +541,8 @@ define([
for (var i=0; i < OutputArea.display_order.length; i++) {
var type = OutputArea.display_order[i];
var append = OutputArea.append_map[type];
if ((json[type] !== undefined) && append) {
var value = json[type];
if ((json.data[type] !== undefined) && append) {
var value = json.data[type];
if (!this.trusted && !OutputArea.safe_outputs[type]) {
// not trusted, sanitize HTML
if (type==='text/html' || type==='text/svg') {
@ -563,16 +586,19 @@ define([
var text_and_math = mathjaxutils.remove_math(markdown);
var text = text_and_math[0];
var math = text_and_math[1];
var html = marked.parser(marked.lexer(text));
html = mathjaxutils.replace_math(html, math);
toinsert.append(html);
marked(text, function (err, html) {
html = mathjaxutils.replace_math(html, math);
toinsert.append(html);
});
element.append(toinsert);
return toinsert;
};
var append_javascript = function (js, md, element) {
// We just eval the JS code, element appears in the local scope.
/**
* We just eval the JS code, element appears in the local scope.
*/
var type = 'application/javascript';
var toinsert = this.create_output_subarea(md, "output_javascript", type);
this.keyboard_manager.register_events(toinsert);
@ -636,14 +662,16 @@ define([
};
OutputArea.prototype._dblclick_to_reset_size = function (img, immediately, resize_parent) {
// Add a resize handler to an element
//
// img: jQuery element
// immediately: bool=False
// Wait for the element to load before creating the handle.
// resize_parent: bool=True
// Should the parent of the element be resized when the element is
// reset (by double click).
/**
* Add a resize handler to an element
*
* img: jQuery element
* immediately: bool=False
* Wait for the element to load before creating the handle.
* resize_parent: bool=True
* Should the parent of the element be resized when the element is
* reset (by double click).
*/
var callback = function (){
var h0 = img.height();
var w0 = img.width();
@ -674,7 +702,9 @@ define([
};
var set_width_height = function (img, md, mime) {
// set width and height of an img element from metadata
/**
* set width and height of an img element from metadata
*/
var height = _get_metadata_key(md, 'height', mime);
if (height !== undefined) img.attr('height', height);
var width = _get_metadata_key(md, 'width', mime);
@ -722,15 +752,17 @@ define([
var toinsert = this.create_output_subarea(md, "output_pdf", type);
var a = $('<a/>').attr('href', 'data:application/pdf;base64,'+pdf);
a.attr('target', '_blank');
a.text('View PDF')
a.text('View PDF');
toinsert.append(a);
element.append(toinsert);
return toinsert;
}
};
var append_latex = function (latex, md, element) {
// This method cannot do the typesetting because the latex first has to
// be on the page.
/**
* This method cannot do the typesetting because the latex first has to
* be on the page.
*/
var type = 'text/latex';
var toinsert = this.create_output_subarea(md, "output_latex", type);
toinsert.append(latex);
@ -783,7 +815,7 @@ define([
// This seemed to be needed otherwise only the cell would be focused.
// But with the modal UI, this seems to work fine with one call to focus().
raw_input.focus();
}
};
OutputArea.prototype._submit_raw_input = function (evt) {
var container = this.element.find("div.raw_input_container");
@ -797,23 +829,25 @@ define([
}
var content = {
output_type : 'stream',
stream : 'stdout',
name : 'stdout',
text : theprompt.text() + echo + '\n'
}
};
// remove form container
container.parent().remove();
// replace with plaintext version in stdout
this.append_output(content, false);
this.events.trigger('send_input_reply.Kernel', value);
}
};
OutputArea.prototype.handle_clear_output = function (msg) {
// msg spec v4 had stdout, stderr, display keys
// v4.1 replaced these with just wait
// The default behavior is the same (stdout=stderr=display=True, wait=False),
// so v4 messages will still be properly handled,
// except for the rarely used clearing less than all output.
/**
* msg spec v4 had stdout, stderr, display keys
* v4.1 replaced these with just wait
* The default behavior is the same (stdout=stderr=display=True, wait=False),
* so v4 messages will still be properly handled,
* except for the rarely used clearing less than all output.
*/
this.clear_output(msg.content.wait || false);
};
@ -824,7 +858,7 @@ define([
// If a clear is queued, clear before adding another to the queue.
if (this.clear_queued) {
this.clear_output(false);
};
}
this.clear_queued = true;
} else {
@ -842,80 +876,47 @@ define([
// them to fire if the image is never added to the page.
this.element.find('img').off('load');
this.element.html("");
// Notify others of changes.
this.element.trigger('changed');
this.outputs = [];
this.trusted = true;
this.unscroll_area();
return;
};
}
};
// JSON serialization
OutputArea.prototype.fromJSON = function (outputs) {
OutputArea.prototype.fromJSON = function (outputs, metadata) {
var len = outputs.length;
var data;
metadata = metadata || {};
for (var i=0; i<len; i++) {
data = outputs[i];
var msg_type = data.output_type;
if (msg_type == "pyout") {
// pyout message has been renamed to execute_result,
// but the nbformat has not been updated,
// so transform back to pyout for json.
msg_type = data.output_type = "execute_result";
} else if (msg_type == "pyerr") {
// pyerr message has been renamed to error,
// but the nbformat has not been updated,
// so transform back to pyerr for json.
msg_type = data.output_type = "error";
this.append_output(outputs[i]);
}
if (metadata.collapsed !== undefined) {
this.collapsed = metadata.collapsed;
if (metadata.collapsed) {
this.collapse_output();
}
if (msg_type === "display_data" || msg_type === "execute_result") {
// convert short keys to mime keys
// TODO: remove mapping of short keys when we update to nbformat 4
data = this.rename_keys(data, OutputArea.mime_map_r);
data.metadata = this.rename_keys(data.metadata, OutputArea.mime_map_r);
// msg spec JSON is an object, nbformat v3 JSON is a JSON string
if (data["application/json"] !== undefined && typeof data["application/json"] === 'string') {
data["application/json"] = JSON.parse(data["application/json"]);
}
}
if (metadata.autoscroll !== undefined) {
this.collapsed = metadata.collapsed;
if (metadata.collapsed) {
this.collapse_output();
} else {
this.expand_output();
}
this.append_output(data);
}
};
OutputArea.prototype.toJSON = function () {
var outputs = [];
var len = this.outputs.length;
var data;
for (var i=0; i<len; i++) {
data = this.outputs[i];
var msg_type = data.output_type;
if (msg_type === "display_data" || msg_type === "execute_result") {
// convert mime keys to short keys
data = this.rename_keys(data, OutputArea.mime_map);
data.metadata = this.rename_keys(data.metadata, OutputArea.mime_map);
// msg spec JSON is an object, nbformat v3 JSON is a JSON string
if (data.json !== undefined && typeof data.json !== 'string') {
data.json = JSON.stringify(data.json);
}
}
if (msg_type == "execute_result") {
// pyout message has been renamed to execute_result,
// but the nbformat has not been updated,
// so transform back to pyout for json.
data.output_type = "pyout";
} else if (msg_type == "error") {
// pyerr message has been renamed to error,
// but the nbformat has not been updated,
// so transform back to pyerr for json.
data.output_type = "pyerr";
}
outputs[i] = data;
}
return outputs;
return this.outputs;
};
/**
@ -948,29 +949,6 @@ define([
OutputArea.minimum_scroll_threshold = 20;
OutputArea.mime_map = {
"text/plain" : "text",
"text/html" : "html",
"image/svg+xml" : "svg",
"image/png" : "png",
"image/jpeg" : "jpeg",
"text/latex" : "latex",
"application/json" : "json",
"application/javascript" : "javascript",
};
OutputArea.mime_map_r = {
"text" : "text/plain",
"html" : "text/html",
"svg" : "image/svg+xml",
"png" : "image/png",
"jpeg" : "image/jpeg",
"latex" : "text/latex",
"json" : "application/json",
"javascript" : "application/javascript",
};
OutputArea.display_order = [
'application/javascript',
'text/html',

@ -9,15 +9,17 @@ define([
"use strict";
var Pager = function (pager_selector, pager_splitter_selector, options) {
// Constructor
//
// Parameters:
// pager_selector: string
// pager_splitter_selector: string
// options: dictionary
// Dictionary of keyword arguments.
// events: $(Events) instance
// layout_manager: LayoutManager instance
/**
* Constructor
*
* Parameters:
* pager_selector: string
* pager_splitter_selector: string
* options: dictionary
* Dictionary of keyword arguments.
* events: $(Events) instance
* layout_manager: LayoutManager instance
*/
this.events = options.events;
this.pager_element = $(pager_selector);
this.pager_button_area = $('#pager_button_area');
@ -30,7 +32,9 @@ define([
axis:'y',
helper: null ,
drag: function(event, ui) {
// recalculate the amount of space the pager should take
/**
* recalculate the amount of space the pager should take
*/
var pheight = ($(document.body).height()-event.clientY-4);
var downprct = pheight/options.layout_manager.app_height();
downprct = Math.min(0.9, downprct);
@ -173,8 +177,10 @@ define([
};
Pager.prototype.append_text = function (text) {
// The only user content injected with this HTML call is escaped by
// the fixConsole() method.
/**
* The only user content injected with this HTML call is escaped by
* the fixConsole() method.
*/
this.pager_element.find(".container").append($('<pre/>').html(utils.fixCarriageReturn(utils.fixConsole(text))));
};

@ -11,14 +11,16 @@ define([
var platform = utils.platform;
var QuickHelp = function (options) {
// Constructor
//
// Parameters:
// options: dictionary
// Dictionary of keyword arguments.
// events: $(Events) instance
// keyboard_manager: KeyboardManager instance
// notebook: Notebook instance
/**
* Constructor
*
* Parameters:
* options: dictionary
* Dictionary of keyword arguments.
* events: $(Events) instance
* keyboard_manager: KeyboardManager instance
* notebook: Notebook instance
*/
this.keyboard_manager = options.keyboard_manager;
this.notebook = options.notebook;
this.keyboard_manager.quick_help = this;
@ -34,10 +36,10 @@ define([
platform_specific = [
{ shortcut: "Cmd-Up", help:"go to cell start" },
{ shortcut: "Cmd-Down", help:"go to cell end" },
{ shortcut: "Opt-Left", help:"go one word left" },
{ shortcut: "Opt-Right", help:"go one word right" },
{ shortcut: "Opt-Backspace", help:"del word before" },
{ shortcut: "Opt-Delete", help:"del word after" },
{ shortcut: "Alt-Left", help:"go one word left" },
{ shortcut: "Alt-Right", help:"go one word right" },
{ shortcut: "Alt-Backspace", help:"del word before" },
{ shortcut: "Alt-Delete", help:"del word after" },
];
} else {
// PC specific
@ -65,12 +67,10 @@ define([
].concat( platform_specific );
QuickHelp.prototype.show_keyboard_shortcuts = function () {
// toggles display of keyboard shortcut dialog
/**
* toggles display of keyboard shortcut dialog
*/
var that = this;
if ( this.force_rebuild ) {
this.shortcut_dialog.remove();
@ -139,7 +139,9 @@ define([
keys[i] = "<code><strong>" + k + "</strong></code>";
continue; // leave individual keys lower-cased
}
keys[i] = ( special_case[k] ? special_case[k] : k.charAt(0).toUpperCase() + k.slice(1) );
if (k.indexOf(',') === -1){
keys[i] = ( special_case[k] ? special_case[k] : k.charAt(0).toUpperCase() + k.slice(1) );
}
keys[i] = "<code><strong>" + keys[i] + "</strong></code>";
}
return keys.join('-');

@ -12,7 +12,9 @@ define([
"use strict";
var SaveWidget = function (selector, options) {
// TODO: Remove circular ref.
/**
* TODO: Remove circular ref.
*/
this.notebook = undefined;
this.selector = selector;
this.events = options.events;
@ -28,7 +30,7 @@ define([
SaveWidget.prototype.bind_events = function () {
var that = this;
this.element.find('span#notebook_name').click(function () {
that.rename_notebook();
that.rename_notebook({notebook: that.notebook});
});
this.events.on('notebook_loaded.Notebook', function () {
that.update_notebook_name();
@ -46,6 +48,11 @@ define([
this.events.on('notebook_save_failed.Notebook', function () {
that.set_save_status('Autosave Failed!');
});
this.events.on('notebook_read_only.Notebook', function () {
that.set_save_status('(read only)');
// disable future set_save_status
that.set_save_status = function () {};
});
this.events.on('checkpoints_listed.Notebook', function (event, data) {
that._set_last_checkpoint(data[0]);
});
@ -69,41 +76,53 @@ define([
$("<br/>")
).append(
$('<input/>').attr('type','text').attr('size','25').addClass('form-control')
.val(that.notebook.get_notebook_name())
.val(options.notebook.get_notebook_name())
);
dialog.modal({
var d = dialog.modal({
title: "Rename Notebook",
body: dialog_body,
notebook: options.notebook,
keyboard_manager: this.keyboard_manager,
buttons : {
"Cancel": {},
"OK": {
class: "btn-primary",
click: function () {
var new_name = $(this).find('input').val();
if (!that.notebook.test_notebook_name(new_name)) {
$(this).find('.rename-message').text(
"Invalid notebook name. Notebook names must "+
"have 1 or more characters and can contain any characters " +
"except :/\\. Please enter a new notebook name:"
);
return false;
} else {
that.notebook.rename(new_name);
var new_name = d.find('input').val();
if (!options.notebook.test_notebook_name(new_name)) {
d.find('.rename-message').text(
"Invalid notebook name. Notebook names must "+
"have 1 or more characters and can contain any characters " +
"except :/\\. Please enter a new notebook name:"
);
return false;
} else {
d.find('.rename-message').text("Renaming...");
d.find('input[type="text"]').prop('disabled', true);
that.notebook.rename(new_name).then(
function () {
d.modal('hide');
}, function (error) {
d.find('.rename-message').text(error.message || 'Unknown error');
d.find('input[type="text"]').prop('disabled', false).focus().select();
}
);
return false;
}
}
}}
},
open : function (event, ui) {
var that = $(this);
// Upon ENTER, click the OK button.
that.find('input[type="text"]').keydown(function (event, ui) {
"Cancel": {}
},
open : function () {
/**
* Upon ENTER, click the OK button.
*/
d.find('input[type="text"]').keydown(function (event) {
if (event.which === keyboard.keycodes.enter) {
that.find('.btn-primary').first().click();
d.find('.btn-primary').first().click();
return false;
}
});
that.find('input[type="text"]').focus().select();
d.find('input[type="text"]').focus().select();
}
});
};
@ -122,14 +141,12 @@ define([
SaveWidget.prototype.update_address_bar = function(){
var base_url = this.notebook.base_url;
var nbname = this.notebook.notebook_name;
var path = this.notebook.notebook_path;
var state = {path : path, name: nbname};
var state = {path : path};
window.history.replaceState(state, "", utils.url_join_encode(
base_url,
"notebooks",
path,
nbname)
path)
);
};
@ -193,13 +210,15 @@ define([
var that = this;
var recall = function(t){
// recall slightly later (1s) as long timeout in js might be imprecise,
// and you want to be call **after** the change of formatting should happend.
/**
* recall slightly later (1s) as long timeout in js might be imprecise,
* and you want to be call **after** the change of formatting should happend.
*/
return setTimeout(
$.proxy(that._regularly_update_checkpoint_date, that),
t + 1000
);
}
};
var tdelta = Math.ceil(new Date()-this._checkpoint_date);
// update regularly for the first 6hours and show

@ -4,47 +4,57 @@ define(['jquery'], function($){
"use strict";
var ScrollManager = function(notebook, options) {
// Public constructor.
/**
* Public constructor.
*/
this.notebook = notebook;
options = options || {};
this.animation_speed = options.animation_speed || 250; //ms
};
ScrollManager.prototype.scroll = function (delta) {
// Scroll the document.
//
// Parameters
// ----------
// delta: integer
// direction to scroll the document. Positive is downwards.
// Unit is one page length.
/**
* Scroll the document.
*
* Parameters
* ----------
* delta: integer
* direction to scroll the document. Positive is downwards.
* Unit is one page length.
*/
this.scroll_some(delta);
return false;
};
ScrollManager.prototype.scroll_to = function(selector) {
// Scroll to an element in the notebook.
/**
* Scroll to an element in the notebook.
*/
$('#notebook').animate({'scrollTop': $(selector).offset().top + $('#notebook').scrollTop() - $('#notebook').offset().top}, this.animation_speed);
};
ScrollManager.prototype.scroll_some = function(pages) {
// Scroll up or down a given number of pages.
//
// Parameters
// ----------
// pages: integer
// number of pages to scroll the document, may be positive or negative.
/**
* Scroll up or down a given number of pages.
*
* Parameters
* ----------
* pages: integer
* number of pages to scroll the document, may be positive or negative.
*/
$('#notebook').animate({'scrollTop': $('#notebook').scrollTop() + pages * $('#notebook').height()}, this.animation_speed);
};
ScrollManager.prototype.get_first_visible_cell = function() {
// Gets the index of the first visible cell in the document.
// First, attempt to be smart by guessing the index of the cell we are
// scrolled to. Then, walk from there up or down until the right cell
// is found. To guess the index, get the top of the last cell, and
// divide that by the number of cells to get an average cell height.
// Then divide the scroll height by the average cell height.
/**
* Gets the index of the first visible cell in the document.
*
* First, attempt to be smart by guessing the index of the cell we are
* scrolled to. Then, walk from there up or down until the right cell
* is found. To guess the index, get the top of the last cell, and
* divide that by the number of cells to get an average cell height.
* Then divide the scroll height by the average cell height.
*/
var cell_count = this.notebook.ncells();
var first_cell_top = this.notebook.get_cell(0).element.offset().top;
var last_cell_top = this.notebook.get_cell(cell_count-1).element.offset().top;
@ -65,34 +75,40 @@ define(['jquery'], function($){
var TargetScrollManager = function(notebook, options) {
// Public constructor.
/**
* Public constructor.
*/
ScrollManager.apply(this, [notebook, options]);
};
TargetScrollManager.prototype = new ScrollManager();
TargetScrollManager.prototype = Object.create(ScrollManager.prototype);
TargetScrollManager.prototype.is_target = function (index) {
// Check if a cell should be a scroll stop.
//
// Returns `true` if the cell is a cell that the scroll manager
// should scroll to. Otherwise, false is returned.
//
// Parameters
// ----------
// index: integer
// index of the cell to test.
/**
* Check if a cell should be a scroll stop.
*
* Returns `true` if the cell is a cell that the scroll manager
* should scroll to. Otherwise, false is returned.
*
* Parameters
* ----------
* index: integer
* index of the cell to test.
*/
return false;
};
TargetScrollManager.prototype.scroll = function (delta) {
// Scroll the document.
//
// Parameters
// ----------
// delta: integer
// direction to scroll the document. Positive is downwards.
// Units are targets.
// Try to scroll to the next slide.
/**
* Scroll the document.
*
* Parameters
* ----------
* delta: integer
* direction to scroll the document. Positive is downwards.
* Units are targets.
*
* Try to scroll to the next slide.
*/
var cell_count = this.notebook.ncells();
var selected_index = this.get_first_visible_cell() + delta;
while (0 <= selected_index && selected_index < cell_count && !this.is_target(selected_index)) {
@ -111,10 +127,12 @@ define(['jquery'], function($){
var SlideScrollManager = function(notebook, options) {
// Public constructor.
/**
* Public constructor.
*/
TargetScrollManager.apply(this, [notebook, options]);
};
SlideScrollManager.prototype = new TargetScrollManager();
SlideScrollManager.prototype = Object.create(TargetScrollManager.prototype);
SlideScrollManager.prototype.is_target = function (index) {
var cell = this.notebook.get_cell(index);
@ -126,24 +144,28 @@ define(['jquery'], function($){
var HeadingScrollManager = function(notebook, options) {
// Public constructor.
/**
* Public constructor.
*/
ScrollManager.apply(this, [notebook, options]);
options = options || {};
this._level = options.heading_level || 1;
};
HeadingScrollManager.prototype = new ScrollManager();
HeadingScrollManager.prototype = Object.create(ScrollManager.prototype)
HeadingScrollManager.prototype.scroll = function (delta) {
// Scroll the document.
//
// Parameters
// ----------
// delta: integer
// direction to scroll the document. Positive is downwards.
// Units are headers.
// Get all of the header elements that match the heading level or are of
// greater magnitude (a smaller header number).
/**
* Scroll the document.
*
* Parameters
* ----------
* delta: integer
* direction to scroll the document. Positive is downwards.
* Units are headers.
*
* Get all of the header elements that match the heading level or are of
* greater magnitude (a smaller header number).
*/
var headers = $();
var i;
for (i = 1; i <= this._level; i++) {

@ -10,23 +10,28 @@ define([
'notebook/js/mathjaxutils',
'notebook/js/celltoolbar',
'components/marked/lib/marked',
], function(IPython, utils, $, cell, security, mathjaxutils, celltoolbar, marked) {
'codemirror/lib/codemirror',
'codemirror/mode/gfm/gfm',
'notebook/js/codemirror-ipythongfm'
], function(IPython,utils , $, cell, security, mathjaxutils, celltoolbar, marked, CodeMirror, gfm, ipgfm) {
"use strict";
var Cell = cell.Cell;
var TextCell = function (options) {
// Constructor
//
// Construct a new TextCell, codemirror mode is by default 'htmlmixed',
// and cell type is 'text' cell start as not redered.
//
// Parameters:
// options: dictionary
// Dictionary of keyword arguments.
// events: $(Events) instance
// config: dictionary
// keyboard_manager: KeyboardManager instance
// notebook: Notebook instance
/**
* Constructor
*
* Construct a new TextCell, codemirror mode is by default 'htmlmixed',
* and cell type is 'text' cell start as not redered.
*
* Parameters:
* options: dictionary
* Dictionary of keyword arguments.
* events: $(Events) instance
* config: dictionary
* keyboard_manager: KeyboardManager instance
* notebook: Notebook instance
*/
options = options || {};
// in all TextCell/Cell subclasses
@ -38,10 +43,7 @@ define([
this.config = options.config;
// we cannot put this as a class key as it has handle to "this".
var cm_overwrite_options = {
onKeyEvent: $.proxy(this.handle_keyevent,this)
};
var config = utils.mergeopt(TextCell, this.config, {cm_config:cm_overwrite_options});
var config = utils.mergeopt(TextCell, this.config);
Cell.apply(this, [{
config: config,
keyboard_manager: options.keyboard_manager,
@ -52,7 +54,7 @@ define([
this.rendered = false;
};
TextCell.prototype = new Cell();
TextCell.prototype = Object.create(Cell.prototype);
TextCell.options_default = {
cm_config : {
@ -83,6 +85,7 @@ define([
inner_cell.append(this.celltoolbar.element);
var input_area = $('<div/>').addClass('input_area');
this.code_mirror = new CodeMirror(input_area.get(0), this.cm_config);
this.code_mirror.on('keydown', $.proxy(this.handle_keyevent,this))
// The tabindex=-1 makes this div focusable.
var render_area = $('<div/>').addClass('text_cell_render rendered_html')
.attr('tabindex','-1');
@ -92,27 +95,6 @@ define([
};
/**
* Bind the DOM evet to cell actions
* Need to be called after TextCell.create_element
* @private
* @method bind_event
*/
TextCell.prototype.bind_events = function () {
Cell.prototype.bind_events.apply(this);
var that = this;
this.element.dblclick(function () {
if (that.selected === false) {
this.events.trigger('select.Cell', {'cell':that});
}
var cont = that.unrender();
if (cont) {
that.focus_editor();
}
});
};
// Cell level actions
TextCell.prototype.select = function () {
@ -131,8 +113,6 @@ define([
if (cont) {
var text_cell = this.element;
var output = text_cell.find("div.text_cell_render");
output.hide();
text_cell.find('div.input_area').show();
if (this.get_text() === this.placeholder) {
this.set_text('');
}
@ -217,15 +197,17 @@ define([
var MarkdownCell = function (options) {
// Constructor
//
// Parameters:
// options: dictionary
// Dictionary of keyword arguments.
// events: $(Events) instance
// config: dictionary
// keyboard_manager: KeyboardManager instance
// notebook: Notebook instance
/**
* Constructor
*
* Parameters:
* options: dictionary
* Dictionary of keyword arguments.
* events: $(Events) instance
* config: dictionary
* keyboard_manager: KeyboardManager instance
* notebook: Notebook instance
*/
options = options || {};
var config = utils.mergeopt(MarkdownCell, options.config);
TextCell.apply(this, [$.extend({}, options, {config: config})]);
@ -240,7 +222,22 @@ define([
placeholder: "Type *Markdown* and LaTeX: $\\alpha^2$"
};
MarkdownCell.prototype = new TextCell();
MarkdownCell.prototype = Object.create(TextCell.prototype);
MarkdownCell.prototype.set_heading_level = function (level) {
/**
* make a markdown cell a heading
*/
level = level || 1;
var source = this.get_text();
source = source.replace(/^(#*)\s?/,
new Array(level + 1).join('#') + ' ');
this.set_text(source);
this.refresh();
if (this.rendered) {
this.render();
}
};
/**
* @method render
@ -248,43 +245,56 @@ define([
MarkdownCell.prototype.render = function () {
var cont = TextCell.prototype.render.apply(this);
if (cont) {
var that = this;
var text = this.get_text();
var math = null;
if (text === "") { text = this.placeholder; }
var text_and_math = mathjaxutils.remove_math(text);
text = text_and_math[0];
math = text_and_math[1];
var html = marked.parser(marked.lexer(text));
html = mathjaxutils.replace_math(html, math);
html = security.sanitize_html(html);
html = $($.parseHTML(html));
// links in markdown cells should open in new tabs
html.find("a[href]").not('[href^="#"]').attr("target", "_blank");
this.set_rendered(html);
this.element.find('div.input_area').hide();
this.element.find("div.text_cell_render").show();
this.typeset();
marked(text, function (err, html) {
html = mathjaxutils.replace_math(html, math);
html = security.sanitize_html(html);
html = $($.parseHTML(html));
// add anchors to headings
html.find(":header").addBack(":header").each(function (i, h) {
h = $(h);
var hash = h.text().replace(/ /g, '-');
h.attr('id', hash);
h.append(
$('<a/>')
.addClass('anchor-link')
.attr('href', '#' + hash)
.text('¶')
);
});
// links in markdown cells should open in new tabs
html.find("a[href]").not('[href^="#"]').attr("target", "_blank");
that.set_rendered(html);
that.typeset();
that.events.trigger("rendered.MarkdownCell", {cell: that});
});
}
return cont;
};
var RawCell = function (options) {
// Constructor
//
// Parameters:
// options: dictionary
// Dictionary of keyword arguments.
// events: $(Events) instance
// config: dictionary
// keyboard_manager: KeyboardManager instance
// notebook: Notebook instance
/**
* Constructor
*
* Parameters:
* options: dictionary
* Dictionary of keyword arguments.
* events: $(Events) instance
* config: dictionary
* keyboard_manager: KeyboardManager instance
* notebook: Notebook instance
*/
options = options || {};
var config = utils.mergeopt(RawCell, options.config);
TextCell.apply(this, [$.extend({}, options, {config: config})]);
// RawCell should always hide its rendered div
this.element.find('div.text_cell_render').hide();
this.cell_type = 'raw';
};
@ -294,7 +304,7 @@ define([
"When passing through nbconvert, a Raw Cell's content is added to the output unmodified."
};
RawCell.prototype = new TextCell();
RawCell.prototype = Object.create(TextCell.prototype);
/** @method bind_events **/
RawCell.prototype.bind_events = function () {
@ -328,123 +338,15 @@ define([
return cont;
};
var HeadingCell = function (options) {
// Constructor
//
// Parameters:
// options: dictionary
// Dictionary of keyword arguments.
// events: $(Events) instance
// config: dictionary
// keyboard_manager: KeyboardManager instance
// notebook: Notebook instance
options = options || {};
var config = utils.mergeopt(HeadingCell, options.config);
TextCell.apply(this, [$.extend({}, options, {config: config})]);
this.level = 1;
this.cell_type = 'heading';
};
HeadingCell.options_default = {
cm_config: {
theme: 'heading-1'
},
placeholder: "Type Heading Here"
};
HeadingCell.prototype = new TextCell();
/** @method fromJSON */
HeadingCell.prototype.fromJSON = function (data) {
if (data.level !== undefined){
this.level = data.level;
}
TextCell.prototype.fromJSON.apply(this, arguments);
this.code_mirror.setOption("theme", "heading-"+this.level);
};
/** @method toJSON */
HeadingCell.prototype.toJSON = function () {
var data = TextCell.prototype.toJSON.apply(this);
data.level = this.get_level();
return data;
};
/**
* Change heading level of cell, and re-render
* @method set_level
*/
HeadingCell.prototype.set_level = function (level) {
this.level = level;
this.code_mirror.setOption("theme", "heading-"+level);
if (this.rendered) {
this.rendered = false;
this.render();
}
};
/** The depth of header cell, based on html (h1 to h6)
* @method get_level
* @return {integer} level - for 1 to 6
*/
HeadingCell.prototype.get_level = function () {
return this.level;
};
HeadingCell.prototype.get_rendered = function () {
var r = this.element.find("div.text_cell_render");
return r.children().first().html();
};
HeadingCell.prototype.render = function () {
var cont = TextCell.prototype.render.apply(this);
if (cont) {
var text = this.get_text();
var math = null;
// Markdown headings must be a single line
text = text.replace(/\n/g, ' ');
if (text === "") { text = this.placeholder; }
text = new Array(this.level + 1).join("#") + " " + text;
var text_and_math = mathjaxutils.remove_math(text);
text = text_and_math[0];
math = text_and_math[1];
var html = marked.parser(marked.lexer(text));
html = mathjaxutils.replace_math(html, math);
html = security.sanitize_html(html);
var h = $($.parseHTML(html));
// add id and linkback anchor
var hash = h.text().trim().replace(/ /g, '-');
h.attr('id', hash);
h.append(
$('<a/>')
.addClass('anchor-link')
.attr('href', '#' + hash)
.text('¶')
);
this.set_rendered(h);
this.element.find('div.input_area').hide();
this.element.find("div.text_cell_render").show();
this.typeset();
}
return cont;
};
// Backwards compatability.
IPython.TextCell = TextCell;
IPython.MarkdownCell = MarkdownCell;
IPython.RawCell = RawCell;
IPython.HeadingCell = HeadingCell;
var textcell = {
'TextCell': TextCell,
'MarkdownCell': MarkdownCell,
'RawCell': RawCell,
'HeadingCell': HeadingCell,
TextCell: TextCell,
MarkdownCell: MarkdownCell,
RawCell: RawCell
};
return textcell;
});

@ -116,12 +116,10 @@ define([
};
Tooltip.prototype.showInPager = function (cell) {
// reexecute last call in pager by appending ? to show back in pager
var that = this;
var payload = {};
payload.text = that._reply.content.data['text/plain'];
this.events.trigger('open_with_text.Pager', payload);
/**
* reexecute last call in pager by appending ? to show back in pager
*/
this.events.trigger('open_with_text.Pager', this._reply.content);
this.remove_and_cancel_tooltip();
};
@ -148,9 +146,11 @@ define([
// return true on successfully removing a visible tooltip; otherwise return
// false.
Tooltip.prototype.remove_and_cancel_tooltip = function (force) {
// note that we don't handle closing directly inside the calltip
// as in the completer, because it is not focusable, so won't
// get the event.
/**
* note that we don't handle closing directly inside the calltip
* as in the completer, because it is not focusable, so won't
* get the event.
*/
this.cancel_pending();
if (!this._hidden) {
if (force || !this._sticky) {
@ -183,39 +183,18 @@ define([
// easy access for julia monkey patching.
Tooltip.last_token_re = /[a-z_][0-9a-z._]*$/gi;
Tooltip.prototype.extract_oir_token = function(line){
// use internally just to make the request to the kernel
// Feel free to shorten this logic if you are better
// than me in regEx
// basicaly you shoul be able to get xxx.xxx.xxx from
// something(range(10), kwarg=smth) ; xxx.xxx.xxx( firstarg, rand(234,23), kwarg1=2,
// remove everything between matchin bracket (need to iterate)
var matchBracket = /\([^\(\)]+\)/g;
var endBracket = /\([^\(]*$/g;
var oldline = line;
line = line.replace(matchBracket, "");
while (oldline != line) {
oldline = line;
line = line.replace(matchBracket, "");
}
// remove everything after last open bracket
line = line.replace(endBracket, "");
// reset the regex object
Tooltip.last_token_re.lastIndex = 0;
return Tooltip.last_token_re.exec(line);
};
Tooltip.prototype._request_tooltip = function (cell, text, cursor_pos) {
var callbacks = $.proxy(this._show, this);
var msg_id = cell.kernel.inspect(text, cursor_pos, callbacks);
};
// make an imediate completion request
// make an immediate completion request
Tooltip.prototype.request = function (cell, hide_if_no_docstring) {
// request(codecell)
// Deal with extracting the text from the cell and counting
// call in a row
/**
* request(codecell)
* Deal with extracting the text from the cell and counting
* call in a row
*/
this.cancel_pending();
var editor = cell.code_mirror;
var cursor = editor.getCursor();
@ -225,10 +204,11 @@ define([
this._hide_if_no_docstring = hide_if_no_docstring;
if(editor.somethingSelected()){
// get only the most recent selection.
text = editor.getSelection();
}
// need a permanent handel to code_mirror for future auto recall
// need a permanent handle to code_mirror for future auto recall
this.code_mirror = editor;
// now we treat the different number of keypress
@ -276,8 +256,10 @@ define([
// should be called with the kernel reply to actually show the tooltip
Tooltip.prototype._show = function (reply) {
// move the bubble if it is not hidden
// otherwise fade it
/**
* move the bubble if it is not hidden
* otherwise fade it
*/
this._reply = reply;
var content = reply.content;
if (!content.found) {
@ -328,7 +310,7 @@ define([
this.text.scrollTop(0);
};
// Backwards compatability.
// Backwards compatibility.
IPython.Tooltip = Tooltip;
return {'Tooltip': Tooltip};

@ -11,13 +11,13 @@ define([
var tour_style = "<div class='popover tour'>\n" +
"<div class='arrow'></div>\n" +
"<div style='position:absolute; top:7px; right:7px'>\n" +
"<button class='btn btn-default btn-sm icon-remove' data-role='end'></button>\n" +
"<button class='btn btn-default btn-sm fa fa-times' data-role='end'></button>\n" +
"</div><h3 class='popover-title'></h3>\n" +
"<div class='popover-content'></div>\n" +
"<div class='popover-navigation'>\n" +
"<button class='btn btn-default icon-step-backward' data-role='prev'></button>\n" +
"<button class='btn btn-default icon-step-forward pull-right' data-role='next'></button>\n" +
"<button id='tour-pause' class='btn btn-sm btn-default icon-pause' data-resume-text='' data-pause-text='' data-role='pause-resume'></button>\n" +
"<button class='btn btn-default fa fa-step-backward' data-role='prev'></button>\n" +
"<button class='btn btn-default fa fa-step-forward pull-right' data-role='next'></button>\n" +
"<button id='tour-pause' class='btn btn-sm btn-default fa fa-pause' data-resume-text='' data-pause-text='' data-role='pause-resume'></button>\n" +
"</div>\n" +
"</div>";
@ -31,7 +31,7 @@ define([
title: "Welcome to the Notebook Tour",
placement: 'bottom',
orphan: true,
content: "You can use the left and right arrow keys to go backwards and forwards.",
content: "You can use the left and right arrow keys to go backwards and forwards."
}, {
element: "#notebook_name",
title: "Filename",
@ -91,31 +91,31 @@ define([
element: "#kernel_indicator",
title: "Kernel Indicator",
placement: 'bottom',
onShow: function(tour) { events.trigger('status_idle.Kernel');},
content: "This is the Kernel indicator. It looks like this when the Kernel is idle.",
onShow: function(tour) { events.trigger('kernel_idle.Kernel');},
content: "This is the Kernel indicator. It looks like this when the Kernel is idle."
}, {
element: "#kernel_indicator",
title: "Kernel Indicator",
placement: 'bottom',
onShow: function(tour) { events.trigger('status_busy.Kernel'); },
content: "The Kernel indicator looks like this when the Kernel is busy.",
onShow: function(tour) { events.trigger('kernel_busy.Kernel'); },
content: "The Kernel indicator looks like this when the Kernel is busy."
}, {
element: ".icon-stop",
element: ".fa-stop",
placement: 'bottom',
title: "Interrupting the Kernel",
onHide: function(tour) { events.trigger('status_idle.Kernel'); },
onHide: function(tour) { events.trigger('kernel_idle.Kernel'); },
content: "To cancel a computation in progress, you can click here."
}, {
element: "#notification_kernel",
placement: 'bottom',
onShow: function(tour) { $('.icon-stop').click(); },
onShow: function(tour) { $('.fa-stop').click(); },
title: "Notification Area",
content: "Messages in response to user actions (Save, Interrupt, etc) appear here."
}, {
title: "Fin.",
placement: 'bottom',
orphan: true,
content: "This concludes the IPython Notebook User Interface tour.Tour. Happy hacking!",
content: "This concludes the IPython Notebook User Interface Tour. Happy hacking!"
}
];
@ -156,7 +156,7 @@ define([
};
NotebookTour.prototype.toggle_pause_play = function () {
$('#tour-pause').toggleClass('icon-pause icon-play');
$('#tour-pause').toggleClass('fa-pause fa-play');
};
NotebookTour.prototype.edit_mode = function() {

@ -1,13 +1,12 @@
/* CSS font colors for translated ANSI colors. */
.ansibold {font-weight: bold;}
/* use dark versions for foreground, to improve visibility */
.ansiblack {color: black;}
.ansired {color: darkred;}
.ansigreen {color: darkgreen;}
.ansiyellow {color: brown;}
.ansiyellow {color: #c4a000;}
.ansiblue {color: darkblue;}
.ansipurple {color: darkviolet;}
.ansicyan {color: steelblue;}
@ -22,4 +21,3 @@
.ansibgpurple {background-color: magenta;}
.ansibgcyan {background-color: cyan;}
.ansibggray {background-color: gray;}

@ -61,3 +61,34 @@ div.prompt:empty {
padding-top: 0;
padding-bottom: 0;
}
div.unrecognized_cell {
// from text_cell
padding: 5px 5px 5px 0px;
.hbox();
.inner_cell {
.border-radius(@border-radius-base);
padding: 5px;
font-weight: bold;
color: red;
border: 1px solid @light_border_color;
background: darken(@cell_background, 5%);
// remove decoration from link
a {
color: inherit;
text-decoration: none;
&:hover {
color: inherit;
text-decoration: none;
}
}
}
}
@media (max-width: 480px) {
// remove prompt indentation on small screens
div.unrecognized_cell > div.prompt {
display: none;
}
}

@ -5,13 +5,13 @@
border: thin solid #CFCFCF;
border-bottom: none;
background : #EEE;
border-radius : 3px 3px 0px 0px;
border-radius : @border-radius-base @border-radius-base 0px 0px;
width:100%;
-webkit-box-pack: end;
height: @celltoolbar-height;
padding-right: 4px;
.hbox();
.reverse();
.end();
}
.ctb_hideshow {

@ -8,8 +8,14 @@
border-top: 1px;
border-radius: 0px 0px @border-radius-base @border-radius-base;
}
.navbar-toggle {
float: left;
}
.navbar-collapse {
clear: left;
}
li.dropdown {
line-height: 12px;
@ -21,6 +27,7 @@
ul.navbar-right {
padding-top: 2px;
margin-bottom: 0px;
}
}

@ -172,3 +172,19 @@ input.raw_input:focus {
p.p-space {
margin-bottom: 10px;
}
div.output_unrecognized {
padding: 5px;
font-weight: bold;
color: red;
// remove decoration from link
a {
color: inherit;
text-decoration: none;
&:hover {
color: inherit;
text-decoration: none;
}
}
}

@ -0,0 +1,17 @@
.terminal {
float: left;
border: black solid 5px;
font-family: "DejaVu Sans Mono", "Liberation Mono", monospace;
font-size: 11px;
color: white;
background: black;
}
.terminal-cursor {
color: black;
background: white;
}
#terminado-container {
margin: 8px;
}

@ -32,29 +32,33 @@ h1,h2,h3,h4,h5,h6 {
}
}
div.cell.text_cell.rendered {
padding: 0px;
.text_cell.rendered .input_area {
display: none;
}
.cm-s-heading-1,
.cm-s-heading-2,
.cm-s-heading-3,
.cm-s-heading-4,
.cm-s-heading-5,
.cm-s-heading-6 {
.text_cell.unrendered .text_cell_render {
display:none;
}
.cm-header-1,
.cm-header-2,
.cm-header-3,
.cm-header-4,
.cm-header-5,
.cm-header-6 {
font-weight: bold;
font-family: @font-family-sans-serif;
}
.cm-s-heading-1 { font-size:150%; }
.cm-s-heading-2 { font-size: 130%; }
.cm-s-heading-3 { font-size: 120%; }
.cm-s-heading-4 { font-size: 110%; }
.cm-s-heading-5 {
font-size: 100%;
.cm-header-1 { font-size: 185.7%; }
.cm-header-2 { font-size: 157.1%; }
.cm-header-3 { font-size: 128.6%; }
.cm-header-4 { font-size: 110%; }
.cm-header-5 {
font-size: 100%;
font-style: italic;
}
.cm-s-heading-6 {
font-size: 90%;
.cm-header-6 {
font-size: 100%;
font-style: italic;
}

@ -0,0 +1,67 @@
// Copyright (c) IPython Development Team.
// Distributed under the terms of the Modified BSD License.
define([
'jquery',
'base/js/utils',
],
function($, utils) {
var ConfigSection = function(section_name, options) {
this.section_name = section_name;
this.base_url = options.base_url;
this.data = {};
var that = this;
/* .loaded is a promise, fulfilled the first time the config is loaded
* from the server. Code can do:
* conf.loaded.then(function() { ... using conf.data ... });
*/
this._one_load_finished = false;
this.loaded = new Promise(function(resolve, reject) {
that._finish_firstload = resolve;
});
};
ConfigSection.prototype.api_url = function() {
return utils.url_join_encode(this.base_url, 'api/config', this.section_name);
};
ConfigSection.prototype._load_done = function() {
if (!this._one_load_finished) {
this._one_load_finished = true;
this._finish_firstload();
}
};
ConfigSection.prototype.load = function() {
var that = this;
return utils.promising_ajax(this.api_url(), {
cache: false,
type: "GET",
dataType: "json",
}).then(function(data) {
that.data = data;
that._load_done();
return data;
});
};
ConfigSection.prototype.update = function(newdata) {
var that = this;
return utils.promising_ajax(this.api_url(), {
processData: false,
type : "PATCH",
data: JSON.stringify(newdata),
dataType : "json",
contentType: 'application/json',
}).then(function(data) {
that.data = data;
that._load_done();
return data;
});
};
return {ConfigSection: ConfigSection};
});

@ -0,0 +1,250 @@
// Copyright (c) IPython Development Team.
// Distributed under the terms of the Modified BSD License.
define([
'base/js/namespace',
'jquery',
'base/js/utils',
], function(IPython, $, utils) {
var Contents = function(options) {
/**
* Constructor
*
* A contents handles passing file operations
* to the back-end. This includes checkpointing
* with the normal file operations.
*
* Parameters:
* options: dictionary
* Dictionary of keyword arguments.
* base_url: string
*/
this.base_url = options.base_url;
};
/** Error type */
Contents.DIRECTORY_NOT_EMPTY_ERROR = 'DirectoryNotEmptyError';
Contents.DirectoryNotEmptyError = function() {
// Constructor
//
// An error representing the result of attempting to delete a non-empty
// directory.
this.message = 'A directory must be empty before being deleted.';
};
Contents.DirectoryNotEmptyError.prototype = Object.create(Error.prototype);
Contents.DirectoryNotEmptyError.prototype.name =
Contents.DIRECTORY_NOT_EMPTY_ERROR;
Contents.prototype.api_url = function() {
var url_parts = [this.base_url, 'api/contents'].concat(
Array.prototype.slice.apply(arguments));
return utils.url_join_encode.apply(null, url_parts);
};
/**
* Creates a basic error handler that wraps a jqXHR error as an Error.
*
* Takes a callback that accepts an Error, and returns a callback that can
* be passed directly to $.ajax, which will wrap the error from jQuery
* as an Error, and pass that to the original callback.
*
* @method create_basic_error_handler
* @param{Function} callback
* @return{Function}
*/
Contents.prototype.create_basic_error_handler = function(callback) {
if (!callback) {
return utils.log_ajax_error;
}
return function(xhr, status, error) {
callback(utils.wrap_ajax_error(xhr, status, error));
};
};
/**
* File Functions (including notebook operations)
*/
/**
* Get a file.
*
* Calls success with file JSON model, or error with error.
*
* @method get
* @param {String} path
* @param {Object} options
* type : 'notebook', 'file', or 'directory'
* format: 'text' or 'base64'; only relevant for type: 'file'
*/
Contents.prototype.get = function (path, options) {
/**
* We do the call with settings so we can set cache to false.
*/
var settings = {
processData : false,
cache : false,
type : "GET",
dataType : "json",
};
var url = this.api_url(path);
params = {};
if (options.type) { params.type = options.type; }
if (options.format) { params.format = options.format; }
return utils.promising_ajax(url + '?' + $.param(params), settings);
};
/**
* Creates a new untitled file or directory in the specified directory path.
*
* @method new
* @param {String} path: the directory in which to create the new file/directory
* @param {Object} options:
* ext: file extension to use
* type: model type to create ('notebook', 'file', or 'directory')
*/
Contents.prototype.new_untitled = function(path, options) {
var data = JSON.stringify({
ext: options.ext,
type: options.type
});
var settings = {
processData : false,
type : "POST",
data: data,
dataType : "json",
};
return utils.promising_ajax(this.api_url(path), settings);
};
Contents.prototype.delete = function(path) {
var settings = {
processData : false,
type : "DELETE",
dataType : "json",
};
var url = this.api_url(path);
return utils.promising_ajax(url, settings).catch(
// Translate certain errors to more specific ones.
function(error) {
// TODO: update IPEP27 to specify errors more precisely, so
// that error types can be detected here with certainty.
if (error.xhr.status === 400) {
throw new Contents.DirectoryNotEmptyError();
}
throw error;
}
);
};
Contents.prototype.rename = function(path, new_path) {
var data = {path: new_path};
var settings = {
processData : false,
type : "PATCH",
data : JSON.stringify(data),
dataType: "json",
contentType: 'application/json',
};
var url = this.api_url(path);
return utils.promising_ajax(url, settings);
};
Contents.prototype.save = function(path, model) {
/**
* We do the call with settings so we can set cache to false.
*/
var settings = {
processData : false,
type : "PUT",
data : JSON.stringify(model),
contentType: 'application/json',
};
var url = this.api_url(path);
return utils.promising_ajax(url, settings);
};
Contents.prototype.copy = function(from_file, to_dir) {
/**
* Copy a file into a given directory via POST
* The server will select the name of the copied file
*/
var url = this.api_url(to_dir);
var settings = {
processData : false,
type: "POST",
data: JSON.stringify({copy_from: from_file}),
dataType : "json",
};
return utils.promising_ajax(url, settings);
};
/**
* Checkpointing Functions
*/
Contents.prototype.create_checkpoint = function(path) {
var url = this.api_url(path, 'checkpoints');
var settings = {
type : "POST",
dataType : "json",
};
return utils.promising_ajax(url, settings);
};
Contents.prototype.list_checkpoints = function(path) {
var url = this.api_url(path, 'checkpoints');
var settings = {
type : "GET",
cache: false,
dataType: "json",
};
return utils.promising_ajax(url, settings);
};
Contents.prototype.restore_checkpoint = function(path, checkpoint_id) {
var url = this.api_url(path, 'checkpoints', checkpoint_id);
var settings = {
type : "POST",
};
return utils.promising_ajax(url, settings);
};
Contents.prototype.delete_checkpoint = function(path, checkpoint_id) {
var url = this.api_url(path, 'checkpoints', checkpoint_id);
var settings = {
type : "DELETE",
};
return utils.promising_ajax(url, settings);
};
/**
* File management functions
*/
/**
* List notebooks and directories at a given path
*
* On success, load_callback is called with an array of dictionaries
* representing individual files or directories. Each dictionary has
* the keys:
* type: "notebook" or "directory"
* created: created date
* last_modified: last modified dat
* @method list_notebooks
* @param {String} path The path to list notebooks in
*/
Contents.prototype.list_contents = function(path) {
return this.get(path, {type: 'directory'});
};
IPython.Contents = Contents;
return {'Contents': Contents};
});

@ -21,7 +21,9 @@ define([
};
CommManager.prototype.init_kernel = function (kernel) {
// connect the kernel, and register message handlers
/**
* connect the kernel, and register message handlers
*/
this.kernel = kernel;
var msg_types = ['comm_open', 'comm_msg', 'comm_close'];
for (var i = 0; i < msg_types.length; i++) {
@ -31,8 +33,10 @@ define([
};
CommManager.prototype.new_comm = function (target_name, data, callbacks, metadata) {
// Create a new Comm, register it, and open its Kernel-side counterpart
// Mimics the auto-registration in `Comm.__init__` in the IPython Comm
/**
* Create a new Comm, register it, and open its Kernel-side counterpart
* Mimics the auto-registration in `Comm.__init__` in the IPython Comm
*/
var comm = new Comm(target_name);
this.register_comm(comm);
comm.open(data, callbacks, metadata);
@ -40,24 +44,32 @@ define([
};
CommManager.prototype.register_target = function (target_name, f) {
// Register a target function for a given target name
/**
* Register a target function for a given target name
*/
this.targets[target_name] = f;
};
CommManager.prototype.unregister_target = function (target_name, f) {
// Unregister a target function for a given target name
/**
* Unregister a target function for a given target name
*/
delete this.targets[target_name];
};
CommManager.prototype.register_comm = function (comm) {
// Register a comm in the mapping
this.comms[comm.comm_id] = comm;
/**
* Register a comm in the mapping
*/
this.comms[comm.comm_id] = Promise.resolve(comm);
comm.kernel = this.kernel;
return comm.comm_id;
};
CommManager.prototype.unregister_comm = function (comm) {
// Remove a comm from the mapping
/**
* Remove a comm from the mapping
*/
delete this.comms[comm.comm_id];
};
@ -65,48 +77,63 @@ define([
CommManager.prototype.comm_open = function (msg) {
var content = msg.content;
var f = this.targets[content.target_name];
if (f === undefined) {
console.log("No such target registered: ", content.target_name);
console.log("Available targets are: ", this.targets);
return;
}
var comm = new Comm(content.target_name, content.comm_id);
this.register_comm(comm);
try {
f(comm, msg);
} catch (e) {
console.log("Exception opening new comm:", e, e.stack, msg);
comm.close();
this.unregister_comm(comm);
}
};
CommManager.prototype.comm_close = function (msg) {
var that = this;
var comm_id = content.comm_id;
this.comms[comm_id] = utils.load_class(content.target_name, content.target_module,
this.targets).then(function(target) {
var comm = new Comm(content.target_name, comm_id);
comm.kernel = that.kernel;
try {
var response = target(comm, msg);
} catch (e) {
comm.close();
that.unregister_comm(comm);
var wrapped_error = new utils.WrappedError("Exception opening new comm", e);
console.error(wrapped_error);
return Promise.reject(wrapped_error);
}
// Regardless of the target return value, we need to
// then return the comm
return Promise.resolve(response).then(function() {return comm;});
}, utils.reject('Could not open comm', true));
return this.comms[comm_id];
};
CommManager.prototype.comm_close = function(msg) {
var content = msg.content;
var comm = this.comms[content.comm_id];
if (comm === undefined) {
if (this.comms[content.comm_id] === undefined) {
console.error('Comm promise not found for comm id ' + content.comm_id);
return;
}
this.unregister_comm(comm);
try {
comm.handle_close(msg);
} catch (e) {
console.log("Exception closing comm: ", e, e.stack, msg);
}
this.comms[content.comm_id] = this.comms[content.comm_id].then(function(comm) {
this.unregister_comm(comm);
try {
comm.handle_close(msg);
} catch (e) {
console.log("Exception closing comm: ", e, e.stack, msg);
}
// don't return a comm, so that further .then() functions
// get an undefined comm input
});
};
CommManager.prototype.comm_msg = function (msg) {
CommManager.prototype.comm_msg = function(msg) {
var content = msg.content;
var comm = this.comms[content.comm_id];
if (comm === undefined) {
if (this.comms[content.comm_id] === undefined) {
console.error('Comm promise not found for comm id ' + content.comm_id);
return;
}
try {
comm.handle_msg(msg);
} catch (e) {
console.log("Exception handling comm msg: ", e, e.stack, msg);
}
this.comms[content.comm_id] = this.comms[content.comm_id].then(function(comm) {
try {
comm.handle_msg(msg);
} catch (e) {
console.log("Exception handling comm msg: ", e, e.stack, msg);
}
return comm;
});
};
//-----------------------------------------------------------------------
@ -129,12 +156,12 @@ define([
return this.kernel.send_shell_message("comm_open", content, callbacks, metadata);
};
Comm.prototype.send = function (data, callbacks, metadata) {
Comm.prototype.send = function (data, callbacks, metadata, buffers) {
var content = {
comm_id : this.comm_id,
data : data || {},
};
return this.kernel.send_shell_message("comm_msg", content, callbacks, metadata);
return this.kernel.send_shell_message("comm_msg", content, callbacks, metadata, buffers);
};
Comm.prototype.close = function (data, callbacks, metadata) {
@ -160,7 +187,7 @@ define([
// methods for handling incoming messages
Comm.prototype._maybe_callback = function (key, msg) {
Comm.prototype._callback = function (key, msg) {
var callback = this['_' + key + '_callback'];
if (callback) {
try {
@ -172,11 +199,11 @@ define([
};
Comm.prototype.handle_msg = function (msg) {
this._maybe_callback('msg', msg);
this._callback('msg', msg);
};
Comm.prototype.handle_close = function (msg) {
this._maybe_callback('close', msg);
this._callback('close', msg);
};
// For backwards compatability.

@ -1,626 +0,0 @@
// Copyright (c) IPython Development Team.
// Distributed under the terms of the Modified BSD License.
define([
'base/js/namespace',
'jquery',
'base/js/utils',
'services/kernels/js/comm',
'widgets/js/init',
], function(IPython, $, utils, comm, widgetmanager) {
"use strict";
// Initialization and connection.
/**
* A Kernel Class to communicate with the Python kernel
* @Class Kernel
*/
var Kernel = function (kernel_service_url, ws_url, notebook, name) {
this.events = notebook.events;
this.kernel_id = null;
this.shell_channel = null;
this.iopub_channel = null;
this.stdin_channel = null;
this.kernel_service_url = kernel_service_url;
this.name = name;
this.ws_url = ws_url || IPython.utils.get_body_data("wsUrl");
if (!this.ws_url) {
// trailing 's' in https will become wss for secure web sockets
this.ws_url = location.protocol.replace('http', 'ws') + "//" + location.host;
}
this.running = false;
this.username = "username";
this.session_id = utils.uuid();
this._msg_callbacks = {};
this.post = $.post;
if (typeof(WebSocket) !== 'undefined') {
this.WebSocket = WebSocket;
} else if (typeof(MozWebSocket) !== 'undefined') {
this.WebSocket = MozWebSocket;
} else {
alert('Your browser does not have WebSocket support, please try Chrome, Safari or Firefox ≥ 6. Firefox 4 and 5 are also supported by you have to enable WebSockets in about:config.');
}
this.bind_events();
this.init_iopub_handlers();
this.comm_manager = new comm.CommManager(this);
this.widget_manager = new widgetmanager.WidgetManager(this.comm_manager, notebook);
this.last_msg_id = null;
this.last_msg_callbacks = {};
};
Kernel.prototype._get_msg = function (msg_type, content, metadata) {
var msg = {
header : {
msg_id : utils.uuid(),
username : this.username,
session : this.session_id,
msg_type : msg_type,
version : "5.0"
},
metadata : metadata || {},
content : content,
parent_header : {}
};
return msg;
};
Kernel.prototype.bind_events = function () {
var that = this;
this.events.on('send_input_reply.Kernel', function(evt, data) {
that.send_input_reply(data);
});
};
// Initialize the iopub handlers
Kernel.prototype.init_iopub_handlers = function () {
var output_msg_types = ['stream', 'display_data', 'execute_result', 'error'];
this._iopub_handlers = {};
this.register_iopub_handler('status', $.proxy(this._handle_status_message, this));
this.register_iopub_handler('clear_output', $.proxy(this._handle_clear_output, this));
for (var i=0; i < output_msg_types.length; i++) {
this.register_iopub_handler(output_msg_types[i], $.proxy(this._handle_output_message, this));
}
};
/**
* Start the Python kernel
* @method start
*/
Kernel.prototype.start = function (params) {
params = params || {};
if (!this.running) {
var qs = $.param(params);
this.post(utils.url_join_encode(this.kernel_service_url) + '?' + qs,
$.proxy(this._kernel_started, this),
'json'
);
}
};
/**
* Restart the python kernel.
*
* Emit a 'status_restarting.Kernel' event with
* the current object as parameter
*
* @method restart
*/
Kernel.prototype.restart = function () {
this.events.trigger('status_restarting.Kernel', {kernel: this});
if (this.running) {
this.stop_channels();
this.post(utils.url_join_encode(this.kernel_url, "restart"),
$.proxy(this._kernel_started, this),
'json'
);
}
};
Kernel.prototype._kernel_started = function (json) {
console.log("Kernel started: ", json.id);
this.running = true;
this.kernel_id = json.id;
this.kernel_url = utils.url_path_join(this.kernel_service_url, this.kernel_id);
this.start_channels();
};
Kernel.prototype._websocket_closed = function(ws_url, early) {
this.stop_channels();
this.events.trigger('websocket_closed.Kernel',
{ws_url: ws_url, kernel: this, early: early}
);
};
/**
* Start the `shell`and `iopub` channels.
* Will stop and restart them if they already exist.
*
* @method start_channels
*/
Kernel.prototype.start_channels = function () {
var that = this;
this.stop_channels();
var ws_host_url = this.ws_url + this.kernel_url;
console.log("Starting WebSockets:", ws_host_url);
this.shell_channel = new this.WebSocket(
this.ws_url + utils.url_join_encode(this.kernel_url, "shell")
);
this.stdin_channel = new this.WebSocket(
this.ws_url + utils.url_join_encode(this.kernel_url, "stdin")
);
this.iopub_channel = new this.WebSocket(
this.ws_url + utils.url_join_encode(this.kernel_url, "iopub")
);
var already_called_onclose = false; // only alert once
var ws_closed_early = function(evt){
if (already_called_onclose){
return;
}
already_called_onclose = true;
if ( ! evt.wasClean ){
that._websocket_closed(ws_host_url, true);
}
};
var ws_closed_late = function(evt){
if (already_called_onclose){
return;
}
already_called_onclose = true;
if ( ! evt.wasClean ){
that._websocket_closed(ws_host_url, false);
}
};
var ws_error = function(evt){
if (already_called_onclose){
return;
}
already_called_onclose = true;
that._websocket_closed(ws_host_url, false);
};
var channels = [this.shell_channel, this.iopub_channel, this.stdin_channel];
for (var i=0; i < channels.length; i++) {
channels[i].onopen = $.proxy(this._ws_opened, this);
channels[i].onclose = ws_closed_early;
channels[i].onerror = ws_error;
}
// switch from early-close to late-close message after 1s
setTimeout(function() {
for (var i=0; i < channels.length; i++) {
if (channels[i] !== null) {
channels[i].onclose = ws_closed_late;
}
}
}, 1000);
this.shell_channel.onmessage = $.proxy(this._handle_shell_reply, this);
this.iopub_channel.onmessage = $.proxy(this._handle_iopub_message, this);
this.stdin_channel.onmessage = $.proxy(this._handle_input_request, this);
};
/**
* Handle a websocket entering the open state
* sends session and cookie authentication info as first message.
* Once all sockets are open, signal the Kernel.status_started event.
* @method _ws_opened
*/
Kernel.prototype._ws_opened = function (evt) {
// send the session id so the Session object Python-side
// has the same identity
evt.target.send(this.session_id + ':' + document.cookie);
var channels = [this.shell_channel, this.iopub_channel, this.stdin_channel];
for (var i=0; i < channels.length; i++) {
// if any channel is not ready, don't trigger event.
if ( channels[i].readyState !== WebSocket.OPEN ) return;
}
// all events ready, trigger started event.
this.events.trigger('status_started.Kernel', {kernel: this});
};
/**
* Stop the websocket channels.
* @method stop_channels
*/
Kernel.prototype.stop_channels = function () {
var channels = [this.shell_channel, this.iopub_channel, this.stdin_channel];
for (var i=0; i < channels.length; i++) {
if ( channels[i] !== null ) {
channels[i].onclose = null;
channels[i].close();
}
}
this.shell_channel = this.iopub_channel = this.stdin_channel = null;
};
// Main public methods.
// send a message on the Kernel's shell channel
Kernel.prototype.send_shell_message = function (msg_type, content, callbacks, metadata) {
var msg = this._get_msg(msg_type, content, metadata);
this.shell_channel.send(JSON.stringify(msg));
this.set_callbacks_for_msg(msg.header.msg_id, callbacks);
return msg.header.msg_id;
};
/**
* Get kernel info
*
* @param callback {function}
* @method kernel_info
*
* When calling this method, pass a callback function that expects one argument.
* The callback will be passed the complete `kernel_info_reply` message documented
* [here](http://ipython.org/ipython-doc/dev/development/messaging.html#kernel-info)
*/
Kernel.prototype.kernel_info = function (callback) {
var callbacks;
if (callback) {
callbacks = { shell : { reply : callback } };
}
return this.send_shell_message("kernel_info_request", {}, callbacks);
};
/**
* Get info on an object
*
* @param code {string}
* @param cursor_pos {integer}
* @param callback {function}
* @method inspect
*
* When calling this method, pass a callback function that expects one argument.
* The callback will be passed the complete `inspect_reply` message documented
* [here](http://ipython.org/ipython-doc/dev/development/messaging.html#object-information)
*/
Kernel.prototype.inspect = function (code, cursor_pos, callback) {
var callbacks;
if (callback) {
callbacks = { shell : { reply : callback } };
}
var content = {
code : code,
cursor_pos : cursor_pos,
detail_level : 0,
};
return this.send_shell_message("inspect_request", content, callbacks);
};
/**
* Execute given code into kernel, and pass result to callback.
*
* @async
* @method execute
* @param {string} code
* @param [callbacks] {Object} With the following keys (all optional)
* @param callbacks.shell.reply {function}
* @param callbacks.shell.payload.[payload_name] {function}
* @param callbacks.iopub.output {function}
* @param callbacks.iopub.clear_output {function}
* @param callbacks.input {function}
* @param {object} [options]
* @param [options.silent=false] {Boolean}
* @param [options.user_expressions=empty_dict] {Dict}
* @param [options.allow_stdin=false] {Boolean} true|false
*
* @example
*
* The options object should contain the options for the execute call. Its default
* values are:
*
* options = {
* silent : true,
* user_expressions : {},
* allow_stdin : false
* }
*
* When calling this method pass a callbacks structure of the form:
*
* callbacks = {
* shell : {
* reply : execute_reply_callback,
* payload : {
* set_next_input : set_next_input_callback,
* }
* },
* iopub : {
* output : output_callback,
* clear_output : clear_output_callback,
* },
* input : raw_input_callback
* }
*
* Each callback will be passed the entire message as a single arugment.
* Payload handlers will be passed the corresponding payload and the execute_reply message.
*/
Kernel.prototype.execute = function (code, callbacks, options) {
var content = {
code : code,
silent : true,
store_history : false,
user_expressions : {},
allow_stdin : false
};
callbacks = callbacks || {};
if (callbacks.input !== undefined) {
content.allow_stdin = true;
}
$.extend(true, content, options);
this.events.trigger('execution_request.Kernel', {kernel: this, content:content});
return this.send_shell_message("execute_request", content, callbacks);
};
/**
* When calling this method, pass a function to be called with the `complete_reply` message
* as its only argument when it arrives.
*
* `complete_reply` is documented
* [here](http://ipython.org/ipython-doc/dev/development/messaging.html#complete)
*
* @method complete
* @param code {string}
* @param cursor_pos {integer}
* @param callback {function}
*
*/
Kernel.prototype.complete = function (code, cursor_pos, callback) {
var callbacks;
if (callback) {
callbacks = { shell : { reply : callback } };
}
var content = {
code : code,
cursor_pos : cursor_pos,
};
return this.send_shell_message("complete_request", content, callbacks);
};
Kernel.prototype.interrupt = function () {
if (this.running) {
this.events.trigger('status_interrupting.Kernel', {kernel: this});
this.post(utils.url_join_encode(this.kernel_url, "interrupt"));
}
};
Kernel.prototype.kill = function (success, error) {
if (this.running) {
this.running = false;
var settings = {
cache : false,
type : "DELETE",
success : success,
error : error || utils.log_ajax_error,
};
$.ajax(utils.url_join_encode(this.kernel_url), settings);
this.stop_channels();
}
};
Kernel.prototype.send_input_reply = function (input) {
var content = {
value : input,
};
this.events.trigger('input_reply.Kernel', {kernel: this, content:content});
var msg = this._get_msg("input_reply", content);
this.stdin_channel.send(JSON.stringify(msg));
return msg.header.msg_id;
};
// Reply handlers
Kernel.prototype.register_iopub_handler = function (msg_type, callback) {
this._iopub_handlers[msg_type] = callback;
};
Kernel.prototype.get_iopub_handler = function (msg_type) {
// get iopub handler for a specific message type
return this._iopub_handlers[msg_type];
};
Kernel.prototype.get_callbacks_for_msg = function (msg_id) {
// get callbacks for a specific message
if (msg_id == this.last_msg_id) {
return this.last_msg_callbacks;
} else {
return this._msg_callbacks[msg_id];
}
};
Kernel.prototype.clear_callbacks_for_msg = function (msg_id) {
if (this._msg_callbacks[msg_id] !== undefined ) {
delete this._msg_callbacks[msg_id];
}
};
Kernel.prototype._finish_shell = function (msg_id) {
var callbacks = this._msg_callbacks[msg_id];
if (callbacks !== undefined) {
callbacks.shell_done = true;
if (callbacks.iopub_done) {
this.clear_callbacks_for_msg(msg_id);
}
}
};
Kernel.prototype._finish_iopub = function (msg_id) {
var callbacks = this._msg_callbacks[msg_id];
if (callbacks !== undefined) {
callbacks.iopub_done = true;
if (callbacks.shell_done) {
this.clear_callbacks_for_msg(msg_id);
}
}
};
/* Set callbacks for a particular message.
* Callbacks should be a struct of the following form:
* shell : {
*
* }
*/
Kernel.prototype.set_callbacks_for_msg = function (msg_id, callbacks) {
this.last_msg_id = msg_id;
if (callbacks) {
// shallow-copy mapping, because we will modify it at the top level
var cbcopy = this._msg_callbacks[msg_id] = this.last_msg_callbacks = {};
cbcopy.shell = callbacks.shell;
cbcopy.iopub = callbacks.iopub;
cbcopy.input = callbacks.input;
cbcopy.shell_done = (!callbacks.shell);
cbcopy.iopub_done = (!callbacks.iopub);
} else {
this.last_msg_callbacks = {};
}
};
Kernel.prototype._handle_shell_reply = function (e) {
var reply = $.parseJSON(e.data);
this.events.trigger('shell_reply.Kernel', {kernel: this, reply:reply});
var content = reply.content;
var metadata = reply.metadata;
var parent_id = reply.parent_header.msg_id;
var callbacks = this.get_callbacks_for_msg(parent_id);
if (!callbacks || !callbacks.shell) {
return;
}
var shell_callbacks = callbacks.shell;
// signal that shell callbacks are done
this._finish_shell(parent_id);
if (shell_callbacks.reply !== undefined) {
shell_callbacks.reply(reply);
}
if (content.payload && shell_callbacks.payload) {
this._handle_payloads(content.payload, shell_callbacks.payload, reply);
}
};
Kernel.prototype._handle_payloads = function (payloads, payload_callbacks, msg) {
var l = payloads.length;
// Payloads are handled by triggering events because we don't want the Kernel
// to depend on the Notebook or Pager classes.
for (var i=0; i<l; i++) {
var payload = payloads[i];
var callback = payload_callbacks[payload.source];
if (callback) {
callback(payload, msg);
}
}
};
Kernel.prototype._handle_status_message = function (msg) {
var execution_state = msg.content.execution_state;
var parent_id = msg.parent_header.msg_id;
// dispatch status msg callbacks, if any
var callbacks = this.get_callbacks_for_msg(parent_id);
if (callbacks && callbacks.iopub && callbacks.iopub.status) {
try {
callbacks.iopub.status(msg);
} catch (e) {
console.log("Exception in status msg handler", e, e.stack);
}
}
if (execution_state === 'busy') {
this.events.trigger('status_busy.Kernel', {kernel: this});
} else if (execution_state === 'idle') {
// signal that iopub callbacks are (probably) done
// async output may still arrive,
// but only for the most recent request
this._finish_iopub(parent_id);
// trigger status_idle event
this.events.trigger('status_idle.Kernel', {kernel: this});
} else if (execution_state === 'restarting') {
// autorestarting is distinct from restarting,
// in that it means the kernel died and the server is restarting it.
// status_restarting sets the notification widget,
// autorestart shows the more prominent dialog.
this.events.trigger('status_autorestarting.Kernel', {kernel: this});
this.events.trigger('status_restarting.Kernel', {kernel: this});
} else if (execution_state === 'dead') {
this.stop_channels();
this.events.trigger('status_dead.Kernel', {kernel: this});
}
};
// handle clear_output message
Kernel.prototype._handle_clear_output = function (msg) {
var callbacks = this.get_callbacks_for_msg(msg.parent_header.msg_id);
if (!callbacks || !callbacks.iopub) {
return;
}
var callback = callbacks.iopub.clear_output;
if (callback) {
callback(msg);
}
};
// handle an output message (execute_result, display_data, etc.)
Kernel.prototype._handle_output_message = function (msg) {
var callbacks = this.get_callbacks_for_msg(msg.parent_header.msg_id);
if (!callbacks || !callbacks.iopub) {
return;
}
var callback = callbacks.iopub.output;
if (callback) {
callback(msg);
}
};
// dispatch IOPub messages to respective handlers.
// each message type should have a handler.
Kernel.prototype._handle_iopub_message = function (e) {
var msg = $.parseJSON(e.data);
var handler = this.get_iopub_handler(msg.header.msg_type);
if (handler !== undefined) {
handler(msg);
}
};
Kernel.prototype._handle_input_request = function (e) {
var request = $.parseJSON(e.data);
var header = request.header;
var content = request.content;
var metadata = request.metadata;
var msg_type = header.msg_type;
if (msg_type !== 'input_request') {
console.log("Invalid input request!", request);
return;
}
var callbacks = this.get_callbacks_for_msg(request.parent_header.msg_id);
if (callbacks) {
if (callbacks.input) {
callbacks.input(request);
}
}
};
// Backwards compatability.
IPython.Kernel = Kernel;
return {'Kernel': Kernel};
});

File diff suppressed because it is too large Load Diff

@ -0,0 +1,120 @@
// Copyright (c) IPython Development Team.
// Distributed under the terms of the Modified BSD License.
define([
'underscore',
], function (_) {
"use strict";
var _deserialize_array_buffer = function (buf) {
var data = new DataView(buf);
// read the header: 1 + nbufs 32b integers
var nbufs = data.getUint32(0);
var offsets = [];
var i;
for (i = 1; i <= nbufs; i++) {
offsets.push(data.getUint32(i * 4));
}
var json_bytes = new Uint8Array(buf.slice(offsets[0], offsets[1]));
var msg = JSON.parse(
(new TextDecoder('utf8')).decode(json_bytes)
);
// the remaining chunks are stored as DataViews in msg.buffers
msg.buffers = [];
var start, stop;
for (i = 1; i < nbufs; i++) {
start = offsets[i];
stop = offsets[i+1] || buf.byteLength;
msg.buffers.push(new DataView(buf.slice(start, stop)));
}
return msg;
};
var _deserialize_binary = function(data, callback) {
/**
* deserialize the binary message format
* callback will be called with a message whose buffers attribute
* will be an array of DataViews.
*/
if (data instanceof Blob) {
// data is Blob, have to deserialize from ArrayBuffer in reader callback
var reader = new FileReader();
reader.onload = function () {
var msg = _deserialize_array_buffer(this.result);
callback(msg);
};
reader.readAsArrayBuffer(data);
} else {
// data is ArrayBuffer, can deserialize directly
var msg = _deserialize_array_buffer(data);
callback(msg);
}
};
var deserialize = function (data, callback) {
/**
* deserialize a message and pass the unpacked message object to callback
*/
if (typeof data === "string") {
// text JSON message
callback(JSON.parse(data));
} else {
// binary message
_deserialize_binary(data, callback);
}
};
var _serialize_binary = function (msg) {
/**
* implement the binary serialization protocol
* serializes JSON message to ArrayBuffer
*/
msg = _.clone(msg);
var offsets = [];
var buffers = [];
msg.buffers.map(function (buf) {
buffers.push(buf);
});
delete msg.buffers;
var json_utf8 = (new TextEncoder('utf8')).encode(JSON.stringify(msg));
buffers.unshift(json_utf8);
var nbufs = buffers.length;
offsets.push(4 * (nbufs + 1));
var i;
for (i = 0; i + 1 < buffers.length; i++) {
offsets.push(offsets[offsets.length-1] + buffers[i].byteLength);
}
var msg_buf = new Uint8Array(
offsets[offsets.length-1] + buffers[buffers.length-1].byteLength
);
// use DataView.setUint32 for network byte-order
var view = new DataView(msg_buf.buffer);
// write nbufs to first 4 bytes
view.setUint32(0, nbufs);
// write offsets to next 4 * nbufs bytes
for (i = 0; i < offsets.length; i++) {
view.setUint32(4 * (i+1), offsets[i]);
}
// write all the buffers at their respective offsets
for (i = 0; i < buffers.length; i++) {
msg_buf.set(new Uint8Array(buffers[i].buffer), offsets[i]);
}
// return raw ArrayBuffer
return msg_buf.buffer;
};
var serialize = function (msg) {
if (msg.buffers && msg.buffers.length) {
return _serialize_binary(msg);
} else {
return JSON.stringify(msg);
}
};
var exports = {
deserialize : deserialize,
serialize: serialize
};
return exports;
});

@ -1,150 +0,0 @@
// Copyright (c) IPython Development Team.
// Distributed under the terms of the Modified BSD License.
define([
'base/js/namespace',
'jquery',
'base/js/utils',
'services/kernels/js/kernel',
], function(IPython, $, utils, kernel) {
"use strict";
var Session = function(options){
this.kernel = null;
this.id = null;
this.notebook = options.notebook;
this.events = options.notebook.events;
this.name = options.notebook_name;
this.path = options.notebook_path;
this.kernel_name = options.kernel_name;
this.base_url = options.base_url;
this.ws_url = options.ws_url;
};
Session.prototype.start = function (success, error) {
var that = this;
var model = {
notebook : {
name : this.name,
path : this.path
},
kernel : {
name : this.kernel_name
}
};
var settings = {
processData : false,
cache : false,
type : "POST",
data: JSON.stringify(model),
dataType : "json",
success : function (data, status, xhr) {
that._handle_start_success(data);
if (success) {
success(data, status, xhr);
}
},
error : function (xhr, status, err) {
that._handle_start_failure(xhr, status, err);
if (error !== undefined) {
error(xhr, status, err);
}
utils.log_ajax_error(xhr, status, err);
}
};
var url = utils.url_join_encode(this.base_url, 'api/sessions');
$.ajax(url, settings);
};
Session.prototype.rename_notebook = function (name, path) {
this.name = name;
this.path = path;
var model = {
notebook : {
name : this.name,
path : this.path
}
};
var settings = {
processData : false,
cache : false,
type : "PATCH",
data: JSON.stringify(model),
dataType : "json",
error : utils.log_ajax_error,
};
var url = utils.url_join_encode(this.base_url, 'api/sessions', this.id);
$.ajax(url, settings);
};
Session.prototype.delete = function (success, error) {
var settings = {
processData : false,
cache : false,
type : "DELETE",
dataType : "json",
success : success,
error : error || utils.log_ajax_error,
};
if (this.kernel) {
this.kernel.running = false;
this.kernel.stop_channels();
}
var url = utils.url_join_encode(this.base_url, 'api/sessions', this.id);
$.ajax(url, settings);
};
// Kernel related things
/**
* Create the Kernel object associated with this Session.
*
* @method _handle_start_success
*/
Session.prototype._handle_start_success = function (data, status, xhr) {
this.id = data.id;
// If we asked for 'python', the response will have 'python3' or 'python2'.
this.kernel_name = data.kernel.name;
this.events.trigger('started.Session', this);
var kernel_service_url = utils.url_path_join(this.base_url, "api/kernels");
this.kernel = new kernel.Kernel(kernel_service_url, this.ws_url, this.notebook, this.kernel_name);
this.kernel._kernel_started(data.kernel);
};
Session.prototype._handle_start_failure = function (xhr, status, error) {
this.events.trigger('start_failed.Session', [this, xhr, status, error]);
this.events.trigger('status_dead.Kernel');
};
/**
* Prompt the user to restart the IPython kernel.
*
* @method restart_kernel
*/
Session.prototype.restart_kernel = function () {
this.kernel.restart();
};
Session.prototype.interrupt_kernel = function() {
this.kernel.interrupt();
};
Session.prototype.kill_kernel = function() {
this.kernel.kill();
};
var SessionAlreadyStarting = function (message) {
this.name = "SessionAlreadyStarting";
this.message = (message || "");
};
SessionAlreadyStarting.prototype = Error.prototype;
// For backwards compatability.
IPython.Session = Session;
return {
Session: Session,
SessionAlreadyStarting: SessionAlreadyStarting,
};
});

@ -0,0 +1,319 @@
// Copyright (c) IPython Development Team.
// Distributed under the terms of the Modified BSD License.
define([
'base/js/namespace',
'jquery',
'base/js/utils',
'services/kernels/kernel',
], function(IPython, $, utils, kernel) {
"use strict";
/**
* Session object for accessing the session REST api. The session
* should be used to start kernels and then shut them down -- for
* all other operations, the kernel object should be used.
*
* Options should include:
* - notebook_path: the path (not including name) to the notebook
* - kernel_name: the type of kernel (e.g. python3)
* - base_url: the root url of the notebook server
* - ws_url: the url to access websockets
* - notebook: Notebook object
*
* @class Session
* @param {Object} options
*/
var Session = function (options) {
this.id = null;
this.notebook_model = {
path: options.notebook_path
};
this.kernel_model = {
id: null,
name: options.kernel_name
};
this.base_url = options.base_url;
this.ws_url = options.ws_url;
this.session_service_url = utils.url_join_encode(this.base_url, 'api/sessions');
this.session_url = null;
this.notebook = options.notebook;
this.kernel = null;
this.events = options.notebook.events;
this.bind_events();
};
Session.prototype.bind_events = function () {
var that = this;
var record_status = function (evt, info) {
console.log('Session: ' + evt.type + ' (' + info.session.id + ')');
};
this.events.on('kernel_created.Session', record_status);
this.events.on('kernel_dead.Session', record_status);
this.events.on('kernel_killed.Session', record_status);
// if the kernel dies, then also remove the session
this.events.on('kernel_dead.Kernel', function () {
that.delete();
});
};
// Public REST api functions
/**
* GET /api/sessions
*
* Get a list of the current sessions.
*
* @function list
* @param {function} [success] - function executed on ajax success
* @param {function} [error] - functon executed on ajax error
*/
Session.prototype.list = function (success, error) {
$.ajax(this.session_service_url, {
processData: false,
cache: false,
type: "GET",
dataType: "json",
success: success,
error: this._on_error(error)
});
};
/**
* POST /api/sessions
*
* Start a new session. This function can only executed once.
*
* @function start
* @param {function} [success] - function executed on ajax success
* @param {function} [error] - functon executed on ajax error
*/
Session.prototype.start = function (success, error) {
var that = this;
var on_success = function (data, status, xhr) {
if (that.kernel) {
that.kernel.name = that.kernel_model.name;
} else {
var kernel_service_url = utils.url_path_join(that.base_url, "api/kernels");
that.kernel = new kernel.Kernel(kernel_service_url, that.ws_url, that.notebook, that.kernel_model.name);
}
that.events.trigger('kernel_created.Session', {session: that, kernel: that.kernel});
that.kernel._kernel_created(data.kernel);
if (success) {
success(data, status, xhr);
}
};
var on_error = function (xhr, status, err) {
that.events.trigger('kernel_dead.Session', {session: that, xhr: xhr, status: status, error: err});
if (error) {
error(xhr, status, err);
}
};
$.ajax(this.session_service_url, {
processData: false,
cache: false,
type: "POST",
data: JSON.stringify(this._get_model()),
dataType: "json",
success: this._on_success(on_success),
error: this._on_error(on_error)
});
};
/**
* GET /api/sessions/[:session_id]
*
* Get information about a session.
*
* @function get_info
* @param {function} [success] - function executed on ajax success
* @param {function} [error] - functon executed on ajax error
*/
Session.prototype.get_info = function (success, error) {
$.ajax(this.session_url, {
processData: false,
cache: false,
type: "GET",
dataType: "json",
success: this._on_success(success),
error: this._on_error(error)
});
};
/**
* PATCH /api/sessions/[:session_id]
*
* Rename or move a notebook. If the given name or path are
* undefined, then they will not be changed.
*
* @function rename_notebook
* @param {string} [path] - new notebook path
* @param {function} [success] - function executed on ajax success
* @param {function} [error] - functon executed on ajax error
*/
Session.prototype.rename_notebook = function (path, success, error) {
if (path !== undefined) {
this.notebook_model.path = path;
}
$.ajax(this.session_url, {
processData: false,
cache: false,
type: "PATCH",
data: JSON.stringify(this._get_model()),
dataType: "json",
success: this._on_success(success),
error: this._on_error(error)
});
};
/**
* DELETE /api/sessions/[:session_id]
*
* Kill the kernel and shutdown the session.
*
* @function delete
* @param {function} [success] - function executed on ajax success
* @param {function} [error] - functon executed on ajax error
*/
Session.prototype.delete = function (success, error) {
if (this.kernel) {
this.events.trigger('kernel_killed.Session', {session: this, kernel: this.kernel});
this.kernel._kernel_dead();
}
$.ajax(this.session_url, {
processData: false,
cache: false,
type: "DELETE",
dataType: "json",
success: this._on_success(success),
error: this._on_error(error)
});
};
/**
* Restart the session by deleting it and the starting it
* fresh. If options are given, they can include any of the
* following:
*
* - notebook_path - the path to the notebook
* - kernel_name - the name (type) of the kernel
*
* @function restart
* @param {Object} [options] - options for the new kernel
* @param {function} [success] - function executed on ajax success
* @param {function} [error] - functon executed on ajax error
*/
Session.prototype.restart = function (options, success, error) {
var that = this;
var start = function () {
if (options && options.notebook_path) {
that.notebook_model.path = options.notebook_path;
}
if (options && options.kernel_name) {
that.kernel_model.name = options.kernel_name;
}
that.kernel_model.id = null;
that.start(success, error);
};
this.delete(start, start);
};
// Helper functions
/**
* Get the data model for the session, which includes the notebook path
* and kernel (name and id).
*
* @function _get_model
* @returns {Object} - the data model
*/
Session.prototype._get_model = function () {
return {
notebook: this.notebook_model,
kernel: this.kernel_model
};
};
/**
* Update the data model from the given JSON object, which should
* have attributes of `id`, `notebook`, and/or `kernel`. If
* provided, the notebook data must include name and path, and the
* kernel data must include name and id.
*
* @function _update_model
* @param {Object} data - updated data model
*/
Session.prototype._update_model = function (data) {
if (data && data.id) {
this.id = data.id;
this.session_url = utils.url_join_encode(this.session_service_url, this.id);
}
if (data && data.notebook) {
this.notebook_model.path = data.notebook.path;
}
if (data && data.kernel) {
this.kernel_model.name = data.kernel.name;
this.kernel_model.id = data.kernel.id;
}
};
/**
* Handle a successful AJAX request by updating the session data
* model with the response, and then optionally calling a provided
* callback.
*
* @function _on_success
* @param {function} success - callback
*/
Session.prototype._on_success = function (success) {
var that = this;
return function (data, status, xhr) {
that._update_model(data);
if (success) {
success(data, status, xhr);
}
};
};
/**
* Handle a failed AJAX request by logging the error message, and
* then optionally calling a provided callback.
*
* @function _on_error
* @param {function} error - callback
*/
Session.prototype._on_error = function (error) {
return function (xhr, status, err) {
utils.log_ajax_error(xhr, status, err);
if (error) {
error(xhr, status, err);
}
};
};
/**
* Error type indicating that the session is already starting.
*/
var SessionAlreadyStarting = function (message) {
this.name = "SessionAlreadyStarting";
this.message = (message || "");
};
SessionAlreadyStarting.prototype = Error.prototype;
// For backwards compatability.
IPython.Session = Session;
return {
Session: Session,
SessionAlreadyStarting: SessionAlreadyStarting
};
});

@ -294,7 +294,7 @@ div.traceback-wrapper {
color: darkgreen;
}
.ansiyellow {
color: brown;
color: #c4a000;
}
.ansiblue {
color: darkblue;
@ -349,12 +349,6 @@ div.cell {
display: flex;
flex-direction: column;
align-items: stretch;
/* Old browsers */
-webkit-box-flex: 0;
-moz-box-flex: 0;
box-flex: 0;
/* Modern browsers */
flex: none;
border-radius: 4px;
box-sizing: border-box;
-moz-box-sizing: border-box;
@ -405,12 +399,6 @@ div.inner_cell {
flex-direction: column;
align-items: stretch;
/* Old browsers */
-webkit-box-flex: 0;
-moz-box-flex: 0;
box-flex: 0;
/* Modern browsers */
flex: none;
/* Old browsers */
-webkit-box-flex: 1;
-moz-box-flex: 1;
box-flex: 1;
@ -431,6 +419,44 @@ div.prompt:empty {
padding-top: 0;
padding-bottom: 0;
}
div.unrecognized_cell {
padding: 5px 5px 5px 0px;
/* Old browsers */
display: -webkit-box;
-webkit-box-orient: horizontal;
-webkit-box-align: stretch;
display: -moz-box;
-moz-box-orient: horizontal;
-moz-box-align: stretch;
display: box;
box-orient: horizontal;
box-align: stretch;
/* Modern browsers */
display: flex;
flex-direction: row;
align-items: stretch;
}
div.unrecognized_cell .inner_cell {
border-radius: 4px;
padding: 5px;
font-weight: bold;
color: red;
border: 1px solid #cfcfcf;
background: #eaeaea;
}
div.unrecognized_cell .inner_cell a {
color: inherit;
text-decoration: none;
}
div.unrecognized_cell .inner_cell a:hover {
color: inherit;
text-decoration: none;
}
@media (max-width: 480px) {
div.unrecognized_cell > div.prompt {
display: none;
}
}
/* any special styling for code cells that are currently running goes here */
div.input {
page-break-inside: avoid;
@ -448,12 +474,6 @@ div.input {
display: flex;
flex-direction: row;
align-items: stretch;
/* Old browsers */
-webkit-box-flex: 0;
-moz-box-flex: 0;
box-flex: 0;
/* Modern browsers */
flex: none;
}
@media (max-width: 480px) {
div.input {
@ -471,12 +491,6 @@ div.input {
display: flex;
flex-direction: column;
align-items: stretch;
/* Old browsers */
-webkit-box-flex: 0;
-moz-box-flex: 0;
box-flex: 0;
/* Modern browsers */
flex: none;
}
}
/* input_area and input_prompt must match in top border and margin for alignment */
@ -730,12 +744,6 @@ div.output_wrapper {
display: flex;
flex-direction: column;
align-items: stretch;
/* Old browsers */
-webkit-box-flex: 0;
-moz-box-flex: 0;
box-flex: 0;
/* Modern browsers */
flex: none;
}
/* class for the output area when it should be height-limited */
div.output_scroll {
@ -767,12 +775,6 @@ div.output_collapsed {
display: flex;
flex-direction: column;
align-items: stretch;
/* Old browsers */
-webkit-box-flex: 0;
-moz-box-flex: 0;
box-flex: 0;
/* Modern browsers */
flex: none;
}
div.out_prompt_overlay {
height: 100%;
@ -807,12 +809,6 @@ div.output_area {
display: flex;
flex-direction: row;
align-items: stretch;
/* Old browsers */
-webkit-box-flex: 0;
-moz-box-flex: 0;
box-flex: 0;
/* Modern browsers */
flex: none;
}
div.output_area .MathJax_Display {
text-align: left !important;
@ -842,12 +838,6 @@ div.output_area .rendered_html img {
display: flex;
flex-direction: column;
align-items: stretch;
/* Old browsers */
-webkit-box-flex: 0;
-moz-box-flex: 0;
box-flex: 0;
/* Modern browsers */
flex: none;
}
@media (max-width: 480px) {
div.output_area {
@ -865,12 +855,6 @@ div.output_area .rendered_html img {
display: flex;
flex-direction: column;
align-items: stretch;
/* Old browsers */
-webkit-box-flex: 0;
-moz-box-flex: 0;
box-flex: 0;
/* Modern browsers */
flex: none;
}
}
div.output_area pre {
@ -942,6 +926,19 @@ input.raw_input:focus {
p.p-space {
margin-bottom: 10px;
}
div.output_unrecognized {
padding: 5px;
font-weight: bold;
color: red;
}
div.output_unrecognized a {
color: inherit;
text-decoration: none;
}
div.output_unrecognized a:hover {
color: inherit;
text-decoration: none;
}
.rendered_html {
color: #000000;
/* any extras will just be numbers: */
@ -1130,12 +1127,6 @@ div.text_cell {
display: flex;
flex-direction: row;
align-items: stretch;
/* Old browsers */
-webkit-box-flex: 0;
-moz-box-flex: 0;
box-flex: 0;
/* Modern browsers */
flex: none;
}
@media (max-width: 480px) {
div.text_cell > div.prompt {
@ -1167,36 +1158,39 @@ h5:hover .anchor-link,
h6:hover .anchor-link {
visibility: visible;
}
div.cell.text_cell.rendered {
padding: 0px;
.text_cell.rendered .input_area {
display: none;
}
.cm-s-heading-1,
.cm-s-heading-2,
.cm-s-heading-3,
.cm-s-heading-4,
.cm-s-heading-5,
.cm-s-heading-6 {
.text_cell.unrendered .text_cell_render {
display: none;
}
.cm-header-1,
.cm-header-2,
.cm-header-3,
.cm-header-4,
.cm-header-5,
.cm-header-6 {
font-weight: bold;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
.cm-s-heading-1 {
font-size: 150%;
.cm-header-1 {
font-size: 185.7%;
}
.cm-s-heading-2 {
font-size: 130%;
.cm-header-2 {
font-size: 157.1%;
}
.cm-s-heading-3 {
font-size: 120%;
.cm-header-3 {
font-size: 128.6%;
}
.cm-s-heading-4 {
.cm-header-4 {
font-size: 110%;
}
.cm-s-heading-5 {
.cm-header-5 {
font-size: 100%;
font-style: italic;
}
.cm-s-heading-6 {
font-size: 90%;
.cm-header-6 {
font-size: 100%;
font-style: italic;
}
.widget-area {
@ -1229,12 +1223,6 @@ div.cell.text_cell.rendered {
display: flex;
flex-direction: row;
align-items: stretch;
/* Old browsers */
-webkit-box-flex: 0;
-moz-box-flex: 0;
box-flex: 0;
/* Modern browsers */
flex: none;
}
.widget-area .widget-subarea {
padding: 0.44em 0.4em 0.4em 1px;
@ -1257,12 +1245,6 @@ div.cell.text_cell.rendered {
flex-direction: column;
align-items: stretch;
/* Old browsers */
-webkit-box-flex: 0;
-moz-box-flex: 0;
box-flex: 0;
/* Modern browsers */
flex: none;
/* Old browsers */
-webkit-box-flex: 2;
-moz-box-flex: 2;
box-flex: 2;
@ -1336,12 +1318,6 @@ div.cell.text_cell.rendered {
display: flex;
flex-direction: row;
align-items: stretch;
/* Old browsers */
-webkit-box-flex: 0;
-moz-box-flex: 0;
box-flex: 0;
/* Modern browsers */
flex: none;
}
.widget-hslider .ui-slider {
/* Inner, invisible slide div */
@ -1362,12 +1338,6 @@ div.cell.text_cell.rendered {
flex-direction: row;
align-items: stretch;
/* Old browsers */
-webkit-box-flex: 0;
-moz-box-flex: 0;
box-flex: 0;
/* Modern browsers */
flex: none;
/* Old browsers */
-webkit-box-flex: 1;
-moz-box-flex: 1;
box-flex: 1;
@ -1415,12 +1385,6 @@ div.cell.text_cell.rendered {
display: flex;
flex-direction: column;
align-items: stretch;
/* Old browsers */
-webkit-box-flex: 0;
-moz-box-flex: 0;
box-flex: 0;
/* Modern browsers */
flex: none;
}
.widget-vslider .ui-slider {
/* Inner, invisible slide div */
@ -1443,12 +1407,6 @@ div.cell.text_cell.rendered {
flex-direction: column;
align-items: stretch;
/* Old browsers */
-webkit-box-flex: 0;
-moz-box-flex: 0;
box-flex: 0;
/* Modern browsers */
flex: none;
/* Old browsers */
-webkit-box-flex: 1;
-moz-box-flex: 1;
box-flex: 1;
@ -1515,16 +1473,14 @@ div.cell.text_cell.rendered {
display: flex;
flex-direction: row;
align-items: stretch;
/* Old browsers */
-webkit-box-flex: 0;
-moz-box-flex: 0;
box-flex: 0;
/* Modern browsers */
flex: none;
margin: 5px;
margin-top: 0px !important;
margin-bottom: 0px !important;
margin-right: 5px;
margin-left: 5px;
}
.widget-hbox input[type="checkbox"] {
margin-top: 9px;
margin-bottom: 10px;
}
.widget-hbox .widget-label {
/* Horizontal Label */
@ -1556,12 +1512,6 @@ div.cell.text_cell.rendered {
display: flex;
flex-direction: column;
align-items: stretch;
/* Old browsers */
-webkit-box-flex: 0;
-moz-box-flex: 0;
box-flex: 0;
/* Modern browsers */
flex: none;
}
.widget-vbox .widget-label {
/* Vertical Label */
@ -1615,12 +1565,6 @@ div.cell.text_cell.rendered {
display: flex;
flex-direction: column;
align-items: stretch;
/* Old browsers */
-webkit-box-flex: 0;
-moz-box-flex: 0;
box-flex: 0;
/* Modern browsers */
flex: none;
box-sizing: border-box;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;

@ -4,7 +4,6 @@
*
*/
@import "../components/bootstrap/less/bootstrap.less";
@import "../components/bootstrap/less/responsive-utilities.less";
/*!
*
@ -27,4 +26,4 @@
// notebook
@import "../notebook/less/style.less";
@import "../notebook/less/terminal.less";

File diff suppressed because it is too large Load Diff

@ -0,0 +1,53 @@
// Copyright (c) IPython Development Team.
// Distributed under the terms of the Modified BSD License.
require([
'jquery',
'termjs',
'base/js/utils',
'base/js/page',
'terminal/js/terminado',
'custom/custom',
], function(
$,
termjs,
utils,
page,
terminado
){
page = new page.Page();
// Test size: 25x80
var termRowHeight = function(){ return 1.00 * $("#dummy-screen")[0].offsetHeight / 25;};
// 1.02 here arrived at by trial and error to make the spacing look right
var termColWidth = function() { return 1.02 * $("#dummy-screen-rows")[0].offsetWidth / 80;};
var base_url = utils.get_body_data('baseUrl');
var ws_path = utils.get_body_data('wsPath');
var ws_url = location.protocol.replace('http', 'ws') + "//" + location.host
+ base_url + ws_path;
var header = $("#header")[0]
function calculate_size() {
height = window.innerHeight - header.offsetHeight;
width = window.innerWidth;
var rows = Math.min(1000, Math.max(20, Math.floor(height/termRowHeight())-1));
var cols = Math.min(1000, Math.max(40, Math.floor(width/termColWidth())-1));
console.log("resize to :", rows , 'rows by ', cols, 'columns');
return {rows: rows, cols: cols};
}
page.show_header();
size = calculate_size();
var terminal = terminado.make_terminal($("#terminado-container")[0], size, ws_url);
page.show_site();
window.onresize = function() {
var geom = calculate_size();
terminal.term.resize(geom.cols, geom.rows);
terminal.socket.send(JSON.stringify(["set_size", geom.rows, geom.cols,
window.innerHeight, window.innerWidth]));
};
});

@ -0,0 +1,39 @@
define ([], function() {
function make_terminal(element, size, ws_url) {
var ws = new WebSocket(ws_url);
var term = new Terminal({
cols: size.cols,
rows: size.rows,
screenKeys: true,
useStyle: false
});
ws.onopen = function(event) {
ws.send(JSON.stringify(["set_size", size.rows, size.cols,
window.innerHeight, window.innerWidth]));
term.on('data', function(data) {
ws.send(JSON.stringify(['stdin', data]));
});
term.on('title', function(title) {
document.title = title;
});
term.open(element);
ws.onmessage = function(event) {
json_msg = JSON.parse(event.data);
switch(json_msg[0]) {
case "stdout":
term.write(json_msg[1]);
break;
case "disconnect":
term.write("\r\n\r\n[CLOSED]\r\n");
break;
}
};
};
return {socket: ws, term: term};
}
return {make_terminal: make_terminal};
});

@ -9,15 +9,17 @@ define([
"use strict";
var KernelList = function (selector, options) {
// Constructor
//
// Parameters:
// selector: string
// options: dictionary
// Dictionary of keyword arguments.
// session_list: SessionList instance
// base_url: string
// notebook_path: string
/**
* Constructor
*
* Parameters:
* selector: string
* options: dictionary
* Dictionary of keyword arguments.
* session_list: SessionList instance
* base_url: string
* notebook_path: string
*/
notebooklist.NotebookList.call(this, selector, $.extend({
element_name: 'running'},
options));
@ -25,22 +27,27 @@ define([
KernelList.prototype = Object.create(notebooklist.NotebookList.prototype);
KernelList.prototype.add_duplicate_button = function () {
/**
* do nothing
*/
};
KernelList.prototype.sessions_loaded = function (d) {
this.sessions = d;
this.clear_list();
var item, path_name;
for (path_name in d) {
if (!d.hasOwnProperty(path_name)) {
var item, path;
for (path in d) {
if (!d.hasOwnProperty(path)) {
// nothing is safe in javascript
continue;
}
item = this.new_item(-1);
this.add_link({
name: path_name,
path: '',
name: path,
path: path,
type: 'notebook',
}, item);
this.add_shutdown_button(item, this.sessions[path_name]);
}
$('#running_list_header').toggle($.isEmptyObject(d));
};

@ -2,31 +2,38 @@
// Distributed under the terms of the Modified BSD License.
require([
'base/js/namespace',
'jquery',
'base/js/namespace',
'base/js/dialog',
'base/js/events',
'base/js/page',
'base/js/utils',
'contents',
'tree/js/notebooklist',
'tree/js/clusterlist',
'tree/js/sessionlist',
'tree/js/kernellist',
'tree/js/terminallist',
'auth/js/loginwidget',
// only loaded, not used:
'jqueryui',
'bootstrap',
'custom/custom',
], function(
IPython,
$,
$,
IPython,
dialog,
events,
page,
utils,
page,
utils,
contents_service,
notebooklist,
clusterlist,
sesssionlist,
kernellist,
kernellist,
terminallist,
loginwidget){
"use strict";
page = new page.Page();
@ -34,20 +41,45 @@ require([
base_url: utils.get_body_data("baseUrl"),
notebook_path: utils.get_body_data("notebookPath"),
};
session_list = new sesssionlist.SesssionList($.extend({
var session_list = new sesssionlist.SesssionList($.extend({
events: events},
common_options));
notebook_list = new notebooklist.NotebookList('#notebook_list', $.extend({
var contents = new contents_service.Contents($.extend({
events: events},
common_options));
var notebook_list = new notebooklist.NotebookList('#notebook_list', $.extend({
contents: contents,
session_list: session_list},
common_options));
cluster_list = new clusterlist.ClusterList('#cluster_list', common_options);
kernel_list = new kernellist.KernelList('#running_list', $.extend({
var cluster_list = new clusterlist.ClusterList('#cluster_list', common_options);
var kernel_list = new kernellist.KernelList('#running_list', $.extend({
session_list: session_list},
common_options));
login_widget = new loginwidget.LoginWidget('#login_widget', common_options);
var terminal_list;
if (utils.get_body_data("terminalsAvailable") === "True") {
terminal_list = new terminallist.TerminalList('#terminal_list', common_options);
}
var login_widget = new loginwidget.LoginWidget('#login_widget', common_options);
$('#new_notebook').click(function (e) {
notebook_list.new_notebook();
var w = window.open();
contents.new_untitled(common_options.notebook_path, {type: "notebook"}).then(
function (data) {
w.location = utils.url_join_encode(
common_options.base_url, 'notebooks', data.path
);
},
function(error) {
w.close();
dialog.modal({
title : 'Creating Notebook Failed',
body : "The error was: " + error.message,
buttons : {'OK' : {'class' : 'btn-primary'}}
});
}
);
});
var interval_id=0;
@ -56,13 +88,21 @@ require([
var time_refresh = 60; // in sec
var enable_autorefresh = function(){
//refresh immediately , then start interval
/**
*refresh immediately , then start interval
*/
session_list.load_sessions();
cluster_list.load_list();
if (terminal_list) {
terminal_list.load_terminals();
}
if (!interval_id){
interval_id = setInterval(function(){
session_list.load_sessions();
cluster_list.load_list();
if (terminal_list) {
terminal_list.load_terminals();
}
}, time_refresh*1000);
}
};
@ -111,5 +151,4 @@ require([
if (window.location.hash) {
$("#tabs").find("a[href=" + window.location.hash + "]").click();
}
});

@ -6,20 +6,24 @@ define([
'jquery',
'base/js/utils',
'base/js/dialog',
], function(IPython, $, utils, dialog) {
'base/js/events',
], function(IPython, $, utils, dialog, events) {
"use strict";
var NotebookList = function (selector, options) {
// Constructor
//
// Parameters:
// selector: string
// options: dictionary
// Dictionary of keyword arguments.
// session_list: SessionList instance
// element_name: string
// base_url: string
// notebook_path: string
/**
* Constructor
*
* Parameters:
* selector: string
* options: dictionary
* Dictionary of keyword arguments.
* session_list: SessionList instance
* element_name: string
* base_url: string
* notebook_path: string
* contents: Contents instance
*/
var that = this;
this.session_list = options.session_list;
// allow code re-use by just changing element_name in kernellist.js
@ -34,6 +38,7 @@ define([
this.sessions = {};
this.base_url = options.base_url || utils.get_body_data("baseUrl");
this.notebook_path = options.notebook_path || utils.get_body_data("notebookPath");
this.contents = options.contents;
if (this.session_list && this.session_list.events) {
this.session_list.events.on('sessions_loaded.Dashboard',
function(e, d) { that.sessions_loaded(d); });
@ -98,7 +103,7 @@ define([
};
reader.onerror = function (event) {
var item = $(event.target).data('item');
var name = item.data('name')
var name = item.data('name');
item.remove();
dialog.modal({
title : 'Failed to read file',
@ -116,11 +121,13 @@ define([
};
NotebookList.prototype.clear_list = function (remove_uploads) {
// Clears the navigation tree.
//
// Parameters
// remove_uploads: bool=False
// Should upload prompts also be removed from the tree.
/**
* Clears the navigation tree.
*
* Parameters
* remove_uploads: bool=False
* Should upload prompts also be removed from the tree.
*/
if (remove_uploads) {
this.element.children('.list_item').remove();
} else {
@ -140,37 +147,26 @@ define([
NotebookList.prototype.load_list = function () {
var that = this;
var settings = {
processData : false,
cache : false,
type : "GET",
dataType : "json",
success : $.proxy(this.list_loaded, this),
error : $.proxy( function(xhr, status, error){
utils.log_ajax_error(xhr, status, error);
that.list_loaded([], null, null, {msg:"Error connecting to server."});
},this)
};
var url = utils.url_join_encode(
this.base_url,
'api',
'contents',
this.notebook_path
this.contents.list_contents(that.notebook_path).then(
$.proxy(this.draw_notebook_list, this),
function(error) {
that.draw_notebook_list({content: []}, "Server error: " + error.message);
}
);
$.ajax(url, settings);
};
NotebookList.prototype.list_loaded = function (data, status, xhr, param) {
var message = 'Notebook list empty.';
if (param !== undefined && param.msg) {
message = param.msg;
}
/**
* Draw the list of notebooks
* @method draw_notebook_list
* @param {Array} list An array of dictionaries representing files or
* directories.
* @param {String} error_msg An error message
*/
NotebookList.prototype.draw_notebook_list = function (list, error_msg) {
var message = error_msg || 'Notebook list empty.';
var item = null;
var model = null;
var list = data.content;
var len = list.length;
var len = list.content.length;
this.clear_list();
var n_uploads = this.element.children('.list_item').length;
if (len === 0) {
@ -186,16 +182,18 @@ define([
model = {
type: 'directory',
name: '..',
path: path,
path: utils.url_path_split(path)[0],
};
this.add_link(model, item);
offset += 1;
}
for (var i=0; i<len; i++) {
model = list[i];
model = list.content[i];
item = this.new_item(i+offset);
this.add_link(model, item);
}
// Trigger an event when we've finished drawing the notebook list.
events.trigger('draw_notebook_list.NotebookList');
};
@ -249,8 +247,7 @@ define([
utils.url_join_encode(
this.base_url,
uri_prefix,
path,
name
path
)
);
// directory nav doesn't open new tabs
@ -258,14 +255,16 @@ define([
if (model.type !== "directory") {
link.attr('target','_blank');
}
var path_name = utils.url_path_join(path, name);
if (model.type !== 'directory') {
this.add_duplicate_button(item);
}
if (model.type == 'file') {
this.add_delete_button(item);
} else if (model.type == 'notebook') {
if(this.sessions[path_name] === undefined){
if (this.sessions[path] === undefined){
this.add_delete_button(item);
} else {
this.add_shutdown_button(item, this.sessions[path_name]);
this.add_shutdown_button(item, this.sessions[path]);
}
}
};
@ -315,12 +314,40 @@ define([
$.ajax(url, settings);
return false;
});
// var new_buttons = item.find('a'); // shutdown_button;
item.find(".item_buttons").text("").append(shutdown_button);
item.find(".item_buttons").append(shutdown_button);
};
NotebookList.prototype.add_duplicate_button = function (item) {
var notebooklist = this;
var duplicate_button = $("<button/>").text("Duplicate").addClass("btn btn-default btn-xs").
click(function (e) {
// $(this) is the button that was clicked.
var that = $(this);
var name = item.data('name');
var path = item.data('path');
var message = 'Are you sure you want to duplicate ' + name + '?';
var copy_from = {copy_from : path};
IPython.dialog.modal({
title : "Duplicate " + name,
body : message,
buttons : {
Duplicate : {
class: "btn-primary",
click: function() {
notebooklist.contents.copy(path, notebooklist.notebook_path).then(function () {
notebooklist.load_list();
});
}
},
Cancel : {}
}
});
return false;
});
item.find(".item_buttons").append(duplicate_button);
};
NotebookList.prototype.add_delete_button = function (item) {
var new_buttons = $('<span/>').addClass("btn-group pull-right");
var notebooklist = this;
var delete_button = $("<button/>").text("Delete").addClass("btn btn-default btn-xs").
click(function (e) {
@ -330,6 +357,7 @@ define([
// data because the outer scope's values change as we iterate through the loop.
var parent_item = that.parents('div.list_item');
var name = parent_item.data('name');
var path = parent_item.data('path');
var message = 'Are you sure you want to permanently delete the file: ' + name + '?';
dialog.modal({
title : "Delete file",
@ -338,23 +366,11 @@ define([
Delete : {
class: "btn-danger",
click: function() {
var settings = {
processData : false,
cache : false,
type : "DELETE",
dataType : "json",
success : function (data, status, xhr) {
parent_item.remove();
},
error : utils.log_ajax_error,
};
var url = utils.url_join_encode(
notebooklist.base_url,
'api/contents',
notebooklist.notebook_path,
name
notebooklist.contents.delete(path).then(
function() {
notebooklist.notebook_deleted(path);
}
);
$.ajax(url, settings);
}
},
Cancel : {}
@ -362,17 +378,30 @@ define([
});
return false;
});
item.find(".item_buttons").text("").append(delete_button);
item.find(".item_buttons").append(delete_button);
};
NotebookList.prototype.notebook_deleted = function(path) {
/**
* Remove the deleted notebook.
*/
$( ":data(path)" ).each(function() {
var element = $(this);
if (element.data("path") == path) {
element.remove();
events.trigger('notebook_deleted.NotebookList');
}
});
};
NotebookList.prototype.add_upload_button = function (item, type) {
NotebookList.prototype.add_upload_button = function (item) {
var that = this;
var upload_button = $('<button/>').text("Upload")
.addClass('btn btn-primary btn-xs upload_button')
.click(function (e) {
var path = that.notebook_path;
var filename = item.find('.item_name > input').val();
var path = utils.url_path_join(that.notebook_path, filename);
var filedata = item.data('filedata');
var format = 'text';
if (filename.length === 0 || filename[0] === '.') {
@ -394,10 +423,7 @@ define([
filedata = btoa(bytes);
format = 'base64';
}
var model = {
path: path,
name: filename
};
var model = {};
var name_and_ext = utils.splitext(filename);
var file_ext = name_and_ext[1];
@ -427,34 +453,20 @@ define([
model.content = filedata;
content_type = 'application/octet-stream';
}
var filedata = item.data('filedata');
filedata = item.data('filedata');
var settings = {
processData : false,
cache : false,
type : 'PUT',
data : JSON.stringify(model),
headers : {'Content-Type': content_type},
success : function (data, status, xhr) {
item.removeClass('new-file');
that.add_link(model, item);
that.add_delete_button(item);
that.session_list.load_sessions();
},
error : utils.log_ajax_error,
var on_success = function () {
item.removeClass('new-file');
that.add_link(model, item);
that.add_delete_button(item);
that.session_list.load_sessions();
};
var url = utils.url_join_encode(
that.base_url,
'api/contents',
that.notebook_path,
filename
);
var exists = false;
$.each(that.element.find('.list_item:not(.new-file)'), function(k,v){
if ($(v).data('name') === filename) { exists = true; return false; }
});
if (exists) {
dialog.modal({
title : "Replace file",
@ -462,7 +474,9 @@ define([
buttons : {
Overwrite : {
class: "btn-danger",
click: function() { $.ajax(url, settings); }
click: function () {
that.contents.save(path, model).then(on_success);
}
},
Cancel : {
click: function() { item.remove(); }
@ -470,7 +484,7 @@ define([
}
});
} else {
$.ajax(url, settings);
that.contents.save(path, model).then(on_success);
}
return false;
@ -487,52 +501,6 @@ define([
};
NotebookList.prototype.new_notebook = function(){
var path = this.notebook_path;
var base_url = this.base_url;
var settings = {
processData : false,
cache : false,
type : "POST",
dataType : "json",
async : false,
success : function (data, status, xhr) {
var notebook_name = data.name;
window.open(
utils.url_join_encode(
base_url,
'notebooks',
path,
notebook_name),
'_blank'
);
},
error : $.proxy(this.new_notebook_failed, this),
};
var url = utils.url_join_encode(
base_url,
'api/contents',
path
);
$.ajax(url, settings);
};
NotebookList.prototype.new_notebook_failed = function (xhr, status, error) {
utils.log_ajax_error(xhr, status, error);
var msg;
if (xhr.responseJSON && xhr.responseJSON.message) {
msg = xhr.responseJSON.message;
} else {
msg = xhr.statusText;
}
dialog.modal({
title : 'Creating Notebook Failed',
body : "The error was: " + msg,
buttons : {'OK' : {'class' : 'btn-primary'}}
});
};
// Backwards compatability.
IPython.NotebookList = NotebookList;

@ -9,13 +9,15 @@ define([
"use strict";
var SesssionList = function (options) {
// Constructor
//
// Parameters:
// options: dictionary
// Dictionary of keyword arguments.
// events: $(Events) instance
// base_url : string
/**
* Constructor
*
* Parameters:
* options: dictionary
* Dictionary of keyword arguments.
* events: $(Events) instance
* base_url : string
*/
this.events = options.events;
this.sessions = {};
this.base_url = options.base_url || utils.get_body_data("baseUrl");
@ -40,10 +42,7 @@ define([
var len = data.length;
var nb_path;
for (var i=0; i<len; i++) {
nb_path = utils.url_path_join(
data[i].notebook.path,
data[i].notebook.name
);
nb_path = data[i].notebook.path;
this.sessions[nb_path] = data[i].id;
}
this.events.trigger('sessions_loaded.Dashboard', this.sessions);

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

Loading…
Cancel
Save