This commit is contained in:
Pierre Gerold 2015-12-07 11:53:38 +01:00
commit 7d6c1c4774
41 changed files with 986 additions and 597 deletions

View File

@ -1,5 +1,6 @@
# http://travis-ci.org/#!/ipython/ipython
language: python
group: edge
cache:
directories:
- ~/.cache/bower

View File

@ -0,0 +1,224 @@
Custom front-end extensions
===========================
This describes the basic steps to write a JavaScript extension for the Jupyter
notebook front-end. This allows you to customize the behaviour of the various
pages like the dashboard, the notebook, or the text editor.
The structure of a front-end extension
--------------------------------------
.. note::
The notebook front-end and Javascript API are not stable, and are subject
to a lot of changes. Any extension written for the current notebook is
almost guaranteed to break in the next release.
.. _AMD module: https://en.wikipedia.org/wiki/Asynchronous_module_definition
A front-end extension is a JavaScript file that defines an `AMD module`_
which exposes at least a function called ``load_ipython_extension``, which
takes no arguments. We will not get into the details of what each of these
terms consists of yet, but here is the minimal code needed for a working
extension:
.. code:: javascript
// file my_extension/main.js
define(function(){
function load_ipython_extension(){
console.info('this is my first extension');
}
return {
load_ipython_extension: load_ipython_extension
};
});
.. note::
Although for historical reasons the function is called
``load_ipython_extension``, it does apply to the Jupyter notebook in
general, and will work regardless of the kernel in use.
If you are familiar with JavaScript, you can use this template to require any
Jupyter module and modify its configuration, or do anything else in client-side
Javascript. Your extension will be loaded at the right time during the notebook
page initialisation for you to set up a listener for the various events that
the page can trigger.
You might want access to the current instances of the various Jupyter notebook
components on the page, as opposed to the classes defined in the modules. The
current instances are exposed by a module named ``base/js/namespace``. If you
plan on accessing instances on the page, you should ``require`` this module
rather than accessing the global variable ``Jupyter``, which will be removed in
future. The following example demonstrates how to access the current notebook
instance:
.. code:: javascript
// file my_extension/main.js
define([
'base/js/namespace'
], function(
Jupyter
) {
function load_ipython_extension() {
console.log(
'This is the current notebook application instance:',
Jupyter.notebook
);
}
return {
load_ipython_extension: load_ipython_extension
};
});
Modifying key bindings
----------------------
One of the abilities of extensions is to modify key bindings, although once
again this is an API which is not guaranteed to be stable. However, custom key
bindings are frequently requested, and are helpful to increase accessibility,
so in the following we show how to access them.
Here is an example of an extension that will unbind the shortcut ``0,0`` in
command mode, which normally restarts the kernel, and bind ``0,0,0`` in its
place:
.. code:: javascript
// file my_extension/main.js
define([
'base/js/namespace'
], function(
Jupyter
) {
function load_ipython_extension() {
Jupyter.keyboard_manager.command_shortcuts.remove_shortcut('0,0');
Jupyter.keyboard_manager.command_shortcuts.add_shortcut('0,0,0', 'jupyter-notebook:restart-kernel');
}
return {
load_ipython_extension: load_ipython_extension
};
});
.. note::
The standard keybindings might not work correctly on non-US keyboards.
Unfortunately, this is a limitation of browser implementations and the
status of keyboard event handling on the web in general. We appreciate your
feedback if you have issues binding keys, or have any ideas to help improve
the situation.
You can see that I have used the **action name**
``jupyter-notebook:restart-kernel`` to bind the new shortcut. There is no API
yet to access the list of all available *actions*, though the following in the
JavaScript console of your browser on a notebook page should give you an idea
of what is available:
.. code:: javascript
Object.keys(require('base/js/namespace').actions._actions);
In this example, we changed a keyboard shortcut in **command mode**; you
can also customize keyboard shortcuts in **edit mode**.
However, most of the keyboard shortcuts in edit mode are handled by CodeMirror,
which supports custom key bindings via a completely different API.
Defining and registering your own actions
-----------------------------------------
As part of your front-end extension, you may wish to define actions, which can
be attached to toolbar buttons, or called from the command palette. Here is an
example of an extension that defines a (not very useful!) action to show an
alert, and adds a toolabr button using the full action name:
.. code:: javascript
// file my_extension/main.js
define([
'base/js/namespace'
], function(
Jupyter
) {
function load_ipython_extension() {
var handler = function () {
alert('this is an alert from my_extension!');
};
var action = {
icon: 'fa-comment-o', // a font-awesome class used on buttons, etc
help : 'Show an alert',
help_index : 'zz',
handler : handler
};
var prefix = 'my_extension';
var action_name = 'show-alert';
var full_action_name = Jupyter.actions.register(action, name, prefix); // returns 'my_extension:show-alert'
Jupyter.toolbar.add_buttons_group([full_action_name]);
}
return {
load_ipython_extension: load_ipython_extension
};
});
Every action needs a name, which, when joined with its prefix to make the full
action name, should be unique. Built-in actions, like the
``jupyter-notebook:restart-kernel`` we bound in the earlier
`Modifying key bindings`_ example, use the prefix ``jupyter-notebook``. For
actions defined in an extension, it makes sense to use the extension name as
the prefix. For the action name, the following guidelines should be considered:
.. adapted from notebook/static/notebook/js/actions.js
* First pick a noun and a verb for the action. For example, if the action is
"restart kernel," the verb is "restart" and the noun is "kernel".
* Omit terms like "selected" and "active" by default, so "delete-cell", rather
than "delete-selected-cell". Only provide a scope like "-all-" if it is other
than the default "selected" or "active" scope.
* If an action has a secondary action, separate the secondary action with
"-and-", so "restart-kernel-and-clear-output".
* Use above/below or previous/next to indicate spatial and sequential
relationships.
* Don't ever use before/after as they have a temporal connotation that is
confusing when used in a spatial context.
* For dialogs, use a verb that indicates what the dialog will accomplish, such
as "confirm-restart-kernel".
Installing and enabling extensions
----------------------------------
You can install your nbextension with the command:
jupyter nbextension install path/to/my_extension/
Where my_extension is the directory containing the Javascript files.
This will copy it to a Jupyter data directory (the exact location is platform
dependent - see :ref:`jupyter_path`).
For development, you can use the ``--symlink`` flag to symlink your extension
rather than copying it, so there's no need to reinstall after changes.
To use your extension, you'll also need to **enable** it, which tells the
notebook interface to load it. You can do that with another command:
jupyter nbextension enable my_extension/main
The argument refers to the Javascript module containing your
``load_ipython_extension`` function, which is ``my_extension/main.js`` in this
example. There is a corresponding ``disable`` command to stop using an
extension without uninstalling it.

View File

@ -43,6 +43,6 @@ def log_request(handler):
msg = msg + ' referer={referer}'
if status >= 500 and status != 502:
# log all headers if it caused an error
log_method(json.dumps(request.headers, indent=2))
log_method(json.dumps(dict(request.headers), indent=2))
log_method(msg.format(**ns))

View File

@ -120,7 +120,7 @@ class DeprecationHandler(IPythonHandler):
def get(self, url_path):
self.set_header("Content-Type", 'text/javascript')
self.finish("""
console.warn('`/static/widgets/js` is deprecated. Use `/nbextensions/widgets/widgets/js` instead.');
console.warn('`/static/widgets/js` is deprecated. Use `nbextensions/widgets/widgets/js` instead.');
define(['%s'], function(x) { return x; });
""" % url_path_join('nbextensions', 'widgets', 'widgets', url_path.rstrip('.js')))
self.log.warn('Deprecated widget Javascript path /static/widgets/js/*.js was used')

View File

@ -6,6 +6,7 @@ import os
import sys
import time
from contextlib import contextmanager
from itertools import combinations
from nose import SkipTest
from tornado.web import HTTPError
@ -188,6 +189,41 @@ class TestFileContentsManager(TestCase):
class TestContentsManager(TestCase):
@contextmanager
def assertRaisesHTTPError(self, status, msg=None):
msg = msg or "Should have raised HTTPError(%i)" % status
try:
yield
except HTTPError as e:
self.assertEqual(e.status_code, status)
else:
self.fail(msg)
def make_populated_dir(self, api_path):
cm = self.contents_manager
self.make_dir(api_path)
cm.new(path="/".join([api_path, "nb.ipynb"]))
cm.new(path="/".join([api_path, "file.txt"]))
def check_populated_dir_files(self, api_path):
dir_model = self.contents_manager.get(api_path)
self.assertEqual(dir_model['path'], api_path)
self.assertEqual(dir_model['type'], "directory")
for entry in dir_model['content']:
if entry['type'] == "directory":
continue
elif entry['type'] == "file":
self.assertEqual(entry['name'], "file.txt")
complete_path = "/".join([api_path, "file.txt"])
self.assertEqual(entry["path"], complete_path)
elif entry['type'] == "notebook":
self.assertEqual(entry['name'], "nb.ipynb")
complete_path = "/".join([api_path, "nb.ipynb"])
self.assertEqual(entry["path"], complete_path)
def setUp(self):
self._temp_dir = TemporaryDirectory()
@ -460,6 +496,55 @@ class TestContentsManager(TestCase):
# Check that a 'get' on the deleted notebook raises and error
self.assertRaises(HTTPError, cm.get, path)
def test_rename(self):
cm = self.contents_manager
# Create a new notebook
nb, name, path = self.new_notebook()
# Rename the notebook
cm.rename(path, "changed_path")
# Attempting to get the notebook under the old name raises an error
self.assertRaises(HTTPError, cm.get, path)
# Fetching the notebook under the new name is successful
assert isinstance(cm.get("changed_path"), dict)
# Ported tests on nested directory renaming from pgcontents
all_dirs = ['foo', 'bar', 'foo/bar', 'foo/bar/foo', 'foo/bar/foo/bar']
unchanged_dirs = all_dirs[:2]
changed_dirs = all_dirs[2:]
for _dir in all_dirs:
self.make_populated_dir(_dir)
self.check_populated_dir_files(_dir)
# Renaming to an existing directory should fail
for src, dest in combinations(all_dirs, 2):
with self.assertRaisesHTTPError(409):
cm.rename(src, dest)
# Creating a notebook in a non_existant directory should fail
with self.assertRaisesHTTPError(404):
cm.new_untitled("foo/bar_diff", ext=".ipynb")
cm.rename("foo/bar", "foo/bar_diff")
# Assert that unchanged directories remain so
for unchanged in unchanged_dirs:
self.check_populated_dir_files(unchanged)
# Assert changed directories can no longer be accessed under old names
for changed_dirname in changed_dirs:
with self.assertRaisesHTTPError(404):
cm.get(changed_dirname)
new_dirname = changed_dirname.replace("foo/bar", "foo/bar_diff", 1)
self.check_populated_dir_files(new_dirname)
# Created a notebook in the renamed directory should work
cm.new_untitled("foo/bar_diff", ext=".ipynb")
def test_delete_root(self):
cm = self.contents_manager
with self.assertRaises(HTTPError) as err:

View File

@ -25,12 +25,15 @@ class MainKernelHandler(APIHandler):
@web.authenticated
@json_errors
@gen.coroutine
def get(self):
km = self.kernel_manager
self.finish(json.dumps(km.list_kernels()))
kernels = yield gen.maybe_future(km.list_kernels())
self.finish(json.dumps(kernels))
@web.authenticated
@json_errors
@gen.coroutine
def post(self):
km = self.kernel_manager
model = self.get_json_body()
@ -41,7 +44,7 @@ class MainKernelHandler(APIHandler):
else:
model.setdefault('name', km.default_kernel_name)
kernel_id = km.start_kernel(kernel_name=model['name'])
kernel_id = yield gen.maybe_future(km.start_kernel(kernel_name=model['name']))
model = km.kernel_model(kernel_id)
location = url_path_join(self.base_url, 'api', 'kernels', url_escape(kernel_id))
self.set_header('Location', location)
@ -61,9 +64,10 @@ class KernelHandler(APIHandler):
@web.authenticated
@json_errors
@gen.coroutine
def delete(self, kernel_id):
km = self.kernel_manager
km.shutdown_kernel(kernel_id)
yield gen.maybe_future(km.shutdown_kernel(kernel_id))
self.set_status(204)
self.finish()

View File

@ -62,7 +62,8 @@ class MappingKernelManager(MultiKernelManager):
while not os.path.isdir(os_path) and os_path != self.root_dir:
os_path = os.path.dirname(os_path)
return os_path
@gen.coroutine
def start_kernel(self, kernel_id=None, path=None, **kwargs):
"""Start a kernel for a session and return its kernel_id.
@ -82,8 +83,9 @@ class MappingKernelManager(MultiKernelManager):
if kernel_id is None:
if path is not None:
kwargs['cwd'] = self.cwd_for_path(path)
kernel_id = super(MappingKernelManager, self).start_kernel(
**kwargs)
kernel_id = yield gen.maybe_future(
super(MappingKernelManager, self).start_kernel(**kwargs)
)
self.log.info("Kernel started: %s" % kernel_id)
self.log.debug("Kernel args: %r" % kwargs)
# register callback for failed auto-restart
@ -94,12 +96,13 @@ class MappingKernelManager(MultiKernelManager):
else:
self._check_kernel_id(kernel_id)
self.log.info("Using existing kernel: %s" % kernel_id)
return kernel_id
# py2-compat
raise gen.Return(kernel_id)
def shutdown_kernel(self, kernel_id, now=False):
"""Shutdown a kernel by kernel_id"""
self._check_kernel_id(kernel_id)
super(MappingKernelManager, self).shutdown_kernel(kernel_id, now=now)
return super(MappingKernelManager, self).shutdown_kernel(kernel_id, now=now)
def restart_kernel(self, kernel_id):
"""Restart a kernel by kernel_id"""

View File

@ -8,7 +8,7 @@ Preliminary documentation at https://github.com/ipython/ipython/wiki/IPEP-16%3A-
import json
from tornado import web
from tornado import gen, web
from ...base.handlers import APIHandler, json_errors
from jupyter_client.jsonutil import date_default
@ -20,20 +20,20 @@ class SessionRootHandler(APIHandler):
@web.authenticated
@json_errors
@gen.coroutine
def get(self):
# Return a list of running sessions
sm = self.session_manager
sessions = sm.list_sessions()
sessions = yield gen.maybe_future(sm.list_sessions())
self.finish(json.dumps(sessions, default=date_default))
@web.authenticated
@json_errors
@gen.coroutine
def post(self):
# Creates a new session
#(unless a session already exists for the named nb)
sm = self.session_manager
cm = self.contents_manager
km = self.kernel_manager
model = self.get_json_body()
if model is None:
@ -49,11 +49,13 @@ class SessionRootHandler(APIHandler):
kernel_name = None
# Check to see if session exists
if sm.session_exists(path=path):
model = sm.get_session(path=path)
exists = yield gen.maybe_future(sm.session_exists(path=path))
if exists:
model = yield gen.maybe_future(sm.get_session(path=path))
else:
try:
model = sm.create_session(path=path, kernel_name=kernel_name)
model = yield gen.maybe_future(
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)
@ -73,14 +75,16 @@ class SessionHandler(APIHandler):
@web.authenticated
@json_errors
@gen.coroutine
def get(self, session_id):
# Returns the JSON model for a single session
sm = self.session_manager
model = sm.get_session(session_id=session_id)
model = yield gen.maybe_future(sm.get_session(session_id=session_id))
self.finish(json.dumps(model, default=date_default))
@web.authenticated
@json_errors
@gen.coroutine
def patch(self, session_id):
# Currently, this handler is strictly for renaming notebooks
sm = self.session_manager
@ -93,17 +97,18 @@ class SessionHandler(APIHandler):
if 'path' in notebook:
changes['path'] = notebook['path']
sm.update_session(session_id, **changes)
model = sm.get_session(session_id=session_id)
yield gen.maybe_future(sm.update_session(session_id, **changes))
model = yield gen.maybe_future(sm.get_session(session_id=session_id))
self.finish(json.dumps(model, default=date_default))
@web.authenticated
@json_errors
@gen.coroutine
def delete(self, session_id):
# Deletes the session with given session_id
sm = self.session_manager
try:
sm.delete_session(session_id)
yield gen.maybe_future(sm.delete_session(session_id))
except KeyError:
# the kernel was deleted but the session wasn't!
raise web.HTTPError(410, "Kernel deleted before session")

View File

@ -6,7 +6,7 @@
import uuid
import sqlite3
from tornado import web
from tornado import gen, web
from traitlets.config.configurable import LoggingConfigurable
from ipython_genutils.py3compat import unicode_type
@ -39,10 +39,16 @@ class SessionManager(LoggingConfigurable):
self._connection = sqlite3.connect(':memory:')
self._connection.row_factory = sqlite3.Row
return self._connection
def close(self):
"""Close the sqlite connection"""
if self._cursor is not None:
self._cursor.close()
self._cursor = None
def __del__(self):
"""Close connection once SessionManager closes"""
self.cursor.close()
self.close()
def session_exists(self, path):
"""Check to see if the session for a given notebook exists"""
@ -56,17 +62,22 @@ class SessionManager(LoggingConfigurable):
def new_session_id(self):
"Create a uuid for a new session"
return unicode_type(uuid.uuid4())
@gen.coroutine
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(path=path)
kernel_id = self.kernel_manager.start_kernel(path=kernel_path,
kernel_name=kernel_name)
return self.save_session(session_id, path=path,
kernel_id=kernel_id)
kernel_id = yield gen.maybe_future(
self.kernel_manager.start_kernel(path=kernel_path, kernel_name=kernel_name)
)
result = yield gen.maybe_future(
self.save_session(session_id, path=path, kernel_id=kernel_id)
)
# py2-compat
raise gen.Return(result)
def save_session(self, session_id, path=None, kernel_id=None):
"""Saves the items for the session with the given session_id

View File

@ -2,7 +2,8 @@
from unittest import TestCase
from tornado import web
from tornado import gen, web
from tornado.ioloop import IOLoop
from ..sessionmanager import SessionManager
from notebook.services.kernels.kernelmanager import MappingKernelManager
@ -37,11 +38,27 @@ class TestSessionManager(TestCase):
kernel_manager=DummyMKM(),
contents_manager=ContentsManager(),
)
self.loop = IOLoop()
def tearDown(self):
self.loop.close(all_fds=True)
def create_sessions(self, *kwarg_list):
@gen.coroutine
def co_add():
sessions = []
for kwargs in kwarg_list:
session = yield self.sm.create_session(**kwargs)
sessions.append(session)
raise gen.Return(sessions)
return self.loop.run_sync(co_add)
def create_session(self, **kwargs):
return self.create_sessions(kwargs)[0]
def test_get_session(self):
sm = self.sm
session_id = sm.create_session(path='/path/to/test.ipynb',
kernel_name='bar')['id']
session_id = self.create_session(path='/path/to/test.ipynb', kernel_name='bar')['id']
model = sm.get_session(session_id=session_id)
expected = {'id':session_id,
'notebook':{'path': u'/path/to/test.ipynb'},
@ -51,13 +68,13 @@ class TestSessionManager(TestCase):
def test_bad_get_session(self):
# Should raise error if a bad key is passed to the database.
sm = self.sm
session_id = sm.create_session(path='/path/to/test.ipynb',
session_id = self.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 = self.sm
session = sm.create_session(path='/path/to/1/test1.ipynb', kernel_name='python')
session = self.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):
@ -68,11 +85,12 @@ class TestSessionManager(TestCase):
def test_list_sessions(self):
sm = self.sm
sessions = [
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 = self.create_sessions(
dict(path='/path/to/1/test1.ipynb', kernel_name='python'),
dict(path='/path/to/2/test2.ipynb', kernel_name='python'),
dict(path='/path/to/3/test3.ipynb', kernel_name='python'),
)
sessions = sm.list_sessions()
expected = [
{
@ -93,10 +111,10 @@ class TestSessionManager(TestCase):
def test_list_sessions_dead_kernel(self):
sm = self.sm
sessions = [
sm.create_session(path='/path/to/1/test1.ipynb', kernel_name='python'),
sm.create_session(path='/path/to/2/test2.ipynb', kernel_name='python'),
]
sessions = self.create_sessions(
dict(path='/path/to/1/test1.ipynb', kernel_name='python'),
dict(path='/path/to/2/test2.ipynb', kernel_name='python'),
)
# kill one of the kernels
sm.kernel_manager.shutdown_kernel(sessions[0]['kernel']['id'])
listed = sm.list_sessions()
@ -116,7 +134,7 @@ class TestSessionManager(TestCase):
def test_update_session(self):
sm = self.sm
session_id = sm.create_session(path='/path/to/test.ipynb',
session_id = self.create_session(path='/path/to/test.ipynb',
kernel_name='julia')['id']
sm.update_session(session_id, path='/path/to/new_name.ipynb')
model = sm.get_session(session_id=session_id)
@ -128,17 +146,17 @@ class TestSessionManager(TestCase):
def test_bad_update_session(self):
# try to update a session with a bad keyword ~ raise error
sm = self.sm
session_id = sm.create_session(path='/path/to/test.ipynb',
session_id = self.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 = self.sm
sessions = [
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 = self.create_sessions(
dict(path='/path/to/1/test1.ipynb', kernel_name='python'),
dict(path='/path/to/2/test2.ipynb', kernel_name='python'),
dict(path='/path/to/3/test3.ipynb', kernel_name='python'),
)
sm.delete_session(sessions[1]['id'])
new_sessions = sm.list_sessions()
expected = [{
@ -156,7 +174,7 @@ class TestSessionManager(TestCase):
def test_bad_delete_session(self):
# try to delete a session that doesn't exist ~ raise error
sm = self.sm
sm.create_session(path='/path/to/test.ipynb', kernel_name='python')
self.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

View File

@ -75,6 +75,8 @@ define(function(require) {
var footer = $("<div/>").addClass("modal-footer");
var default_button;
for (var label in options.buttons) {
var btn_opts = options.buttons[label];
var button = $("<button/>")
@ -88,12 +90,18 @@ define(function(require) {
button.addClass(btn_opts.class);
}
footer.append(button);
if (options.default_button && label === options.default_button) {
default_button = button;
}
}
if (!options.default_button) {
default_button = footer.find("button").last();
}
dialog_content.append(footer);
// hook up on-open event
modal.on("shown.bs.modal", function() {
setTimeout(function() {
footer.find("button").last().focus();
modal.on("shown.bs.modal", function () {
setTimeout(function () {
default_button.focus();
if (options.open) {
$.proxy(options.open, modal)();
}

View File

@ -255,7 +255,13 @@ define([
}
help.sort(function (a, b) {
if (a.help_index === b.help_index) {
return 0;
if (a.shortcut === b.shortcut) {
return 0;
}
if (a.shortcut > b.shortcut) {
return 1;
}
return -1;
}
if (a.help_index === undefined || a.help_index > b.help_index){
return 1;
@ -362,7 +368,7 @@ define([
**/
var action_name = this.actions.get_name(data);
if (! action_name){
throw new Error('does not know how to deal with', data);
throw new Error('does not know how to deal with : ' + data);
}
shortcut = normalize_shortcut(shortcut);
this.set_shortcut(shortcut, action_name);

View File

@ -107,10 +107,10 @@ define(function(require){
}
},
'run-cell':{
help : 'run marked cells',
help : 'run selected cells',
help_index : 'bb',
handler : function (env) {
env.notebook.execute_marked_cells();
env.notebook.execute_selected_cells();
}
},
'run-cell-and-insert-below':{
@ -163,7 +163,7 @@ define(function(require){
handler : function (env) {
var index = env.notebook.get_selected_index();
if (index !== 0 && index !== null) {
env.notebook.select_prev();
env.notebook.select_prev(true);
env.notebook.focus_cell();
}
}
@ -174,23 +174,23 @@ define(function(require){
handler : function (env) {
var index = env.notebook.get_selected_index();
if (index !== (env.notebook.ncells()-1) && index !== null) {
env.notebook.select_next();
env.notebook.select_next(true);
env.notebook.focus_cell();
}
}
},
'extend-marked-cells-above' : {
help: 'extend marked cells above',
'extend-selection-above' : {
help: 'extend selected cells above',
help_index : 'dc',
handler : function (env) {
env.notebook.extend_marked(-1);
env.notebook.extend_selection_by(-1)
}
},
'extend-marked-cells-below' : {
help: 'extend marked cells below',
'extend-selection-below' : {
help: 'extend selected cells below',
help_index : 'dd',
handler : function (env) {
env.notebook.extend_marked(1);
env.notebook.extend_selection_by(1)
}
},
'cut-cell' : {
@ -229,7 +229,7 @@ define(function(require){
help_index : 'ec',
handler : function (env) {
env.notebook.insert_cell_above();
env.notebook.select_prev();
env.notebook.select_prev(true);
env.notebook.focus_cell();
}
},
@ -239,7 +239,7 @@ define(function(require){
help_index : 'ed',
handler : function (env) {
env.notebook.insert_cell_below();
env.notebook.select_next();
env.notebook.select_next(true);
env.notebook.focus_cell();
}
},
@ -380,16 +380,10 @@ define(function(require){
}
},
'merge-cells' : {
help : 'merge marked cells',
help : 'merge selected cells',
help_index: 'el',
handler: function(env) {
env.notebook.merge_marked_cells();
}
},
'close-pager' : {
help_index : 'gd',
handler : function (env) {
env.pager.collapse();
env.notebook.merge_selected_cells();
}
},
'show-command-palette': {
@ -400,29 +394,6 @@ define(function(require){
env.notebook.show_command_palette();
}
},
'toggle-cell-marked': {
help_index : 'cj',
help: 'toggle marks',
icon: 'fa-check',
handler : function(env){
// Use bitwise logic to toggle the marked state.
env.notebook.get_selected_cell().marked ^= true;
}
},
'unmark-all-cells': {
help_index : 'ck',
help : 'unmark all cells',
handler : function(env) {
env.notebook.unmark_all_cells();
}
},
'mark-all-cells': {
help_index : 'cl',
help : 'mark all cells',
handler : function(env) {
env.notebook.mark_all_cells();
}
},
'toggle-toolbar':{
help: 'hide/show the toolbar',
handler : function(env){
@ -438,14 +409,12 @@ define(function(require){
events.trigger('resize-header.Page');
}
},
'close-pager-or-unmark-all-cells': {
help : 'close the pager or unmark all cells',
'close-pager': {
help : 'close the pager',
handler : function(env) {
// Collapse the page if it is open, otherwise unmark all.
// Collapse the page if it is open
if (env.pager && env.pager.expanded) {
env.pager.collapse();
} else {
env.notebook.unmark_all_cells();
}
}
},
@ -481,7 +450,7 @@ define(function(require){
event.preventDefault();
}
env.notebook.command_mode();
env.notebook.select_prev();
env.notebook.select_prev(true);
env.notebook.edit_mode();
cm = env.notebook.get_selected_cell().code_mirror;
cm.setCursor(cm.lastLine(), 0);
@ -498,7 +467,7 @@ define(function(require){
event.preventDefault();
}
env.notebook.command_mode();
env.notebook.select_next();
env.notebook.select_next(true);
env.notebook.edit_mode();
var cm = env.notebook.get_selected_cell().code_mirror;
cm.setCursor(0, 0);

View File

@ -1,4 +1,3 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/**
@ -21,7 +20,7 @@ define([
"use strict";
var overlayHack = CodeMirror.scrollbarModel.native.prototype.overlayHack;
CodeMirror.scrollbarModel.native.prototype.overlayHack = function () {
overlayHack.apply(this, arguments);
// Reverse `min-height: 18px` scrollbar hack on OS X
@ -55,6 +54,7 @@ define([
this.placeholder = config.placeholder || '';
this.selected = false;
this.anchor = false;
this.rendered = false;
this.mode = 'command';
@ -154,30 +154,29 @@ define([
}
};
/**
* trigger on focus and on click to bubble up to the notebook and
* potentially extend the selection if shift-click, contract the selection
* if just codemirror focus (so edit mode).
* We **might** be able to move that to notebook `handle_edit_mode`.
*/
Cell.prototype._on_click = function(event){
if (!this.selected) {
this.events.trigger('select.Cell', {'cell':this, 'extendSelection':event.shiftKey});
}
}
/**
* Subclasses can implement override bind_events.
* Be carefull to call the parent method when overwriting as it fires event.
* this will be triggerd after create_element in constructor.
* Be careful to call the parent method when overwriting as it fires event.
* this will be triggered after create_element in constructor.
* @method bind_events
*/
Cell.prototype.bind_events = function () {
var that = this;
// We trigger events so that Cell doesn't have to depend on Notebook.
that.element.click(function (event) {
if (!that.selected) {
that.events.trigger('select.Cell', {'cell':that});
}
// Cmdtrl-click should mark the cell.
var isMac = navigator.platform.slice(0, 3).toLowerCase() === 'mac';
if ((!isMac && event.ctrlKey) || (isMac && event.metaKey)) {
that.marked = !that.marked;
}
});
that.element.focusin(function (event) {
if (!that.selected) {
that.events.trigger('select.Cell', {'cell':that});
}
that._on_click(event)
});
if (this.code_mirror) {
this.code_mirror.on("change", function(cm, change) {
@ -186,6 +185,9 @@ define([
}
if (this.code_mirror) {
this.code_mirror.on('focus', function(cm, change) {
if (!that.selected) {
that.events.trigger('select.Cell', {'cell':that});
}
that.events.trigger('edit_mode.Cell', {cell: that});
});
}
@ -239,7 +241,7 @@ define([
/**
* Triger typsetting of math by mathjax on current cell element
* Triger typesetting of math by mathjax on current cell element
* @method typeset
*/
Cell.prototype.typeset = function () {
@ -251,7 +253,13 @@ define([
* @method select
* @return is the action being taken
*/
Cell.prototype.select = function () {
Cell.prototype.select = function (moveanchor) {
// if anchor is true, set the move the anchor
moveanchor = (moveanchor === undefined)? true:moveanchor;
if(moveanchor){
this.anchor=true;
}
if (!this.selected) {
this.element.addClass('selected');
this.element.removeClass('unselected');
@ -265,10 +273,14 @@ define([
/**
* handle cell level logic when the cell is unselected
* @method unselect
* @param {bool} leave_selected - true to move cursor away and extend selection
* @return is the action being taken
*/
Cell.prototype.unselect = function (leave_selected) {
Cell.prototype.unselect = function (moveanchor) {
// if anchor is true, remove also the anchor
moveanchor = (moveanchor === undefined)? true:moveanchor;
if (moveanchor){
this.anchor = false
}
if (this.selected) {
this.element.addClass('unselected');
this.element.removeClass('selected');
@ -279,32 +291,9 @@ define([
}
};
/**
* Whether or not the cell is marked.
* @return {boolean}
*/
Object.defineProperty(Cell.prototype, 'marked', {
get: function() {
return this.element.hasClass('marked');
},
set: function(value) {
var isMarked = this.element.hasClass('marked');
// Use a casting comparison. Allows for the caller to assign 0 or
// 1 instead of a boolean value, which in return means the caller
// can do cell.marked ^= true to toggle the mark.
if (isMarked != value) {
if (value) {
this.element.addClass('marked');
} else {
this.element.removeClass('marked');
}
this.events.trigger('marked_changed.Cell', {cell: this, value: value});
}
}
});
/**
* should be overritten by subclass
* should be overwritten by subclass
* @method execute
*/
Cell.prototype.execute = function () {
@ -426,6 +415,7 @@ define([
*/
Cell.prototype.focus_cell = function () {
this.element.focus();
this._on_click({});
};
/**

View File

@ -354,7 +354,6 @@ define([
var chkb = $('<input/>').attr('type', 'checkbox');
var lbl = $('<label/>').append($('<span/>').text(name));
lbl.append(chkb);
chkb.attr("checked", getter(cell));
chkb.click(function(){
@ -362,7 +361,7 @@ define([
setter(cell, !v);
chkb.attr("checked", !v);
});
button_container.append($('<span/>').append(lbl));
button_container.append($('<span/>').append(lbl).append(chkb));
};
};
@ -387,13 +386,12 @@ define([
var text = $('<input/>').attr('type', 'text');
var lbl = $('<label/>').append($('<span/>').text(name));
lbl.append(text);
text.attr("value", getter(cell));
text.keyup(function(){
setter(cell, text.val());
});
button_container.append($('<span/>').append(lbl));
button_container.append($('<span/>').append(lbl).append(text));
IPython.keyboard_manager.register_events(text);
};
};

View File

@ -50,7 +50,15 @@ define([
*/
CodeMirror.commands.delSpaceToPrevTabStop = function(cm){
var from = cm.getCursor(true), to = cm.getCursor(false), sel = !posEq(from, to);
if (!posEq(from, to)) { cm.replaceRange("", from, to); return; }
if (sel) {
var ranges = cm.listSelections();
for (var i = ranges.length - 1; i >= 0; i--) {
var head = ranges[i].head;
var anchor = ranges[i].anchor;
cm.replaceRange("", Pos(head.line, head.ch), CodeMirror.Pos(anchor.line, anchor.ch));
}
return;
}
var cur = cm.getCursor(), line = cm.getLine(cur.line);
var tabsize = cm.getOption('tabSize');
var chToPrevTabStop = cur.ch-(Math.ceil(cur.ch/tabsize)-1)*tabsize;
@ -188,7 +196,7 @@ define([
/** @method bind_events */
CodeCell.prototype.bind_events = function () {
Cell.prototype.bind_events.apply(this);
Cell.prototype.bind_events.apply(this, arguments);
var that = this;
this.element.focusout(
@ -393,7 +401,7 @@ define([
// Basic cell manipulation.
CodeCell.prototype.select = function () {
var cont = Cell.prototype.select.apply(this);
var cont = Cell.prototype.select.apply(this, arguments);
if (cont) {
this.code_mirror.refresh();
this.auto_highlight();
@ -402,7 +410,7 @@ define([
};
CodeCell.prototype.render = function () {
var cont = Cell.prototype.render.apply(this);
var cont = Cell.prototype.render.apply(this, arguments);
// Always execute, even if we are already in the rendered state
return cont;
};
@ -540,7 +548,7 @@ define([
* @return is the action being taken
*/
CodeCell.prototype.unselect = function() {
var cont = Cell.prototype.unselect.call(this);
var cont = Cell.prototype.unselect.apply(this, arguments);
if (cont) {
// When a code cell is unselected, make sure that the corresponding
// tooltip and completer to that cell is closed.

View File

@ -96,14 +96,14 @@ define([
'i,i' : 'jupyter-notebook:interrupt-kernel',
'0,0' : 'jupyter-notebook:confirm-restart-kernel',
'd,d' : 'jupyter-notebook:delete-cell',
'esc': 'jupyter-notebook:close-pager-or-unmark-all-cells',
'esc': 'jupyter-notebook:close-pager',
'up' : 'jupyter-notebook:select-previous-cell',
'k' : 'jupyter-notebook:select-previous-cell',
'j' : 'jupyter-notebook:select-next-cell',
'shift-k': 'jupyter-notebook:extend-marked-cells-above',
'shift-j': 'jupyter-notebook:extend-marked-cells-below',
'shift-up': 'jupyter-notebook:extend-marked-cells-above',
'shift-down': 'jupyter-notebook:extend-marked-cells-below',
'shift-k': 'jupyter-notebook:extend-selection-above',
'shift-j': 'jupyter-notebook:extend-selection-below',
'shift-up': 'jupyter-notebook:extend-selection-above',
'shift-down': 'jupyter-notebook:extend-selection-below',
'x' : 'jupyter-notebook:cut-cell',
'c' : 'jupyter-notebook:copy-cell',
'v' : 'jupyter-notebook:paste-cell-below',

View File

@ -63,7 +63,7 @@ define([
// The selected cell loses focus when the menu is entered, so we
// re-select it upon selection.
var i = that.notebook.get_selected_index();
that.notebook.select(i);
that.notebook.select(i, false);
}
);
};

View File

@ -29,6 +29,12 @@ define(function (require) {
var scrollmanager = require('notebook/js/scrollmanager');
var commandpalette = require('notebook/js/commandpalette');
var _SOFT_SELECTION_CLASS = 'jupyter-soft-selected';
function soft_selected(cell){
return cell.element.hasClass(_SOFT_SELECTION_CLASS);
}
/**
* Contains and manages cells.
* @class Notebook
@ -58,6 +64,8 @@ define(function (require) {
this.ws_url = options.ws_url;
this._session_starting = false;
this.last_modified = null;
// debug 484
this._last_modified = 'init';
// Create default scroll manager.
this.scroll_manager = new scrollmanager.ScrollManager(this);
@ -185,9 +193,6 @@ define(function (require) {
Notebook.prototype.bind_events = function () {
var that = this;
this.events.on('marked_changed.Cell', function() {
that.update_marked_status();
});
this.events.on('set_next_input.Notebook', function (event, data) {
if (data.replace) {
@ -219,7 +224,7 @@ define(function (require) {
this.events.on('select.Cell', function (event, data) {
var index = that.find_cell_index(data.cell);
that.select(index);
that.select(index, !data.extendSelection);
});
this.events.on('edit_mode.Cell', function (event, data) {
@ -296,9 +301,6 @@ define(function (require) {
expand_time(time);
});
this.scroll_manager.onScroll(function () {
that.update_marked_status();
}, 100);
// Firefox 22 broke $(window).on("beforeunload")
// I'm not sure why or how.
@ -582,13 +584,30 @@ define(function (require) {
return i;
};
Notebook.prototype.get_selected_cells = function () {
return this.get_cells().filter(function(cell, index){ return cell.selected || soft_selected(cell) || cell.anchor})
};
Notebook.prototype.get_selected_cells_indices = function () {
var result = [];
this.get_cells().filter(function (cell, index) {
if (cell.selected || soft_selected(cell) || cell.anchor) {
result.push(index);
}
});
return result;
};
/**
* Get the currently selected cell.
*
* @return {Cell} The selected cell
*/
Notebook.prototype.get_selected_cell = function () {
var index = this.get_selected_index();
var index = this.get_selected_cells_indices();
return this.get_cell(index);
};
@ -606,6 +625,15 @@ define(function (require) {
}
};
Notebook.prototype.get_anchor_index = function () {
var result = null;
this.get_cell_elements().filter(function (index) {
if ($(this).data("cell").anchor === true) {
result = index;
}
});
return result;
};
/**
* Get the index of the currently selected cell.
*
@ -620,144 +648,52 @@ define(function (require) {
});
return result;
};
/**
* Toggles the marks on the cells
* @param {Cell[]} [cells] - optionally specify what cells should be toggled
*/
Notebook.prototype.toggle_cells_marked = function(cells) {
cells = cells || this.get_cells();
cells.forEach(function(cell) { cell.marked = !cell.marked; });
};
/**
* Mark all of the cells
* @param {Cell[]} [cells] - optionally specify what cells should be marked
*/
Notebook.prototype.mark_all_cells = function(cells) {
cells = cells || this.get_cells();
cells.forEach(function(cell) { cell.marked = true; });
};
/**
* Unmark all of the cells
* @param {Cell[]} [cells] - optionally specify what cells should be unmarked
*/
Notebook.prototype.unmark_all_cells = function(cells) {
this.get_marked_cells(cells).forEach(function(cell) { cell.marked = false; });
};
/**
* Set the cells that should be marked, exclusively
* @param {Cell[]} cells
*/
Notebook.prototype.set_marked_cells = function(cells) {
this.unmark_all_cells();
this.mark_all_cells(cells);
};
/**
* Gets the cells that are marked
* @param {Cell[]} [cells] - optionally provide the cells to search through
* @return {Cell[]} marked cells
*/
Notebook.prototype.get_marked_cells = function(cells) {
cells = cells || this.get_cells();
return cells.filter(function(cell) { return (cell.marked || cell.selected); });
};
/**
* Sets the cells that are marked by indices
* @param {number[]} indices
* @param {Cell[]} [cells] - optionally provide the cells to search through
*/
Notebook.prototype.set_marked_indices = function(indices, cells) {
cells = cells || this.get_cells();
this.unmark_all_cells(cells);
this.mark_all_cells(cells.filter(function(cell, index) { return indices.indexOf(index) !== -1; }));
};
/**
* Gets the indices of the cells that are marked
* @param {Cell[]} [cells] - optionally provide the cells to search through
* @return {number[]} marked cell indices
*/
Notebook.prototype.get_marked_indices = function(cells) {
cells = cells || this.get_cells();
var markedCells = this.get_marked_cells(cells);
return markedCells.map(function(cell) { return cells.indexOf(cell); });
};
/**
* Checks if the marked cells are contiguous
* @param {Cell[]} [cells] - optionally provide the cells to search through
* @return {boolean}
*/
Notebook.prototype.are_marked_cells_contiguous = function(cells) {
// Get a numerically sorted list of the marked indices.
var markedIndices = this.get_marked_indices(cells).sort(
function(a,b) { return a-b; });
// Check for contiguousness
for (var i = 0; i < markedIndices.length - 1; i++) {
if (markedIndices[i+1] - markedIndices[i] !== 1) {
return false;
}
}
return true;
};
/**
* Checks if the marked cells specified by their indices are contiguous
* @param {number[]} indices - the cell indices to search through
* @param {Cell[]} [cells] - the cells to search through
* @return {boolean}
*/
Notebook.prototype.are_marked_indices_contiguous = function(indices, cells) {
cells = cells || this.get_cells();
return this.are_marked_cells_contiguous(cells.filter(function(cell, index) {
return indices.indexOf(index) !== -1;
}));
};
/**
* Extend the selected range
*
* @param {number} offset
*/
Notebook.prototype.extend_marked = function(offset) {
// Mark currently selected cell
this.get_selected_cell().marked = true;
// Select the cell in the offset direction. Bound index between 0 and
// the number of cells -1.
var selectedIndex = Math.min(Math.max(this.get_selected_index() + offset, 0), this.ncells()-1);
this.select(selectedIndex);
this.ensure_focused();
};
Notebook.prototype.update_marked_status = function() {
var marked_cells = this.get_marked_cells();
var num_offscreen = 0;
var i;
for (i = 0; i < marked_cells.length; i++) {
if (!this.scroll_manager.is_cell_visible(marked_cells[i])) {
num_offscreen += 1;
}
}
this.events.trigger('marked_offscreen.Cell', num_offscreen);
};
// Cell selection.
Notebook.prototype.extend_selection_by = function(delta) {
var index = this.get_selected_index();
// do not move anchor
return this.select(index+delta, false);
};
Notebook.prototype.update_soft_selection = function(){
var i1 = this.get_selected_index();
var i2 = this.get_anchor_index();
var low = Math.min(i1, i2);
var high = Math.max(i1, i2);
if (low !== high){
$('body').addClass('jupyter-multi-select');
} else {
$('body').removeClass('jupyter-multi-select');
}
this.get_cells().map(function(cell, index, all){
if( low <= index && index <= high ){
cell.element.addClass(_SOFT_SELECTION_CLASS);
} else {
cell.element.removeClass(_SOFT_SELECTION_CLASS);
}
})
}
Notebook.prototype._contract_selection = function(){
var i = this.get_selected_index();
this.select(i, true);
}
/**
* Programmatically select a cell.
*
* @param {integer} index - A cell's index
* @param {bool} moveanchor whether to move the selection
* anchor, default to true.
* @return {Notebook} This notebook
*/
Notebook.prototype.select = function (index) {
Notebook.prototype.select = function (index, moveanchor) {
moveanchor = (moveanchor===undefined)? true : moveanchor;
if (this.is_valid_cell_index(index)) {
var sindex = this.get_selected_index();
if (sindex !== null && index !== sindex) {
@ -766,11 +702,13 @@ define(function (require) {
if (this.mode !== 'command') {
this.command_mode();
}
this.get_cell(sindex).unselect();
this.get_cell(sindex).unselect(moveanchor);
}
if(moveanchor){
this.get_cell(this.get_anchor_index()).unselect(true);
}
var cell = this.get_cell(index);
cell.select();
this.update_marked_status();
cell.select(moveanchor);
if (cell.cell_type === 'heading') {
this.events.trigger('selected_cell_type_changed.Notebook',
{'cell_type':cell.cell_type,level:cell.level}
@ -781,17 +719,20 @@ define(function (require) {
);
}
}
this.update_soft_selection();
return this;
};
/**
* Programmatically select the next cell.
*
* @param {bool} moveanchor whether to move the selection
* anchor, default to true.
* @return {Notebook} This notebook
*/
Notebook.prototype.select_next = function () {
Notebook.prototype.select_next = function (moveanchor) {
var index = this.get_selected_index();
this.select(index+1);
this.select(index+1, moveanchor);
return this;
};
@ -800,9 +741,9 @@ define(function (require) {
*
* @return {Notebook} This notebook
*/
Notebook.prototype.select_prev = function () {
Notebook.prototype.select_prev = function (moveanchor) {
var index = this.get_selected_index();
this.select(index-1);
this.select(index-1, moveanchor);
return this;
};
@ -856,6 +797,7 @@ define(function (require) {
* @param {Cell} [cell] Cell to enter edit mode on.
*/
Notebook.prototype.handle_edit_mode = function (cell) {
this._contract_selection();
if (cell && this.mode !== 'edit') {
cell.edit_mode();
this.mode = 'edit';
@ -868,6 +810,7 @@ define(function (require) {
* Make a cell enter edit mode.
*/
Notebook.prototype.edit_mode = function () {
this._contract_selection();
var cell = this.get_selected_cell();
if (cell && this.mode !== 'edit') {
cell.unrender();
@ -976,7 +919,7 @@ define(function (require) {
*/
Notebook.prototype.delete_cells = function(indices) {
if (indices === undefined) {
indices = this.get_marked_indices();
indices = this.get_selected_cells_indices();
}
this.undelete_backup = [];
@ -1416,7 +1359,7 @@ define(function (require) {
* Copy cells.
*/
Notebook.prototype.copy_cell = function () {
var cells = this.get_marked_cells();
var cells = this.get_selected_cells();
if (cells.length === 0) {
cells = [this.get_selected_cell()];
}
@ -1505,7 +1448,6 @@ define(function (require) {
// Unrender the new cell so we can call set_text.
new_cell.unrender();
new_cell.set_text(texta);
new_cell.marked = cell.marked;
}
};
@ -1562,14 +1504,13 @@ define(function (require) {
this.delete_cells(indices);
this.select(this.find_cell_index(target));
this.unmark_all_cells();
};
/**
* Merge the selected range of cells
*/
Notebook.prototype.merge_marked_cells = function() {
this.merge_cells(this.get_marked_indices());
Notebook.prototype.merge_selected_cells = function() {
this.merge_cells(this.get_selected_cells_indices());
};
/**
@ -2007,27 +1948,27 @@ define(function (require) {
/**
* Execute or render cell outputs and go into command mode.
*/
Notebook.prototype.execute_marked_cells = function () {
this.execute_cells(this.get_marked_indices());
Notebook.prototype.execute_selected_cells = function () {
this.execute_cells(this.get_selected_cells_indices());
};
/**
* Alias for execute_marked_cells, for backwards compatibility --
* Alias for execute_selected_cells, for backwards compatibility --
* previously, doing "Run Cell" would only ever run a single cell (hence
* `execute_cell`), but now it runs all marked cells, so that's the
* preferable function to use. But it is good to keep this function to avoid
* breaking existing extensions, etc.
*/
Notebook.prototype.execute_cell = function () {
this.execute_marked_cells();
this.execute_selected_cells();
};
/**
* Execute or render cell outputs and insert a new cell below.
*/
Notebook.prototype.execute_cell_and_insert_below = function () {
// execute the marked cells, and don't insert anything
var indices = this.get_marked_indices();
var indices = this.get_selected_cells_indices();
if (indices.length > 1) {
this.execute_cells(indices);
return;
@ -2059,8 +2000,7 @@ define(function (require) {
* Execute or render cell outputs and select the next cell.
*/
Notebook.prototype.execute_cell_and_select_below = function () {
// execute the marked cells, and don't select anything
var indices = this.get_marked_indices();
var indices = this.get_selected_cells_indices();
if (indices.length > 1) {
this.execute_cells(indices);
return;
@ -2320,6 +2260,8 @@ define(function (require) {
function (data) {
var last_modified = new Date(data.last_modified);
if (last_modified > that.last_modified) {
console.warn("Last saving was done on `"+that.last_modified+"`("+that._last_modified+"), "+
"while the current file seem to have been saved on `"+data.last_modified+"`")
dialog.modal({
notebook: that,
keyboard_manager: that.keyboard_manager,
@ -2365,6 +2307,8 @@ define(function (require) {
Notebook.prototype.save_notebook_success = function (start, data) {
this.set_dirty(false);
this.last_modified = new Date(data.last_modified);
// debug 484
this._last_modified = 'save-success:'+data.last_modified;
if (data.message) {
// save succeeded, but validation failed.
var body = $("<div>");
@ -2513,6 +2457,8 @@ define(function (require) {
that.notebook_name = json.name;
that.notebook_path = json.path;
that.last_modified = new Date(json.last_modified);
// debug 484
that._last_modified = json.last_modified;
that.session.rename_notebook(json.path);
that.events.trigger('notebook_renamed.Notebook', json);
}
@ -2610,6 +2556,8 @@ define(function (require) {
this.scroll_to_top();
this.writable = data.writable || false;
this.last_modified = new Date(data.last_modified);
// debug 484
this._last_modified = 'load-success:'+data.last_modified
var nbmodel = data.content;
var orig_nbformat = nbmodel.metadata.orig_nbformat;
var orig_nbformat_minor = nbmodel.metadata.orig_nbformat_minor;

View File

@ -35,7 +35,7 @@ define([
*/
NotebookNotificationArea.prototype.init_kernel_notification_widget = function () {
var that = this;
var knw = this.new_notification_widget('kernel');
var knw = this.widget('kernel');
var $kernel_ind_icon = $("#kernel_indicator_icon");
var $modal_ind_icon = $("#modal_indicator");
var $readonly_ind_icon = $('#readonly-indicator');
@ -284,7 +284,7 @@ define([
* @method init_notebook_notification_widget
*/
NotebookNotificationArea.prototype.init_notebook_notification_widget = function () {
var nnw = this.new_notification_widget('notebook');
var nnw = this.widget('notebook');
// Notebook events
this.events.on('notebook_loading.Notebook', function () {
@ -346,7 +346,7 @@ define([
* @method init_marked_cells_notification_widget
*/
NotebookNotificationArea.prototype.init_marked_cells_notification_widget = function () {
var mcnw = this.new_notification_widget('marked_cells');
var mcnw = this.widget('marked_cells');
this.events.on('marked_offscreen.Cell', function (evt, num) {
if (num === 0) {

View File

@ -2,7 +2,7 @@
// Distributed under the terms of the Modified BSD License.
define([
'jqueryui',
'jquery-ui',
'base/js/utils',
'base/js/security',
'base/js/keyboard',

View File

@ -2,7 +2,7 @@
// Distributed under the terms of the Modified BSD License.
define([
'jqueryui',
'jquery-ui',
'base/js/utils',
], function($, utils) {
"use strict";

View File

@ -257,8 +257,8 @@ define([
QuickHelp.prototype.build_edit_help = function (cm_shortcuts) {
var edit_shortcuts = this.keyboard_manager.edit_shortcuts.help();
jQuery.merge(cm_shortcuts, edit_shortcuts);
return build_div('<h4>Edit Mode (press <kbd>Enter</kbd> to enable)</h4>', cm_shortcuts);
edit_shortcuts = jQuery.merge(jQuery.merge([], cm_shortcuts), edit_shortcuts);
return build_div('<h4>Edit Mode (press <kbd>Enter</kbd> to enable)</h4>', edit_shortcuts);
};
var build_one = function (s) {

View File

@ -68,7 +68,7 @@ define(function(require){
if(aborted){
body.append($('<p/>').addClass('bg-warning').text("Warning, too many matches ("+html.length+"+), some changes might not be shown or applied"));
} else {
body.append($('<p/>').addClass('bg-info').text(html.length+" match"+(html.length==1?'':'es')));
body.append($('<p/>').text(html.length+" match"+(html.length==1?'':'es')));
}
for(var rindex=0; rindex<html.length; rindex++){
var pre = $('<pre/>')
@ -147,58 +147,60 @@ define(function(require){
* Search N' Replace action handler.
**/
var snr = function(env, event) {
var search = $("<input/>")
.addClass('form-control')
.attr('placeholder','Find');
var isRegExpButton = $('<button/>')
.attr('type', 'button')
.attr('id', 'isreg')
.addClass("btn btn-default")
.addClass("btn btn-default btn-sm")
.attr('data-toggle','button')
.css('font-weight', 'bold')
.attr('title', 'Use regex (JavaScript regex syntax)')
.text('.*');
var onlySelectedButton = $('<button/>')
var onlySelectedButton = $('<button/>')
.append($('<i/>').addClass('fa fa-align-left'))
.attr('type', 'button')
.addClass("btn btn-default")
.append($('<i/>')
.addClass("fa fa-check-square-o")
)
.addClass("btn btn-default btn-sm")
.attr('data-toggle','button')
.attr('title', 'Replace only in selected cells');
.attr('title', 'Replace in selected cells');
var isCaseSensitiveButton = $('<button/>')
.attr('type', 'button')
.addClass("btn btn-default")
.addClass("btn btn-default btn-sm")
.attr('data-toggle','button')
.attr('tabindex', '0')
.attr('title', 'Case sensitive')
.text('a≠A');
.attr('title', 'Match case')
.css('font-weight', 'bold')
.text('Aa');
var search = $("<input/>")
.addClass('form-control input-sm')
.attr('placeholder','Find');
var repl = $("<input/>")
.addClass('form-control')
.attr('placeholder','Replace');
var body = $('<div/>')
.attr('id', 'replace-preview');
var form = $('<form/>')
.attr('id', 'find-and-replace')
.append($('<div/>').addClass('form-group')
var findFormGroup = $('<div/>').addClass('form-group');
findFormGroup.append(
$('<div/>').addClass('input-group')
.append(
$('<div/>').addClass('input-group')
.append(
$('<div/>').addClass('input-group-btn')
.append(isCaseSensitiveButton)
.append(isRegExpButton)
.append(onlySelectedButton)
)
.append(search)
)
)
.append($('<div/>').addClass('form-group')
.append(repl)
)
.append(body);
.append(search)
)
var replace = $("<input/>")
.addClass('form-control input-sm')
.attr('placeholder','Replace');
var replaceFormGroup = $('<div/>').addClass('form-group');
replaceFormGroup.append(replace);
var body = $('<div/>').attr('id', 'replace-preview');
var form = $('<form/>').attr('id', 'find-and-replace')
form.append(findFormGroup);
form.append(replaceFormGroup);
form.append(body);
// return whether the search is case sensitive
var isCaseSensitive = function(){
@ -272,14 +274,14 @@ define(function(require){
}
// might want to warn if replace is empty
var replace = repl.val();
var replaceValue = replace.val();
var lines = get_all_text(get_cells(env));
var _hb = compute_preview_model(sre, lines, isCaseSensitive(), RegExpOrNot, replace);
var _hb = compute_preview_model(sre, lines, isCaseSensitive(), RegExpOrNot, replaceValue);
var html = _hb[0];
var aborted = _hb[1];
build_preview(body, aborted, html, replace);
build_preview(body, aborted, html, replaceValue);
// done on type return false not to submit form
return false;
@ -287,7 +289,7 @@ define(function(require){
var onsubmit = function(event) {
var sre = search.val();
var replace = repl.val();
var replaceValue = replace.val();
if (!sre) {
return false;
}
@ -300,7 +302,7 @@ define(function(require){
for (var c = 0; c < cells.length; c++) {
var cell = cells[c];
var oldvalue = cell.code_mirror.getValue();
var newvalue = oldvalue.replace(reg , replace);
var newvalue = oldvalue.replace(reg , replaceValue);
cell.code_mirror.setValue(newvalue);
if (cell.cell_type === 'markdown') {
cell.rendered = false;
@ -322,19 +324,19 @@ define(function(require){
});
onlySelectedButton.click(function(){
repl.focus();
replace.focus();
setTimeout(function(){onChange();}, 100);
});
search.keypress(function (e) {
if (e.which == 13) {//enter
repl.focus();
replace.focus();
}
});
search.on('input', onChange);
repl.on('input', onChange);
replace.on('input', onChange);
var mod = dialog.modal({
@ -352,7 +354,7 @@ define(function(require){
}
});
repl.keypress(function (e) {
replace.keypress(function (e) {
if (e.which == 13) {//enter
onsubmit();
mod.modal('hide');

View File

@ -118,7 +118,7 @@ define([
// Cell level actions
TextCell.prototype.select = function () {
var cont = Cell.prototype.select.apply(this);
var cont = Cell.prototype.select.apply(this, arguments);
if (cont) {
if (this.mode === 'edit') {
this.code_mirror.refresh();
@ -286,6 +286,9 @@ define([
.addClass('anchor-link')
.attr('href', '#' + hash)
.text('¶')
.on('click',function(){
setTimeout(function(){that.unrender(); that.render()}, 100)
})
);
});
// links in markdown cells should open in new tabs

View File

@ -1,59 +1,65 @@
@_cell_padding_minus_border: @cell_padding - @cell_border_width;
._marked_style(@n) {
border-left-width: @n;
padding-left: @cell_padding - @n;
._selected_style(@c1, @c2, @sep:0, @border_width:@cell_border_width) {
border-left-width: @border_width;
padding-left: @cell_padding - @border_width;
background: linear-gradient(to right, @c1 -40px,@c1 @sep,@c2 @sep,@c2 100%);
}
div.cell {
border: @cell_border_width solid transparent;
.vbox();
.corner-all();
.border-box-sizing();
border-width: @cell_border_width;
border-style: solid;
&.marked {
._marked_style(3px);
border-left-color: @marked_border_color_light;
/* Don't border the cells when printing */
@media print {
border-color: transparent;
}
}
&.selected {
border-color: @border_color;
border-left-color: @marked_border_color;
._marked_style(2px);
&.marked {
._marked_style(4px);
}
/* Don't border the cells when printing */
@media print {
border-color: transparent;
}
}
.edit_mode &.selected {
border-color: @edit_mode_border_color;
/* Don't border the cells when printing */
@media print {
border-color: transparent;
}
}
border-color: transparent;
width: 100%;
padding: @_cell_padding_minus_border;
/* This acts as a spacer between cells, that is outside the border */
margin: 0px;
outline: none;
._selected_style(transparent, transparent, @cell_border_width);
.jupyter-multi-select &.jupyter-soft-selected {
border-left-color: @selected_border_color_light;
border-left-color: @soft_select_color;
._selected_style(@selected_border_color_light, @soft_select_color, 5px, 0px);
@media print {
border-color: transparent;
}
}
&.selected {
border-color: @border_color;
._selected_style(@selected_border_color, transparent, 5px, 0px);
@media print {
border-color: transparent;
}
}
.jupyter-multi-select &.selected.jupyter-soft-selected {
._selected_style(@selected_border_color, @soft_select_color, 7px, 0);
}
.edit_mode &.selected {
border-color: @edit_mode_border_color;
._selected_style(@edit_mode_border_color, transparent, 5px, 0px);
@media print {
border-color: transparent;
}
}
}
.prompt {
/* This needs to be wide enough for 3 digit prompt numbers: In[100]: */
min-width: 14ex;

View File

@ -1,31 +1,37 @@
#find-and-replace {
#replace-preview .match, #replace-preview .insert{
background-color:lightblue;
border-color: darken(lightblue, 20%);
#replace-preview .match, #replace-preview .insert {
background-color: #BBDEFB; // MD Blue 100
border-color: #90CAF9; // MD Blue 200
border-style: solid;
border-width: 1px;
border-radius: @border-radius-base
border-radius: 0px;
}
#replace-preview .replace {
& .match{
//display: none;
background-color:salmon;
text-decoration: line-through;
border-color: darken(salmon, 20%);
& .match {
// text-decoration: line-through;
background-color: #FFCDD2; // MD Red 100
border-color: #EF9A9A; // MD Red 200
border-radius: 0px;
}
& .insert{
background-color: green;
background-color:lightGreen;
border-color: darken(lightGreen, 20%);
& .insert {
background-color: #C8E6C9; // MD Green 100
border-color: #A5D6A7; // MD Green 200
border-radius: 0px;
}
}
& #replace-preview {
#replace-preview {
max-height: 60vh;
overflow: auto;
pre {
padding: 5px 10px;
}
}
}

View File

@ -11,12 +11,17 @@
@code_line_height: 1.21429em; // changed from 1.231 to get 17px even
@code_padding: 0.4em; // 5.6 px
@rendered_html_border_color: black;
@input_prompt_color: navy;
@output_prompt_color: darkred;
@input_prompt_color: #303F9F;
@output_prompt_color: #D84315;
@output_pre_color: black;
@notification_widget_bg: rgba(240, 240, 240, 0.5);
@marked_border_color: #009AF5;
@marked_border_color_light: #7AC7F5;
@edit_mode_border_color: green;
@selected_border_color: #42A5F5;
@selected_border_color_light: #90CAF9;
@soft_select_color: #E3F2FD;
@edit_mode_border_color: #66BB6A;
@cell_padding: 6px;
@cell_border_width: 1px;
@cell_border_width: 1px;

View File

@ -93,7 +93,7 @@ function($, utils) {
ConfigWithDefaults.prototype.get = function(key) {
var that = this;
return this.section.loaded.then(function() {
return this._class_data()[key] || this.defaults[key]
return that._class_data()[key] || that.defaults[key]
});
};

View File

@ -17,7 +17,7 @@ require([
'tree/js/newnotebook',
'auth/js/loginwidget',
// only loaded, not used:
'jqueryui',
'jquery-ui',
'bootstrap',
'custom/custom',
], function(

View File

@ -34,12 +34,17 @@
jquery: 'components/jquery/jquery.min',
bootstrap: 'components/bootstrap/js/bootstrap.min',
bootstraptour: 'components/bootstrap-tour/build/js/bootstrap-tour.min',
jqueryui: 'components/jquery-ui/ui/minified/jquery-ui.min',
'jquery-ui': 'components/jquery-ui/ui/minified/jquery-ui.min',
moment: 'components/moment/moment',
codemirror: 'components/codemirror',
termjs: 'components/term.js/src/term',
typeahead: 'components/jquery-typeahead/dist/jquery.typeahead'
},
map: { // for backward compatibility
"*": {
"jqueryui": "jquery-ui",
}
},
shim: {
typeahead: {
deps: ["jquery"],
@ -60,7 +65,7 @@
deps: ["bootstrap"],
exports: "Tour"
},
jqueryui: {
"jquery-ui": {
deps: ["jquery"],
exports: "$"
}

View File

@ -19,6 +19,7 @@ except ImportError:
from tornado.ioloop import IOLoop
import jupyter_core.paths
from ..notebookapp import NotebookApp
from ipython_genutils.tempdir import TemporaryDirectory
@ -51,9 +52,8 @@ class NotebookTestBase(TestCase):
try:
requests.get(url)
except Exception as e:
if cls.notebook.poll() is not None:
raise RuntimeError("The notebook server exited with status %s" \
% cls.notebook.poll())
if not cls.notebook_thread.is_alive():
raise RuntimeError("The notebook server failed to start")
time.sleep(POLL_INTERVAL)
else:
return
@ -77,31 +77,35 @@ class NotebookTestBase(TestCase):
'JUPYTER_DATA_DIR' : data_dir.name
})
cls.env_patch.start()
cls.path_patch = patch.object(jupyter_core.paths, 'SYSTEM_JUPYTER_PATH', [])
cls.path_patch.start()
cls.config_dir = TemporaryDirectory()
cls.data_dir = data_dir
cls.runtime_dir = TemporaryDirectory()
cls.notebook_dir = TemporaryDirectory()
app = cls.notebook = NotebookApp(
port=cls.port,
port_retries=0,
open_browser=False,
config_dir=cls.config_dir.name,
data_dir=cls.data_dir.name,
runtime_dir=cls.runtime_dir.name,
notebook_dir=cls.notebook_dir.name,
base_url=cls.url_prefix,
config=cls.config,
)
# clear log handlers and propagate to root for nose to capture it
# needs to be redone after initialize, which reconfigures logging
app.log.propagate = True
app.log.handlers = []
app.initialize(argv=[])
app.log.propagate = True
app.log.handlers = []
started = Event()
def start_thread():
app = cls.notebook = NotebookApp(
port=cls.port,
port_retries=0,
open_browser=False,
config_dir=cls.config_dir.name,
data_dir=cls.data_dir.name,
runtime_dir=cls.runtime_dir.name,
notebook_dir=cls.notebook_dir.name,
base_url=cls.url_prefix,
config=cls.config,
)
# don't register signal handler during tests
app.init_signal = lambda : None
# clear log handlers and propagate to root for nose to capture it
# needs to be redone after initialize, which reconfigures logging
app.log.propagate = True
app.log.handlers = []
app.initialize(argv=[])
app.log.propagate = True
app.log.handlers = []
loop = IOLoop.current()
loop.add_callback(started.set)
try:
@ -109,6 +113,7 @@ class NotebookTestBase(TestCase):
finally:
# set the event, so failure to start doesn't cause a hang
started.set()
app.session_manager.close()
cls.notebook_thread = Thread(target=start_thread)
cls.notebook_thread.start()
started.wait()
@ -118,12 +123,13 @@ class NotebookTestBase(TestCase):
def teardown_class(cls):
cls.notebook.stop()
cls.wait_until_dead()
cls.env_patch.start()
cls.home_dir.cleanup()
cls.config_dir.cleanup()
cls.data_dir.cleanup()
cls.runtime_dir.cleanup()
cls.notebook_dir.cleanup()
cls.env_patch.stop()
cls.path_patch.stop()
@classmethod
def base_url(cls):

View File

@ -41,7 +41,7 @@ casper.notebook_test(function () {
this.test.assertEquals(this.get_cell_text(1), 'cd', 'split; Verify that cell 1 has the second half.');
this.validate_notebook_state('split', 'edit', 1);
this.select_cell(0); // Move up to cell 0
this.evaluate(function() { IPython.notebook.extend_marked(1);});
this.evaluate(function() { IPython.notebook.extend_selection_by(1);});
this.trigger_keydown('shift-m'); // Merge
this.validate_notebook_state('merge', 'command', 0);
this.test.assertEquals(this.get_cell_text(0), a, 'merge; Verify that cell 0 has the merged contents.');

View File

@ -3,16 +3,18 @@
//
casper.notebook_test(function () {
var that = this;
var assert_outputs = function (expected) {
var assert_outputs = function (expected, msg_prefix) {
var msg, i;
msg_prefix = "(assert_outputs) "+(msg_prefix || 'no prefix')+": ";
for (i = 0; i < that.get_cells_length(); i++) {
if (expected[i] === undefined) {
msg = 'cell ' + i + ' not executed';
msg = msg_prefix + 'cell ' + i + ' not executed';
that.test.assertFalse(that.cell_has_outputs(i), msg);
} else {
msg = 'cell ' + i + ' executed';
that.test.assertEquals(that.get_output_cell(i).text, expected[i], msg);
msg = msg_prefix + 'cell ' + i + ' executed';
var out = that.get_output_cell(i, undefined, msg_prefix).text
that.test.assertEquals(out, expected[i], msg + 'out is: '+out);
}
}
};
@ -23,20 +25,24 @@ casper.notebook_test(function () {
this.append_cell('print("c")');
this.append_cell('print("d")');
this.test.assertEquals(this.get_cells_length(), 4, "correct number of cells");
});
this.evaluate(function () {
IPython.notebook.unmark_all_cells();
IPython.notebook.set_marked_indices([1, 2]);
});
this.then(function () {
this.select_cell(1);
this.select_cell(2, false);
});
this.then(function () {
this.evaluate(function () {
IPython.notebook.clear_all_output();
});
})
this.then(function(){
this.select_cell(1);
this.validate_notebook_state('before execute', 'command', 1);
this.validate_notebook_state('before execute 1', 'command', 1);
this.select_cell(1);
this.select_cell(2, false);
this.trigger_keydown('ctrl-enter');
});
@ -44,48 +50,29 @@ casper.notebook_test(function () {
this.wait_for_output(2);
this.then(function () {
assert_outputs([undefined, 'b\n', 'c\n', undefined]);
this.validate_notebook_state('run marked cells', 'command', 2);
assert_outputs([undefined, 'b\n', 'c\n', undefined], 'run selected 1');
this.validate_notebook_state('run selected cells 1', 'command', 2);
});
// execute cells in place when there are marked cells
// execute and insert below when there are selected cells
this.then(function () {
this.evaluate(function () {
IPython.notebook.clear_all_output();
});
this.select_cell(1);
this.validate_notebook_state('before execute', 'command', 1);
this.trigger_keydown('shift-enter');
});
this.wait_for_output(1);
this.wait_for_output(2);
this.then(function () {
assert_outputs([undefined, 'b\n', 'c\n', undefined]);
this.validate_notebook_state('run marked cells', 'command', 2);
});
// execute and insert below when there are marked cells
this.then(function () {
this.evaluate(function () {
IPython.notebook.clear_all_output();
});
this.select_cell(1);
this.validate_notebook_state('before execute', 'command', 1);
this.validate_notebook_state('before execute 2', 'command', 1);
this.evaluate(function () {
$("#run_cell_insert_below").click();
});
});
this.wait_for_output(1);
this.wait_for_output(2);
this.then(function () {
assert_outputs([undefined, 'b\n', 'c\n', undefined]);
this.validate_notebook_state('run marked cells', 'command', 2);
assert_outputs([undefined, 'b\n', undefined, undefined , undefined],'run selected cells 2');
this.validate_notebook_state('run selected cells 2', 'edit', 2);
});
// check that it doesn't affect run all above
@ -95,7 +82,7 @@ casper.notebook_test(function () {
});
this.select_cell(1);
this.validate_notebook_state('before execute', 'command', 1);
this.validate_notebook_state('before execute 3', 'command', 1);
this.evaluate(function () {
$("#run_all_cells_above").click();
});
@ -104,7 +91,7 @@ casper.notebook_test(function () {
this.wait_for_output(0);
this.then(function () {
assert_outputs(['a\n', undefined, undefined, undefined]);
assert_outputs(['a\n', undefined, undefined, undefined],'run cells above');
this.validate_notebook_state('run cells above', 'command', 0);
});
@ -115,7 +102,7 @@ casper.notebook_test(function () {
});
this.select_cell(1);
this.validate_notebook_state('before execute', 'command', 1);
this.validate_notebook_state('before execute 4', 'command', 1);
this.evaluate(function () {
$("#run_all_cells_below").click();
});
@ -126,8 +113,8 @@ casper.notebook_test(function () {
this.wait_for_output(3);
this.then(function () {
assert_outputs([undefined, 'b\n', 'c\n', 'd\n']);
this.validate_notebook_state('run cells below', 'command', 3);
assert_outputs([undefined, 'b\n', undefined, 'c\n', 'd\n'],'run cells below');
this.validate_notebook_state('run cells below', 'command', 4);
});
// check that it doesn't affect run all
@ -137,7 +124,7 @@ casper.notebook_test(function () {
});
this.select_cell(1);
this.validate_notebook_state('before execute', 'command', 1);
this.validate_notebook_state('before execute 5', 'command', 1);
this.evaluate(function () {
$("#run_all_cells").click();
});
@ -149,7 +136,7 @@ casper.notebook_test(function () {
this.wait_for_output(3);
this.then(function () {
assert_outputs(['a\n', 'b\n', 'c\n', 'd\n']);
this.validate_notebook_state('run all cells', 'command', 3);
assert_outputs(['a\n', 'b\n', undefined, 'c\n', 'd\n'],'run all cells');
this.validate_notebook_state('run all cells', 'command', 4);
});
});

View File

@ -1,73 +0,0 @@
// Test
casper.notebook_test(function () {
var that = this;
var a = 'print("a")';
var index = this.append_cell(a);
var b = 'print("b")';
index = this.append_cell(b);
var c = 'print("c")';
index = this.append_cell(c);
this.then(function () {
var selectedIndex = this.evaluate(function () {
Jupyter.notebook.select(0);
return Jupyter.notebook.get_selected_index();
});
this.test.assertEquals(this.evaluate(function() {
return Jupyter.notebook.get_marked_cells().length;
}), 1, 'only one cell is marked programmatically');
this.test.assertEquals(this.evaluate(function() {
return Jupyter.notebook.get_marked_indices()[0];
}), selectedIndex, 'marked cell is selected cell');
this.test.assertEquals(this.evaluate(function() {
return $('.cell.marked').length;
}), 0, 'no cells are marked visibily');
this.evaluate(function() {
Jupyter.notebook.mark_all_cells();
});
var cellCount = this.evaluate(function() {
return Jupyter.notebook.ncells();
});
this.test.assertEquals(this.evaluate(function() {
return Jupyter.notebook.get_marked_cells().length;
}), cellCount, 'mark_all');
this.test.assertEquals(this.evaluate(function() {
return $('.cell.marked').length;
}), cellCount, 'marked cells are marked visibily');
this.evaluate(function() {
Jupyter.notebook.unmark_all_cells();
});
this.test.assertEquals(this.evaluate(function() {
return Jupyter.notebook.get_marked_cells().length;
}), 1, 'unmark_all');
this.test.assertEquals(this.evaluate(function() {
return Jupyter.notebook.get_marked_indices()[0];
}), selectedIndex, 'marked cell is selected cell');
this.evaluate(function() {
Jupyter.notebook.set_marked_indices([1]);
});
this.test.assertEquals(this.evaluate(function() {
return Jupyter.notebook.get_marked_cells().length;
}), 2, 'two cells are marked');
this.test.assertEquals(this.evaluate(function() {
return Jupyter.notebook.get_marked_indices();
}), [selectedIndex, 1], 'get/set_marked_indices');
});
});

View File

@ -0,0 +1,49 @@
// Test
casper.notebook_test(function () {
var that = this;
var a = 'print("a")';
var index = this.append_cell(a);
var b = 'print("b")';
index = this.append_cell(b);
var c = 'print("c")';
index = this.append_cell(c);
this.then(function () {
var selectedIndex = this.evaluate(function () {
Jupyter.notebook.select(0);
return Jupyter.notebook.get_selected_index();
});
this.test.assertEquals(this.evaluate(function() {
return Jupyter.notebook.get_selected_cells().length;
}), 1, 'only one cell is selected programmatically');
this.test.assertEquals(this.evaluate(function() {
return $('.cell.jupyter-soft-selected').length;
}), 1, 'one cell is selected');
this.test.assertEquals(this.evaluate(function() {
Jupyter.notebook.extend_selection_by(1);
return Jupyter.notebook.get_selected_cells().length;
}), 2, 'extend selection by one');
this.test.assertEquals(this.evaluate(function() {
Jupyter.notebook.extend_selection_by(-1);
return Jupyter.notebook.get_selected_cells().length;
}), 1, 'contract selection by one');
this.test.assertEquals(this.evaluate(function() {
Jupyter.notebook.select(1);
Jupyter.notebook.extend_selection_by(-1);
return Jupyter.notebook.get_selected_cells().length;
}), 2, 'extend selection by one up');
});
});

View File

@ -4,11 +4,11 @@
casper.notebook_test(function () {
var that = this;
var assert_marked_cells = function (action, indices) {
var marked = that.evaluate(function () {
return IPython.notebook.get_marked_indices();
var assert_selected_cells = function (action, indices) {
var selected = that.evaluate(function () {
return IPython.notebook.get_selected_cells_indices();
});
that.test.assertEquals(marked, indices, action + "; verify marked cells");
that.test.assertEquals( selected, indices, action + "; verify selected cells");
};
var assert_cells = function (action, cells, index) {
@ -22,7 +22,7 @@ casper.notebook_test(function () {
}
that.validate_notebook_state(action, 'command', index);
assert_marked_cells(action, [index]);
assert_selected_cells(action, [index]);
};
var a = 'print("a")';
@ -59,7 +59,7 @@ casper.notebook_test(function () {
this.select_cell(1);
this.trigger_keydown('esc');
this.trigger_keydown('shift-j');
assert_marked_cells("select cells 1-2", [1, 2]);
assert_selected_cells("select cells 1-2", [1, 2]);
this.trigger_keydown('shift-m');
this.trigger_keydown('esc');
assert_cells("merge cells 1-2", [a, bc, d], 1);
@ -75,7 +75,7 @@ casper.notebook_test(function () {
this.select_cell(3);
this.trigger_keydown('esc');
this.trigger_keydown('shift-k');
assert_marked_cells("select cells 3-2", [2, 3]);
assert_selected_cells("select cells 3-2", [2, 3]);
this.trigger_keydown('shift-m');
this.trigger_keydown('esc');
assert_cells("merge cells 3-2", [a, bc, cd], 2);

View File

@ -166,11 +166,15 @@ casper.wait_for_output = function (cell_num, out_num) {
},
// pass parameter from the test suite js to the browser code js
{c : cell_num, o : out_num});
},
function then() { },
function timeout() {
this.echo("wait_for_output timed out on cell "+cell_num+", waiting for "+out_num+" outputs .");
var pn = this.evaluate(function get_prompt(c) {
return (IPython.notebook.get_cell(c)|| {'input_prompt_number':'no cell'}).input_prompt_number;
});
this.echo("cell prompt was :'"+pn+"'.");
});
},
function then() { },
function timeout() {
this.echo("wait_for_output timed out!");
});
};
@ -225,7 +229,8 @@ casper.cell_has_outputs = function (cell_num) {
return result > 0;
};
casper.get_output_cell = function (cell_num, out_num) {
casper.get_output_cell = function (cell_num, out_num, message) {
messsge = message+': ' ||'no category :'
// return an output of a given cell
out_num = out_num || 0;
var result = casper.evaluate(function (c, o) {
@ -240,7 +245,7 @@ casper.get_output_cell = function (cell_num, out_num) {
},
{c : cell_num});
this.test.assertTrue(false,
"Cell " + cell_num + " has no output #" + out_num + " (" + num_outputs + " total)"
message+"Cell " + cell_num + " has no output #" + out_num + " (" + num_outputs + " total)"
);
} else {
return result;
@ -390,14 +395,19 @@ casper.cell_element_function = function(index, selector, function_name, function
casper.validate_notebook_state = function(message, mode, cell_index) {
// Validate the entire dual mode state of the notebook. Make sure no more than
// one cell is selected, focused, in edit mode, etc...
// General tests.
this.test.assertEquals(this.get_keyboard_mode(), this.get_notebook_mode(),
message + '; keyboard and notebook modes match');
// Is the selected cell the only cell that is selected?
if (cell_index!==undefined) {
this.test.assert(this.is_only_cell_selected(cell_index),
message + '; cell ' + cell_index + ' is the only cell selected');
message + '; expecting cell ' + cell_index + ' to be the only cell selected. Got selected cell(s):'+
(function(){
return casper.evaluate(function(){
return IPython.notebook.get_selected_cells_indices();
})
})()
);
}
// Mode specific tests.
@ -429,11 +439,11 @@ casper.validate_notebook_state = function(message, mode, cell_index) {
}
};
casper.select_cell = function(index) {
casper.select_cell = function(index, moveanchor) {
// Select a cell in the notebook.
this.evaluate(function (i) {
IPython.notebook.select(i);
}, {i: index});
this.evaluate(function (i, moveanchor) {
IPython.notebook.select(i, moveanchor);
}, {i: index, moveanchor: moveanchor});
};
casper.click_cell_editor = function(index) {

View File

@ -93,7 +93,6 @@ for more information.
'Programming Language :: Python',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.3',
],
)

View File

@ -18,13 +18,18 @@ var rjs_config = {
jquery: 'components/jquery/jquery.min',
bootstrap: 'components/bootstrap/js/bootstrap.min',
bootstraptour: 'components/bootstrap-tour/build/js/bootstrap-tour.min',
jqueryui: 'components/jquery-ui/ui/minified/jquery-ui.min',
"jquery-ui": 'components/jquery-ui/ui/minified/jquery-ui.min',
moment: 'components/moment/moment',
codemirror: 'components/codemirror',
termjs: 'components/term.js/src/term',
typeahead: 'components/jquery-typeahead/dist/jquery.typeahead',
contents: 'empty:'
},
map: { // for backward compatibility
"*": {
"jqueryui": "jquery-ui",
}
},
shim: {
typeahead: {
deps: ["jquery"],
@ -45,7 +50,7 @@ var rjs_config = {
deps: ["bootstrap"],
exports: "Tour"
},
jqueryui: {
"jquery-ui": {
deps: ["jquery"],
exports: "$"
}

101
tools/secure_notebook.py Normal file
View File

@ -0,0 +1,101 @@
#!/usr/bin/env python
from notebook.auth import passwd
from traitlets.config.loader import JSONFileConfigLoader, ConfigFileNotFound
import six
from jupyter_core.paths import jupyter_config_dir
from traitlets.config import Config
from OpenSSL import crypto, SSL
from socket import gethostname
from pprint import pprint
from time import gmtime, mktime
from os.path import exists, join
import io
import os
import json
def create_self_signed_cert(cert_dir, keyfile, certfiile):
"""
If datacard.crt and datacard.key don't exist in cert_dir, create a new
self-signed cert and keypair and write them into that directory.
"""
if not exists(join(cert_dir, certfiile)) \
or not exists(join(cert_dir, keyfile)):
# create a key pair
k = crypto.PKey()
k.generate_key(crypto.TYPE_RSA, 1024)
# create a self-signed cert
cert = crypto.X509()
cert.get_subject().C = "US"
cert.get_subject().ST = "Jupyter notebook self-signed certificate"
cert.get_subject().L = "Jupyter notebook self-signed certificate"
cert.get_subject().O = "Jupyter notebook self-signed certificate"
cert.get_subject().OU = "my organization"
cert.get_subject().CN = "Jupyter notebook self-signed certificate"
cert.set_serial_number(1000)
cert.gmtime_adj_notBefore(0)
cert.gmtime_adj_notAfter(365*24*60*60)
cert.set_issuer(cert.get_subject())
cert.set_pubkey(k)
cert.sign(k, 'sha256')
with io.open(join(cert_dir, certfile), "wt") as f:
f.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert).decode('utf8'))
with io.open(join(cert_dir, keyfile), "wt") as f:
f.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, k).decode('utf8'))
if __name__ == '__main__':
print("This guide you into securing your notebook server")
print("first choose a password.")
pw = passwd()
print("We will store your password encrypted in the notebook configuration file: ")
print(pw)
loader = JSONFileConfigLoader('jupyter_notebook_config.json', jupyter_config_dir())
try:
config = loader.load_config()
except ConfigFileNotFound:
config = Config()
config.NotebookApp.password = pw
with io.open(os.path.join(jupyter_config_dir(), 'jupyter_notebook_config.json'), 'w') as f:
f.write(six.u(json.dumps(config, indent=2)))
print('... done')
print()
print("Now let's generate self-signed certificates to secure your connexion.")
print("where should the certificate live?")
location = input('path [~/.ssh]: ')
if not location.strip():
location = os.path.expanduser('~/.ssh')
keyfile = input('keyfile name [jupyter_server.key]: ')
if not keyfile.strip():
keyfile = 'jupyter_server.key'
certfile = input('certfile name [jupyter_server.crt]: ')
if not certfile.strip():
certfile = 'jupyter_server.crt'
create_self_signed_cert(location, keyfile, certfile)
fullkey = os.path.join(location, keyfile)
fullcrt = os.path.join(location, certfile)
config.NotebookApp.certfile = fullcrt
config.NotebookApp.keyfile = fullkey
with io.open(os.path.join(jupyter_config_dir(), 'jupyter_notebook_config.json'), 'w') as f:
f.write(six.u(json.dumps(config, indent=2)))