##// END OF EJS Templates
Merge pull request #3743 from minrk/noro...
Matthias Bussonnier -
r11691:c2464fa2 merge
parent child Browse files
Show More
@@ -1,450 +1,415 b''
1 1 """Base Tornado handlers for the notebook.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2011 The IPython Development Team
10 10 #
11 11 # Distributed under the terms of the BSD License. The full license is in
12 12 # the file COPYING, distributed as part of this software.
13 13 #-----------------------------------------------------------------------------
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18
19 19
20 20 import datetime
21 21 import email.utils
22 22 import hashlib
23 23 import logging
24 24 import mimetypes
25 25 import os
26 26 import stat
27 27 import threading
28 28
29 29 from tornado import web
30 30 from tornado import websocket
31 31
32 32 try:
33 33 from tornado.log import app_log
34 34 except ImportError:
35 35 app_log = logging.getLogger()
36 36
37 37 from IPython.config import Application
38 38 from IPython.external.decorator import decorator
39 39 from IPython.utils.path import filefind
40 40
41 41 #-----------------------------------------------------------------------------
42 42 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
43 43 #-----------------------------------------------------------------------------
44 44
45 45 # Google Chrome, as of release 16, changed its websocket protocol number. The
46 46 # parts tornado cares about haven't really changed, so it's OK to continue
47 47 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
48 48 # version as of Oct 30/2011) the version check fails, see the issue report:
49 49
50 50 # https://github.com/facebook/tornado/issues/385
51 51
52 52 # This issue has been fixed in Tornado post 2.1.1:
53 53
54 54 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
55 55
56 56 # Here we manually apply the same patch as above so that users of IPython can
57 57 # continue to work with an officially released Tornado. We make the
58 58 # monkeypatch version check as narrow as possible to limit its effects; once
59 59 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
60 60
61 61 import tornado
62 62
63 63 if tornado.version_info <= (2,1,1):
64 64
65 65 def _execute(self, transforms, *args, **kwargs):
66 66 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
67 67
68 68 self.open_args = args
69 69 self.open_kwargs = kwargs
70 70
71 71 # The difference between version 8 and 13 is that in 8 the
72 72 # client sends a "Sec-Websocket-Origin" header and in 13 it's
73 73 # simply "Origin".
74 74 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
75 75 self.ws_connection = WebSocketProtocol8(self)
76 76 self.ws_connection.accept_connection()
77 77
78 78 elif self.request.headers.get("Sec-WebSocket-Version"):
79 79 self.stream.write(tornado.escape.utf8(
80 80 "HTTP/1.1 426 Upgrade Required\r\n"
81 81 "Sec-WebSocket-Version: 8\r\n\r\n"))
82 82 self.stream.close()
83 83
84 84 else:
85 85 self.ws_connection = WebSocketProtocol76(self)
86 86 self.ws_connection.accept_connection()
87 87
88 88 websocket.WebSocketHandler._execute = _execute
89 89 del _execute
90 90
91 #-----------------------------------------------------------------------------
92 # Decorator for disabling read-only handlers
93 #-----------------------------------------------------------------------------
94
95 @decorator
96 def not_if_readonly(f, self, *args, **kwargs):
97 if self.settings.get('read_only', False):
98 raise web.HTTPError(403, "Notebook server is read-only")
99 else:
100 return f(self, *args, **kwargs)
101
102 @decorator
103 def authenticate_unless_readonly(f, self, *args, **kwargs):
104 """authenticate this page *unless* readonly view is active.
105
106 In read-only mode, the notebook list and print view should
107 be accessible without authentication.
108 """
109
110 @web.authenticated
111 def auth_f(self, *args, **kwargs):
112 return f(self, *args, **kwargs)
113
114 if self.settings.get('read_only', False):
115 return f(self, *args, **kwargs)
116 else:
117 return auth_f(self, *args, **kwargs)
118 91
119 92 #-----------------------------------------------------------------------------
120 93 # Top-level handlers
121 94 #-----------------------------------------------------------------------------
122 95
123 96 class RequestHandler(web.RequestHandler):
124 97 """RequestHandler with default variable setting."""
125 98
126 99 def render(*args, **kwargs):
127 100 kwargs.setdefault('message', '')
128 101 return web.RequestHandler.render(*args, **kwargs)
129 102
130 103 class AuthenticatedHandler(RequestHandler):
131 104 """A RequestHandler with an authenticated user."""
132 105
133 106 def clear_login_cookie(self):
134 107 self.clear_cookie(self.cookie_name)
135 108
136 109 def get_current_user(self):
137 110 user_id = self.get_secure_cookie(self.cookie_name)
138 111 # For now the user_id should not return empty, but it could eventually
139 112 if user_id == '':
140 113 user_id = 'anonymous'
141 114 if user_id is None:
142 115 # prevent extra Invalid cookie sig warnings:
143 116 self.clear_login_cookie()
144 if not self.read_only and not self.login_available:
117 if not self.login_available:
145 118 user_id = 'anonymous'
146 119 return user_id
147 120
148 121 @property
149 122 def cookie_name(self):
150 123 default_cookie_name = 'username-{host}'.format(
151 124 host=self.request.host,
152 125 ).replace(':', '-')
153 126 return self.settings.get('cookie_name', default_cookie_name)
154 127
155 128 @property
156 129 def password(self):
157 130 """our password"""
158 131 return self.settings.get('password', '')
159 132
160 133 @property
161 134 def logged_in(self):
162 135 """Is a user currently logged in?
163 136
164 137 """
165 138 user = self.get_current_user()
166 139 return (user and not user == 'anonymous')
167 140
168 141 @property
169 142 def login_available(self):
170 143 """May a user proceed to log in?
171 144
172 145 This returns True if login capability is available, irrespective of
173 146 whether the user is already logged in or not.
174 147
175 148 """
176 149 return bool(self.settings.get('password', ''))
177 150
178 @property
179 def read_only(self):
180 """Is the notebook read-only?
181
182 """
183 return self.settings.get('read_only', False)
184
185 151
186 152 class IPythonHandler(AuthenticatedHandler):
187 153 """IPython-specific extensions to authenticated handling
188 154
189 155 Mostly property shortcuts to IPython-specific settings.
190 156 """
191 157
192 158 @property
193 159 def config(self):
194 160 return self.settings.get('config', None)
195 161
196 162 @property
197 163 def log(self):
198 164 """use the IPython log by default, falling back on tornado's logger"""
199 165 if Application.initialized():
200 166 return Application.instance().log
201 167 else:
202 168 return app_log
203 169
204 170 @property
205 171 def use_less(self):
206 172 """Use less instead of css in templates"""
207 173 return self.settings.get('use_less', False)
208 174
209 175 #---------------------------------------------------------------
210 176 # URLs
211 177 #---------------------------------------------------------------
212 178
213 179 @property
214 180 def ws_url(self):
215 181 """websocket url matching the current request
216 182
217 183 By default, this is just `''`, indicating that it should match
218 184 the same host, protocol, port, etc.
219 185 """
220 186 return self.settings.get('websocket_url', '')
221 187
222 188 @property
223 189 def mathjax_url(self):
224 190 return self.settings.get('mathjax_url', '')
225 191
226 192 @property
227 193 def base_project_url(self):
228 194 return self.settings.get('base_project_url', '/')
229 195
230 196 @property
231 197 def base_kernel_url(self):
232 198 return self.settings.get('base_kernel_url', '/')
233 199
234 200 #---------------------------------------------------------------
235 201 # Manager objects
236 202 #---------------------------------------------------------------
237 203
238 204 @property
239 205 def kernel_manager(self):
240 206 return self.settings['kernel_manager']
241 207
242 208 @property
243 209 def notebook_manager(self):
244 210 return self.settings['notebook_manager']
245 211
246 212 @property
247 213 def cluster_manager(self):
248 214 return self.settings['cluster_manager']
249 215
250 216 @property
251 217 def project(self):
252 218 return self.notebook_manager.notebook_dir
253 219
254 220 #---------------------------------------------------------------
255 221 # template rendering
256 222 #---------------------------------------------------------------
257 223
258 224 def get_template(self, name):
259 225 """Return the jinja template object for a given name"""
260 226 return self.settings['jinja2_env'].get_template(name)
261 227
262 228 def render_template(self, name, **ns):
263 229 ns.update(self.template_namespace)
264 230 template = self.get_template(name)
265 231 return template.render(**ns)
266 232
267 233 @property
268 234 def template_namespace(self):
269 235 return dict(
270 236 base_project_url=self.base_project_url,
271 237 base_kernel_url=self.base_kernel_url,
272 read_only=self.read_only,
273 238 logged_in=self.logged_in,
274 239 login_available=self.login_available,
275 240 use_less=self.use_less,
276 241 )
277 242
278 243 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
279 244 """static files should only be accessible when logged in"""
280 245
281 @authenticate_unless_readonly
246 @web.authenticated
282 247 def get(self, path):
283 248 return web.StaticFileHandler.get(self, path)
284 249
285 250
286 251 #-----------------------------------------------------------------------------
287 252 # File handler
288 253 #-----------------------------------------------------------------------------
289 254
290 255 # to minimize subclass changes:
291 256 HTTPError = web.HTTPError
292 257
293 258 class FileFindHandler(web.StaticFileHandler):
294 259 """subclass of StaticFileHandler for serving files from a search path"""
295 260
296 261 _static_paths = {}
297 262 # _lock is needed for tornado < 2.2.0 compat
298 263 _lock = threading.Lock() # protects _static_hashes
299 264
300 265 def initialize(self, path, default_filename=None):
301 266 if isinstance(path, basestring):
302 267 path = [path]
303 268 self.roots = tuple(
304 269 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in path
305 270 )
306 271 self.default_filename = default_filename
307 272
308 273 @classmethod
309 274 def locate_file(cls, path, roots):
310 275 """locate a file to serve on our static file search path"""
311 276 with cls._lock:
312 277 if path in cls._static_paths:
313 278 return cls._static_paths[path]
314 279 try:
315 280 abspath = os.path.abspath(filefind(path, roots))
316 281 except IOError:
317 282 # empty string should always give exists=False
318 283 return ''
319 284
320 285 # os.path.abspath strips a trailing /
321 286 # it needs to be temporarily added back for requests to root/
322 287 if not (abspath + os.path.sep).startswith(roots):
323 288 raise HTTPError(403, "%s is not in root static directory", path)
324 289
325 290 cls._static_paths[path] = abspath
326 291 return abspath
327 292
328 293 def get(self, path, include_body=True):
329 294 path = self.parse_url_path(path)
330 295
331 296 # begin subclass override
332 297 abspath = self.locate_file(path, self.roots)
333 298 # end subclass override
334 299
335 300 if os.path.isdir(abspath) and self.default_filename is not None:
336 301 # need to look at the request.path here for when path is empty
337 302 # but there is some prefix to the path that was already
338 303 # trimmed by the routing
339 304 if not self.request.path.endswith("/"):
340 305 self.redirect(self.request.path + "/")
341 306 return
342 307 abspath = os.path.join(abspath, self.default_filename)
343 308 if not os.path.exists(abspath):
344 309 raise HTTPError(404)
345 310 if not os.path.isfile(abspath):
346 311 raise HTTPError(403, "%s is not a file", path)
347 312
348 313 stat_result = os.stat(abspath)
349 314 modified = datetime.datetime.utcfromtimestamp(stat_result[stat.ST_MTIME])
350 315
351 316 self.set_header("Last-Modified", modified)
352 317
353 318 mime_type, encoding = mimetypes.guess_type(abspath)
354 319 if mime_type:
355 320 self.set_header("Content-Type", mime_type)
356 321
357 322 cache_time = self.get_cache_time(path, modified, mime_type)
358 323
359 324 if cache_time > 0:
360 325 self.set_header("Expires", datetime.datetime.utcnow() + \
361 326 datetime.timedelta(seconds=cache_time))
362 327 self.set_header("Cache-Control", "max-age=" + str(cache_time))
363 328 else:
364 329 self.set_header("Cache-Control", "public")
365 330
366 331 self.set_extra_headers(path)
367 332
368 333 # Check the If-Modified-Since, and don't send the result if the
369 334 # content has not been modified
370 335 ims_value = self.request.headers.get("If-Modified-Since")
371 336 if ims_value is not None:
372 337 date_tuple = email.utils.parsedate(ims_value)
373 338 if_since = datetime.datetime(*date_tuple[:6])
374 339 if if_since >= modified:
375 340 self.set_status(304)
376 341 return
377 342
378 343 with open(abspath, "rb") as file:
379 344 data = file.read()
380 345 hasher = hashlib.sha1()
381 346 hasher.update(data)
382 347 self.set_header("Etag", '"%s"' % hasher.hexdigest())
383 348 if include_body:
384 349 self.write(data)
385 350 else:
386 351 assert self.request.method == "HEAD"
387 352 self.set_header("Content-Length", len(data))
388 353
389 354 @classmethod
390 355 def get_version(cls, settings, path):
391 356 """Generate the version string to be used in static URLs.
392 357
393 358 This method may be overridden in subclasses (but note that it
394 359 is a class method rather than a static method). The default
395 360 implementation uses a hash of the file's contents.
396 361
397 362 ``settings`` is the `Application.settings` dictionary and ``path``
398 363 is the relative location of the requested asset on the filesystem.
399 364 The returned value should be a string, or ``None`` if no version
400 365 could be determined.
401 366 """
402 367 # begin subclass override:
403 368 static_paths = settings['static_path']
404 369 if isinstance(static_paths, basestring):
405 370 static_paths = [static_paths]
406 371 roots = tuple(
407 372 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in static_paths
408 373 )
409 374
410 375 try:
411 376 abs_path = filefind(path, roots)
412 377 except IOError:
413 378 app_log.error("Could not find static file %r", path)
414 379 return None
415 380
416 381 # end subclass override
417 382
418 383 with cls._lock:
419 384 hashes = cls._static_hashes
420 385 if abs_path not in hashes:
421 386 try:
422 387 f = open(abs_path, "rb")
423 388 hashes[abs_path] = hashlib.md5(f.read()).hexdigest()
424 389 f.close()
425 390 except Exception:
426 391 app_log.error("Could not open static file %r", path)
427 392 hashes[abs_path] = None
428 393 hsh = hashes.get(abs_path)
429 394 if hsh:
430 395 return hsh[:5]
431 396 return None
432 397
433 398
434 399 def parse_url_path(self, url_path):
435 400 """Converts a static URL path into a filesystem path.
436 401
437 402 ``url_path`` is the path component of the URL with
438 403 ``static_url_prefix`` removed. The return value should be
439 404 filesystem path relative to ``static_path``.
440 405 """
441 406 if os.path.sep != "/":
442 407 url_path = url_path.replace("/", os.path.sep)
443 408 return url_path
444 409
445 410 #-----------------------------------------------------------------------------
446 411 # URL to handler mappings
447 412 #-----------------------------------------------------------------------------
448 413
449 414
450 415 default_handlers = []
@@ -1,91 +1,91 b''
1 1 """Tornado handlers for the live notebook view.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2011 The IPython Development Team
10 10 #
11 11 # Distributed under the terms of the BSD License. The full license is in
12 12 # the file COPYING, distributed as part of this software.
13 13 #-----------------------------------------------------------------------------
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18
19 19 import os
20 20 from tornado import web
21 21 HTTPError = web.HTTPError
22 22
23 from ..base.handlers import IPythonHandler, authenticate_unless_readonly
23 from ..base.handlers import IPythonHandler
24 24 from ..utils import url_path_join
25 25
26 26 #-----------------------------------------------------------------------------
27 27 # Handlers
28 28 #-----------------------------------------------------------------------------
29 29
30 30
31 31 class NewHandler(IPythonHandler):
32 32
33 33 @web.authenticated
34 34 def get(self):
35 35 notebook_id = self.notebook_manager.new_notebook()
36 36 self.redirect(url_path_join(self.base_project_url, notebook_id))
37 37
38 38
39 39 class NamedNotebookHandler(IPythonHandler):
40 40
41 @authenticate_unless_readonly
41 @web.authenticated
42 42 def get(self, notebook_id):
43 43 nbm = self.notebook_manager
44 44 if not nbm.notebook_exists(notebook_id):
45 45 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
46 46 self.write(self.render_template('notebook.html',
47 47 project=self.project,
48 48 notebook_id=notebook_id,
49 49 kill_kernel=False,
50 50 mathjax_url=self.mathjax_url,
51 51 )
52 52 )
53 53
54 54
55 55 class NotebookRedirectHandler(IPythonHandler):
56 56
57 @authenticate_unless_readonly
57 @web.authenticated
58 58 def get(self, notebook_name):
59 59 # strip trailing .ipynb:
60 60 notebook_name = os.path.splitext(notebook_name)[0]
61 61 notebook_id = self.notebook_manager.rev_mapping.get(notebook_name, '')
62 62 if notebook_id:
63 63 url = url_path_join(self.settings.get('base_project_url', '/'), notebook_id)
64 64 return self.redirect(url)
65 65 else:
66 66 raise HTTPError(404)
67 67
68 68
69 69 class NotebookCopyHandler(IPythonHandler):
70 70
71 71 @web.authenticated
72 72 def get(self, notebook_id):
73 73 notebook_id = self.notebook_manager.copy_notebook(notebook_id)
74 74 self.redirect(url_path_join(self.base_project_url, notebook_id))
75 75
76 76
77 77 #-----------------------------------------------------------------------------
78 78 # URL to handler mappings
79 79 #-----------------------------------------------------------------------------
80 80
81 81
82 82 _notebook_id_regex = r"(?P<notebook_id>\w+-\w+-\w+-\w+-\w+)"
83 83 _notebook_name_regex = r"(?P<notebook_name>.+\.ipynb)"
84 84
85 85 default_handlers = [
86 86 (r"/new", NewHandler),
87 87 (r"/%s" % _notebook_id_regex, NamedNotebookHandler),
88 88 (r"/%s" % _notebook_name_regex, NotebookRedirectHandler),
89 89 (r"/%s/copy" % _notebook_id_regex, NotebookCopyHandler),
90 90
91 91 ]
@@ -1,742 +1,725 b''
1 1 # coding: utf-8
2 2 """A tornado based IPython notebook server.
3 3
4 4 Authors:
5 5
6 6 * Brian Granger
7 7 """
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2013 The IPython Development Team
10 10 #
11 11 # Distributed under the terms of the BSD License. The full license is in
12 12 # the file COPYING, distributed as part of this software.
13 13 #-----------------------------------------------------------------------------
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18
19 19 # stdlib
20 20 import errno
21 21 import logging
22 22 import os
23 23 import random
24 24 import select
25 25 import signal
26 26 import socket
27 27 import sys
28 28 import threading
29 29 import time
30 30 import webbrowser
31 31
32 32
33 33 # Third party
34 34 # check for pyzmq 2.1.11
35 35 from IPython.utils.zmqrelated import check_for_zmq
36 36 check_for_zmq('2.1.11', 'IPython.html')
37 37
38 38 from jinja2 import Environment, FileSystemLoader
39 39
40 40 # Install the pyzmq ioloop. This has to be done before anything else from
41 41 # tornado is imported.
42 42 from zmq.eventloop import ioloop
43 43 ioloop.install()
44 44
45 45 # check for tornado 2.1.0
46 46 msg = "The IPython Notebook requires tornado >= 2.1.0"
47 47 try:
48 48 import tornado
49 49 except ImportError:
50 50 raise ImportError(msg)
51 51 try:
52 52 version_info = tornado.version_info
53 53 except AttributeError:
54 54 raise ImportError(msg + ", but you have < 1.1.0")
55 55 if version_info < (2,1,0):
56 56 raise ImportError(msg + ", but you have %s" % tornado.version)
57 57
58 58 from tornado import httpserver
59 59 from tornado import web
60 60
61 61 # Our own libraries
62 62 from IPython.html import DEFAULT_STATIC_FILES_PATH
63 63
64 64 from .services.kernels.kernelmanager import MappingKernelManager
65 65 from .services.notebooks.nbmanager import NotebookManager
66 66 from .services.notebooks.filenbmanager import FileNotebookManager
67 67 from .services.clusters.clustermanager import ClusterManager
68 68
69 69 from .base.handlers import AuthenticatedFileHandler, FileFindHandler
70 70
71 71 from IPython.config.application import catch_config_error, boolean_flag
72 72 from IPython.core.application import BaseIPythonApplication
73 73 from IPython.consoleapp import IPythonConsoleApp
74 74 from IPython.kernel import swallow_argv
75 75 from IPython.kernel.zmq.session import default_secure
76 76 from IPython.kernel.zmq.kernelapp import (
77 77 kernel_flags,
78 78 kernel_aliases,
79 79 )
80 80 from IPython.utils.importstring import import_item
81 81 from IPython.utils.localinterfaces import LOCALHOST
82 82 from IPython.utils import submodule
83 83 from IPython.utils.traitlets import (
84 84 Dict, Unicode, Integer, List, Bool, Bytes,
85 85 DottedObjectName
86 86 )
87 87 from IPython.utils import py3compat
88 88 from IPython.utils.path import filefind
89 89
90 90 from .utils import url_path_join
91 91
92 92 #-----------------------------------------------------------------------------
93 93 # Module globals
94 94 #-----------------------------------------------------------------------------
95 95
96 96 _examples = """
97 97 ipython notebook # start the notebook
98 98 ipython notebook --profile=sympy # use the sympy profile
99 99 ipython notebook --pylab=inline # pylab in inline plotting mode
100 100 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
101 101 ipython notebook --port=5555 --ip=* # Listen on port 5555, all interfaces
102 102 """
103 103
104 104 #-----------------------------------------------------------------------------
105 105 # Helper functions
106 106 #-----------------------------------------------------------------------------
107 107
108 108 def random_ports(port, n):
109 109 """Generate a list of n random ports near the given port.
110 110
111 111 The first 5 ports will be sequential, and the remaining n-5 will be
112 112 randomly selected in the range [port-2*n, port+2*n].
113 113 """
114 114 for i in range(min(5, n)):
115 115 yield port + i
116 116 for i in range(n-5):
117 117 yield port + random.randint(-2*n, 2*n)
118 118
119 119 def load_handlers(name):
120 120 """Load the (URL pattern, handler) tuples for each component."""
121 121 name = 'IPython.html.' + name
122 122 mod = __import__(name, fromlist=['default_handlers'])
123 123 return mod.default_handlers
124 124
125 125 #-----------------------------------------------------------------------------
126 126 # The Tornado web application
127 127 #-----------------------------------------------------------------------------
128 128
129 129 class NotebookWebApplication(web.Application):
130 130
131 131 def __init__(self, ipython_app, kernel_manager, notebook_manager,
132 132 cluster_manager, log,
133 133 base_project_url, settings_overrides):
134 134
135 135 settings = self.init_settings(
136 136 ipython_app, kernel_manager, notebook_manager, cluster_manager,
137 137 log, base_project_url, settings_overrides)
138 138 handlers = self.init_handlers(settings)
139 139
140 140 super(NotebookWebApplication, self).__init__(handlers, **settings)
141 141
142 142 def init_settings(self, ipython_app, kernel_manager, notebook_manager,
143 143 cluster_manager, log,
144 144 base_project_url, settings_overrides):
145 145 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
146 146 # base_project_url will always be unicode, which will in turn
147 147 # make the patterns unicode, and ultimately result in unicode
148 148 # keys in kwargs to handler._execute(**kwargs) in tornado.
149 149 # This enforces that base_project_url be ascii in that situation.
150 150 #
151 151 # Note that the URLs these patterns check against are escaped,
152 152 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
153 153 base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii')
154 154 template_path = os.path.join(os.path.dirname(__file__), "templates")
155 155 settings = dict(
156 156 # basics
157 157 base_project_url=base_project_url,
158 158 base_kernel_url=ipython_app.base_kernel_url,
159 159 template_path=template_path,
160 160 static_path=ipython_app.static_file_path,
161 161 static_handler_class = FileFindHandler,
162 162 static_url_prefix = url_path_join(base_project_url,'/static/'),
163 163
164 164 # authentication
165 165 cookie_secret=ipython_app.cookie_secret,
166 166 login_url=url_path_join(base_project_url,'/login'),
167 read_only=ipython_app.read_only,
168 167 password=ipython_app.password,
169 168
170 169 # managers
171 170 kernel_manager=kernel_manager,
172 171 notebook_manager=notebook_manager,
173 172 cluster_manager=cluster_manager,
174 173
175 174 # IPython stuff
176 175 mathjax_url=ipython_app.mathjax_url,
177 176 config=ipython_app.config,
178 177 use_less=ipython_app.use_less,
179 178 jinja2_env=Environment(loader=FileSystemLoader(template_path)),
180 179 )
181 180
182 181 # allow custom overrides for the tornado web app.
183 182 settings.update(settings_overrides)
184 183 return settings
185 184
186 185 def init_handlers(self, settings):
187 186 # Load the (URL pattern, handler) tuples for each component.
188 187 handlers = []
189 188 handlers.extend(load_handlers('base.handlers'))
190 189 handlers.extend(load_handlers('tree.handlers'))
191 190 handlers.extend(load_handlers('auth.login'))
192 191 handlers.extend(load_handlers('auth.logout'))
193 192 handlers.extend(load_handlers('notebook.handlers'))
194 193 handlers.extend(load_handlers('services.kernels.handlers'))
195 194 handlers.extend(load_handlers('services.notebooks.handlers'))
196 195 handlers.extend(load_handlers('services.clusters.handlers'))
197 196 handlers.extend([
198 197 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : settings['notebook_manager'].notebook_dir}),
199 198 ])
200 199 # prepend base_project_url onto the patterns that we match
201 200 new_handlers = []
202 201 for handler in handlers:
203 202 pattern = url_path_join(settings['base_project_url'], handler[0])
204 203 new_handler = tuple([pattern] + list(handler[1:]))
205 204 new_handlers.append(new_handler)
206 205 return new_handlers
207 206
208 207
209 208
210 209 #-----------------------------------------------------------------------------
211 210 # Aliases and Flags
212 211 #-----------------------------------------------------------------------------
213 212
214 213 flags = dict(kernel_flags)
215 214 flags['no-browser']=(
216 215 {'NotebookApp' : {'open_browser' : False}},
217 216 "Don't open the notebook in a browser after startup."
218 217 )
219 218 flags['no-mathjax']=(
220 219 {'NotebookApp' : {'enable_mathjax' : False}},
221 220 """Disable MathJax
222 221
223 222 MathJax is the javascript library IPython uses to render math/LaTeX. It is
224 223 very large, so you may want to disable it if you have a slow internet
225 224 connection, or for offline use of the notebook.
226 225
227 226 When disabled, equations etc. will appear as their untransformed TeX source.
228 227 """
229 228 )
230 flags['read-only'] = (
231 {'NotebookApp' : {'read_only' : True}},
232 """Allow read-only access to notebooks.
233
234 When using a password to protect the notebook server, this flag
235 allows unauthenticated clients to view the notebook list, and
236 individual notebooks, but not edit them, start kernels, or run
237 code.
238
239 If no password is set, the server will be entirely read-only.
240 """
241 )
242 229
243 230 # Add notebook manager flags
244 231 flags.update(boolean_flag('script', 'FileNotebookManager.save_script',
245 232 'Auto-save a .py script everytime the .ipynb notebook is saved',
246 233 'Do not auto-save .py scripts for every notebook'))
247 234
248 235 # the flags that are specific to the frontend
249 236 # these must be scrubbed before being passed to the kernel,
250 237 # or it will raise an error on unrecognized flags
251 notebook_flags = ['no-browser', 'no-mathjax', 'read-only', 'script', 'no-script']
238 notebook_flags = ['no-browser', 'no-mathjax', 'script', 'no-script']
252 239
253 240 aliases = dict(kernel_aliases)
254 241
255 242 aliases.update({
256 243 'ip': 'NotebookApp.ip',
257 244 'port': 'NotebookApp.port',
258 245 'port-retries': 'NotebookApp.port_retries',
259 246 'transport': 'KernelManager.transport',
260 247 'keyfile': 'NotebookApp.keyfile',
261 248 'certfile': 'NotebookApp.certfile',
262 249 'notebook-dir': 'NotebookManager.notebook_dir',
263 250 'browser': 'NotebookApp.browser',
264 251 })
265 252
266 253 # remove ipkernel flags that are singletons, and don't make sense in
267 254 # multi-kernel evironment:
268 255 aliases.pop('f', None)
269 256
270 257 notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
271 258 u'notebook-dir']
272 259
273 260 #-----------------------------------------------------------------------------
274 261 # NotebookApp
275 262 #-----------------------------------------------------------------------------
276 263
277 264 class NotebookApp(BaseIPythonApplication):
278 265
279 266 name = 'ipython-notebook'
280 267
281 268 description = """
282 269 The IPython HTML Notebook.
283 270
284 271 This launches a Tornado based HTML Notebook Server that serves up an
285 272 HTML5/Javascript Notebook client.
286 273 """
287 274 examples = _examples
288 275
289 276 classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager,
290 277 FileNotebookManager]
291 278 flags = Dict(flags)
292 279 aliases = Dict(aliases)
293 280
294 281 kernel_argv = List(Unicode)
295 282
296 283 def _log_level_default(self):
297 284 return logging.INFO
298 285
299 286 def _log_format_default(self):
300 287 """override default log format to include time"""
301 288 return u"%(asctime)s.%(msecs).03d [%(name)s]%(highlevel)s %(message)s"
302 289
303 290 # create requested profiles by default, if they don't exist:
304 291 auto_create = Bool(True)
305 292
306 293 # file to be opened in the notebook server
307 294 file_to_run = Unicode('')
308 295
309 296 # Network related information.
310 297
311 298 ip = Unicode(LOCALHOST, config=True,
312 299 help="The IP address the notebook server will listen on."
313 300 )
314 301
315 302 def _ip_changed(self, name, old, new):
316 303 if new == u'*': self.ip = u''
317 304
318 305 port = Integer(8888, config=True,
319 306 help="The port the notebook server will listen on."
320 307 )
321 308 port_retries = Integer(50, config=True,
322 309 help="The number of additional ports to try if the specified port is not available."
323 310 )
324 311
325 312 certfile = Unicode(u'', config=True,
326 313 help="""The full path to an SSL/TLS certificate file."""
327 314 )
328 315
329 316 keyfile = Unicode(u'', config=True,
330 317 help="""The full path to a private key file for usage with SSL/TLS."""
331 318 )
332 319
333 320 cookie_secret = Bytes(b'', config=True,
334 321 help="""The random bytes used to secure cookies.
335 322 By default this is a new random number every time you start the Notebook.
336 323 Set it to a value in a config file to enable logins to persist across server sessions.
337 324
338 325 Note: Cookie secrets should be kept private, do not share config files with
339 326 cookie_secret stored in plaintext (you can read the value from a file).
340 327 """
341 328 )
342 329 def _cookie_secret_default(self):
343 330 return os.urandom(1024)
344 331
345 332 password = Unicode(u'', config=True,
346 333 help="""Hashed password to use for web authentication.
347 334
348 335 To generate, type in a python/IPython shell:
349 336
350 337 from IPython.lib import passwd; passwd()
351 338
352 339 The string should be of the form type:salt:hashed-password.
353 340 """
354 341 )
355 342
356 343 open_browser = Bool(True, config=True,
357 344 help="""Whether to open in a browser after starting.
358 345 The specific browser used is platform dependent and
359 346 determined by the python standard library `webbrowser`
360 347 module, unless it is overridden using the --browser
361 348 (NotebookApp.browser) configuration option.
362 349 """)
363 350
364 351 browser = Unicode(u'', config=True,
365 352 help="""Specify what command to use to invoke a web
366 353 browser when opening the notebook. If not specified, the
367 354 default browser will be determined by the `webbrowser`
368 355 standard library module, which allows setting of the
369 356 BROWSER environment variable to override it.
370 357 """)
371 358
372 read_only = Bool(False, config=True,
373 help="Whether to prevent editing/execution of notebooks."
374 )
375
376 359 use_less = Bool(False, config=True,
377 360 help="""Wether to use Browser Side less-css parsing
378 361 instead of compiled css version in templates that allows
379 362 it. This is mainly convenient when working on the less
380 363 file to avoid a build step, or if user want to overwrite
381 364 some of the less variables without having to recompile
382 365 everything.
383 366
384 367 You will need to install the less.js component in the static directory
385 368 either in the source tree or in your profile folder.
386 369 """)
387 370
388 371 webapp_settings = Dict(config=True,
389 372 help="Supply overrides for the tornado.web.Application that the "
390 373 "IPython notebook uses.")
391 374
392 375 enable_mathjax = Bool(True, config=True,
393 376 help="""Whether to enable MathJax for typesetting math/TeX
394 377
395 378 MathJax is the javascript library IPython uses to render math/LaTeX. It is
396 379 very large, so you may want to disable it if you have a slow internet
397 380 connection, or for offline use of the notebook.
398 381
399 382 When disabled, equations etc. will appear as their untransformed TeX source.
400 383 """
401 384 )
402 385 def _enable_mathjax_changed(self, name, old, new):
403 386 """set mathjax url to empty if mathjax is disabled"""
404 387 if not new:
405 388 self.mathjax_url = u''
406 389
407 390 base_project_url = Unicode('/', config=True,
408 391 help='''The base URL for the notebook server.
409 392
410 393 Leading and trailing slashes can be omitted,
411 394 and will automatically be added.
412 395 ''')
413 396 def _base_project_url_changed(self, name, old, new):
414 397 if not new.startswith('/'):
415 398 self.base_project_url = '/'+new
416 399 elif not new.endswith('/'):
417 400 self.base_project_url = new+'/'
418 401
419 402 base_kernel_url = Unicode('/', config=True,
420 403 help='''The base URL for the kernel server
421 404
422 405 Leading and trailing slashes can be omitted,
423 406 and will automatically be added.
424 407 ''')
425 408 def _base_kernel_url_changed(self, name, old, new):
426 409 if not new.startswith('/'):
427 410 self.base_kernel_url = '/'+new
428 411 elif not new.endswith('/'):
429 412 self.base_kernel_url = new+'/'
430 413
431 414 websocket_url = Unicode("", config=True,
432 415 help="""The base URL for the websocket server,
433 416 if it differs from the HTTP server (hint: it almost certainly doesn't).
434 417
435 418 Should be in the form of an HTTP origin: ws[s]://hostname[:port]
436 419 """
437 420 )
438 421
439 422 extra_static_paths = List(Unicode, config=True,
440 423 help="""Extra paths to search for serving static files.
441 424
442 425 This allows adding javascript/css to be available from the notebook server machine,
443 426 or overriding individual files in the IPython"""
444 427 )
445 428 def _extra_static_paths_default(self):
446 429 return [os.path.join(self.profile_dir.location, 'static')]
447 430
448 431 @property
449 432 def static_file_path(self):
450 433 """return extra paths + the default location"""
451 434 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
452 435
453 436 mathjax_url = Unicode("", config=True,
454 437 help="""The url for MathJax.js."""
455 438 )
456 439 def _mathjax_url_default(self):
457 440 if not self.enable_mathjax:
458 441 return u''
459 442 static_url_prefix = self.webapp_settings.get("static_url_prefix",
460 443 url_path_join(self.base_project_url, "static")
461 444 )
462 445 try:
463 446 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), self.static_file_path)
464 447 except IOError:
465 448 if self.certfile:
466 449 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
467 450 base = u"https://c328740.ssl.cf1.rackcdn.com"
468 451 else:
469 452 base = u"http://cdn.mathjax.org"
470 453
471 454 url = base + u"/mathjax/latest/MathJax.js"
472 455 self.log.info("Using MathJax from CDN: %s", url)
473 456 return url
474 457 else:
475 458 self.log.info("Using local MathJax from %s" % mathjax)
476 459 return url_path_join(static_url_prefix, u"mathjax/MathJax.js")
477 460
478 461 def _mathjax_url_changed(self, name, old, new):
479 462 if new and not self.enable_mathjax:
480 463 # enable_mathjax=False overrides mathjax_url
481 464 self.mathjax_url = u''
482 465 else:
483 466 self.log.info("Using MathJax: %s", new)
484 467
485 468 notebook_manager_class = DottedObjectName('IPython.html.services.notebooks.filenbmanager.FileNotebookManager',
486 469 config=True,
487 470 help='The notebook manager class to use.')
488 471
489 472 trust_xheaders = Bool(False, config=True,
490 473 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
491 474 "sent by the upstream reverse proxy. Neccesary if the proxy handles SSL")
492 475 )
493 476
494 477 def parse_command_line(self, argv=None):
495 478 super(NotebookApp, self).parse_command_line(argv)
496 479 if argv is None:
497 480 argv = sys.argv[1:]
498 481
499 482 # Scrub frontend-specific flags
500 483 self.kernel_argv = swallow_argv(argv, notebook_aliases, notebook_flags)
501 484 # Kernel should inherit default config file from frontend
502 485 self.kernel_argv.append("--IPKernelApp.parent_appname='%s'" % self.name)
503 486
504 487 if self.extra_args:
505 488 f = os.path.abspath(self.extra_args[0])
506 489 if os.path.isdir(f):
507 490 nbdir = f
508 491 else:
509 492 self.file_to_run = f
510 493 nbdir = os.path.dirname(f)
511 494 self.config.NotebookManager.notebook_dir = nbdir
512 495
513 496 def init_configurables(self):
514 497 # force Session default to be secure
515 498 default_secure(self.config)
516 499 self.kernel_manager = MappingKernelManager(
517 500 parent=self, log=self.log, kernel_argv=self.kernel_argv,
518 501 connection_dir = self.profile_dir.security_dir,
519 502 )
520 503 kls = import_item(self.notebook_manager_class)
521 504 self.notebook_manager = kls(parent=self, log=self.log)
522 505 self.notebook_manager.load_notebook_names()
523 506 self.cluster_manager = ClusterManager(parent=self, log=self.log)
524 507 self.cluster_manager.update_profiles()
525 508
526 509 def init_logging(self):
527 510 # This prevents double log messages because tornado use a root logger that
528 511 # self.log is a child of. The logging module dipatches log messages to a log
529 512 # and all of its ancenstors until propagate is set to False.
530 513 self.log.propagate = False
531 514
532 515 # hook up tornado 3's loggers to our app handlers
533 516 for name in ('access', 'application', 'general'):
534 517 logging.getLogger('tornado.%s' % name).handlers = self.log.handlers
535 518
536 519 def init_webapp(self):
537 520 """initialize tornado webapp and httpserver"""
538 521 self.web_app = NotebookWebApplication(
539 522 self, self.kernel_manager, self.notebook_manager,
540 523 self.cluster_manager, self.log,
541 524 self.base_project_url, self.webapp_settings
542 525 )
543 526 if self.certfile:
544 527 ssl_options = dict(certfile=self.certfile)
545 528 if self.keyfile:
546 529 ssl_options['keyfile'] = self.keyfile
547 530 else:
548 531 ssl_options = None
549 532 self.web_app.password = self.password
550 533 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
551 534 xheaders=self.trust_xheaders)
552 535 if not self.ip:
553 536 warning = "WARNING: The notebook server is listening on all IP addresses"
554 537 if ssl_options is None:
555 538 self.log.critical(warning + " and not using encryption. This "
556 539 "is not recommended.")
557 if not self.password and not self.read_only:
540 if not self.password:
558 541 self.log.critical(warning + " and not using authentication. "
559 542 "This is highly insecure and not recommended.")
560 543 success = None
561 544 for port in random_ports(self.port, self.port_retries+1):
562 545 try:
563 546 self.http_server.listen(port, self.ip)
564 547 except socket.error as e:
565 548 # XXX: remove the e.errno == -9 block when we require
566 549 # tornado >= 3.0
567 550 if e.errno == -9 and tornado.version_info[0] < 3:
568 551 # The flags passed to socket.getaddrinfo from
569 552 # tornado.netutils.bind_sockets can cause "gaierror:
570 553 # [Errno -9] Address family for hostname not supported"
571 554 # when the interface is not associated, for example.
572 555 # Changing the flags to exclude socket.AI_ADDRCONFIG does
573 556 # not cause this error, but the only way to do this is to
574 557 # monkeypatch socket to remove the AI_ADDRCONFIG attribute
575 558 saved_AI_ADDRCONFIG = socket.AI_ADDRCONFIG
576 559 self.log.warn('Monkeypatching socket to fix tornado bug')
577 560 del(socket.AI_ADDRCONFIG)
578 561 try:
579 562 # retry the tornado call without AI_ADDRCONFIG flags
580 563 self.http_server.listen(port, self.ip)
581 564 except socket.error as e2:
582 565 e = e2
583 566 else:
584 567 self.port = port
585 568 success = True
586 569 break
587 570 # restore the monekypatch
588 571 socket.AI_ADDRCONFIG = saved_AI_ADDRCONFIG
589 572 if e.errno != errno.EADDRINUSE:
590 573 raise
591 574 self.log.info('The port %i is already in use, trying another random port.' % port)
592 575 else:
593 576 self.port = port
594 577 success = True
595 578 break
596 579 if not success:
597 580 self.log.critical('ERROR: the notebook server could not be started because '
598 581 'no available port could be found.')
599 582 self.exit(1)
600 583
601 584 def init_signal(self):
602 585 if not sys.platform.startswith('win'):
603 586 signal.signal(signal.SIGINT, self._handle_sigint)
604 587 signal.signal(signal.SIGTERM, self._signal_stop)
605 588 if hasattr(signal, 'SIGUSR1'):
606 589 # Windows doesn't support SIGUSR1
607 590 signal.signal(signal.SIGUSR1, self._signal_info)
608 591 if hasattr(signal, 'SIGINFO'):
609 592 # only on BSD-based systems
610 593 signal.signal(signal.SIGINFO, self._signal_info)
611 594
612 595 def _handle_sigint(self, sig, frame):
613 596 """SIGINT handler spawns confirmation dialog"""
614 597 # register more forceful signal handler for ^C^C case
615 598 signal.signal(signal.SIGINT, self._signal_stop)
616 599 # request confirmation dialog in bg thread, to avoid
617 600 # blocking the App
618 601 thread = threading.Thread(target=self._confirm_exit)
619 602 thread.daemon = True
620 603 thread.start()
621 604
622 605 def _restore_sigint_handler(self):
623 606 """callback for restoring original SIGINT handler"""
624 607 signal.signal(signal.SIGINT, self._handle_sigint)
625 608
626 609 def _confirm_exit(self):
627 610 """confirm shutdown on ^C
628 611
629 612 A second ^C, or answering 'y' within 5s will cause shutdown,
630 613 otherwise original SIGINT handler will be restored.
631 614
632 615 This doesn't work on Windows.
633 616 """
634 617 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
635 618 time.sleep(0.1)
636 619 info = self.log.info
637 620 info('interrupted')
638 621 print self.notebook_info()
639 622 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
640 623 sys.stdout.flush()
641 624 r,w,x = select.select([sys.stdin], [], [], 5)
642 625 if r:
643 626 line = sys.stdin.readline()
644 627 if line.lower().startswith('y'):
645 628 self.log.critical("Shutdown confirmed")
646 629 ioloop.IOLoop.instance().stop()
647 630 return
648 631 else:
649 632 print "No answer for 5s:",
650 633 print "resuming operation..."
651 634 # no answer, or answer is no:
652 635 # set it back to original SIGINT handler
653 636 # use IOLoop.add_callback because signal.signal must be called
654 637 # from main thread
655 638 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
656 639
657 640 def _signal_stop(self, sig, frame):
658 641 self.log.critical("received signal %s, stopping", sig)
659 642 ioloop.IOLoop.instance().stop()
660 643
661 644 def _signal_info(self, sig, frame):
662 645 print self.notebook_info()
663 646
664 647 def init_components(self):
665 648 """Check the components submodule, and warn if it's unclean"""
666 649 status = submodule.check_submodule_status()
667 650 if status == 'missing':
668 651 self.log.warn("components submodule missing, running `git submodule update`")
669 652 submodule.update_submodules(submodule.ipython_parent())
670 653 elif status == 'unclean':
671 654 self.log.warn("components submodule unclean, you may see 404s on static/components")
672 655 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
673 656
674 657
675 658 @catch_config_error
676 659 def initialize(self, argv=None):
677 660 self.init_logging()
678 661 super(NotebookApp, self).initialize(argv)
679 662 self.init_configurables()
680 663 self.init_components()
681 664 self.init_webapp()
682 665 self.init_signal()
683 666
684 667 def cleanup_kernels(self):
685 668 """Shutdown all kernels.
686 669
687 670 The kernels will shutdown themselves when this process no longer exists,
688 671 but explicit shutdown allows the KernelManagers to cleanup the connection files.
689 672 """
690 673 self.log.info('Shutting down kernels')
691 674 self.kernel_manager.shutdown_all()
692 675
693 676 def notebook_info(self):
694 677 "Return the current working directory and the server url information"
695 678 mgr_info = self.notebook_manager.info_string() + "\n"
696 679 return mgr_info +"The IPython Notebook is running at: %s" % self._url
697 680
698 681 def start(self):
699 682 """ Start the IPython Notebook server app, after initialization
700 683
701 684 This method takes no arguments so all configuration and initialization
702 685 must be done prior to calling this method."""
703 686 ip = self.ip if self.ip else '[all ip addresses on your system]'
704 687 proto = 'https' if self.certfile else 'http'
705 688 info = self.log.info
706 689 self._url = "%s://%s:%i%s" % (proto, ip, self.port,
707 690 self.base_project_url)
708 691 for line in self.notebook_info().split("\n"):
709 692 info(line)
710 693 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
711 694
712 695 if self.open_browser or self.file_to_run:
713 696 ip = self.ip or LOCALHOST
714 697 try:
715 698 browser = webbrowser.get(self.browser or None)
716 699 except webbrowser.Error as e:
717 700 self.log.warn('No web browser found: %s.' % e)
718 701 browser = None
719 702
720 703 if self.file_to_run:
721 704 name, _ = os.path.splitext(os.path.basename(self.file_to_run))
722 705 url = self.notebook_manager.rev_mapping.get(name, '')
723 706 else:
724 707 url = ''
725 708 if browser:
726 709 b = lambda : browser.open("%s://%s:%i%s%s" % (proto, ip,
727 710 self.port, self.base_project_url, url), new=2)
728 711 threading.Thread(target=b).start()
729 712 try:
730 713 ioloop.IOLoop.instance().start()
731 714 except KeyboardInterrupt:
732 715 info("Interrupted...")
733 716 finally:
734 717 self.cleanup_kernels()
735 718
736 719
737 720 #-----------------------------------------------------------------------------
738 721 # Main entry point
739 722 #-----------------------------------------------------------------------------
740 723
741 724 launch_new_instance = NotebookApp.launch_instance
742 725
@@ -1,156 +1,156 b''
1 1 """Tornado handlers for the notebooks web service.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2008-2011 The IPython Development Team
10 10 #
11 11 # Distributed under the terms of the BSD License. The full license is in
12 12 # the file COPYING, distributed as part of this software.
13 13 #-----------------------------------------------------------------------------
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18
19 19 from tornado import web
20 20
21 21 from zmq.utils import jsonapi
22 22
23 23 from IPython.utils.jsonutil import date_default
24 24
25 from ...base.handlers import IPythonHandler, authenticate_unless_readonly
25 from ...base.handlers import IPythonHandler
26 26
27 27 #-----------------------------------------------------------------------------
28 28 # Notebook web service handlers
29 29 #-----------------------------------------------------------------------------
30 30
31 31 class NotebookRootHandler(IPythonHandler):
32 32
33 @authenticate_unless_readonly
33 @web.authenticated
34 34 def get(self):
35 35 nbm = self.notebook_manager
36 36 km = self.kernel_manager
37 37 files = nbm.list_notebooks()
38 38 for f in files :
39 39 f['kernel_id'] = km.kernel_for_notebook(f['notebook_id'])
40 40 self.finish(jsonapi.dumps(files))
41 41
42 42 @web.authenticated
43 43 def post(self):
44 44 nbm = self.notebook_manager
45 45 body = self.request.body.strip()
46 46 format = self.get_argument('format', default='json')
47 47 name = self.get_argument('name', default=None)
48 48 if body:
49 49 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
50 50 else:
51 51 notebook_id = nbm.new_notebook()
52 52 self.set_header('Location', '{0}notebooks/{1}'.format(self.base_project_url, notebook_id))
53 53 self.finish(jsonapi.dumps(notebook_id))
54 54
55 55
56 56 class NotebookHandler(IPythonHandler):
57 57
58 58 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
59 59
60 @authenticate_unless_readonly
60 @web.authenticated
61 61 def get(self, notebook_id):
62 62 nbm = self.notebook_manager
63 63 format = self.get_argument('format', default='json')
64 64 last_mod, name, data = nbm.get_notebook(notebook_id, format)
65 65
66 66 if format == u'json':
67 67 self.set_header('Content-Type', 'application/json')
68 68 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
69 69 elif format == u'py':
70 70 self.set_header('Content-Type', 'application/x-python')
71 71 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
72 72 self.set_header('Last-Modified', last_mod)
73 73 self.finish(data)
74 74
75 75 @web.authenticated
76 76 def put(self, notebook_id):
77 77 nbm = self.notebook_manager
78 78 format = self.get_argument('format', default='json')
79 79 name = self.get_argument('name', default=None)
80 80 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
81 81 self.set_status(204)
82 82 self.finish()
83 83
84 84 @web.authenticated
85 85 def delete(self, notebook_id):
86 86 self.notebook_manager.delete_notebook(notebook_id)
87 87 self.set_status(204)
88 88 self.finish()
89 89
90 90
91 91 class NotebookCheckpointsHandler(IPythonHandler):
92 92
93 93 SUPPORTED_METHODS = ('GET', 'POST')
94 94
95 95 @web.authenticated
96 96 def get(self, notebook_id):
97 97 """get lists checkpoints for a notebook"""
98 98 nbm = self.notebook_manager
99 99 checkpoints = nbm.list_checkpoints(notebook_id)
100 100 data = jsonapi.dumps(checkpoints, default=date_default)
101 101 self.finish(data)
102 102
103 103 @web.authenticated
104 104 def post(self, notebook_id):
105 105 """post creates a new checkpoint"""
106 106 nbm = self.notebook_manager
107 107 checkpoint = nbm.create_checkpoint(notebook_id)
108 108 data = jsonapi.dumps(checkpoint, default=date_default)
109 109 self.set_header('Location', '{0}notebooks/{1}/checkpoints/{2}'.format(
110 110 self.base_project_url, notebook_id, checkpoint['checkpoint_id']
111 111 ))
112 112
113 113 self.finish(data)
114 114
115 115
116 116 class ModifyNotebookCheckpointsHandler(IPythonHandler):
117 117
118 118 SUPPORTED_METHODS = ('POST', 'DELETE')
119 119
120 120 @web.authenticated
121 121 def post(self, notebook_id, checkpoint_id):
122 122 """post restores a notebook from a checkpoint"""
123 123 nbm = self.notebook_manager
124 124 nbm.restore_checkpoint(notebook_id, checkpoint_id)
125 125 self.set_status(204)
126 126 self.finish()
127 127
128 128 @web.authenticated
129 129 def delete(self, notebook_id, checkpoint_id):
130 130 """delete clears a checkpoint for a given notebook"""
131 131 nbm = self.notebook_manager
132 132 nbm.delte_checkpoint(notebook_id, checkpoint_id)
133 133 self.set_status(204)
134 134 self.finish()
135 135
136 136
137 137 #-----------------------------------------------------------------------------
138 138 # URL to handler mappings
139 139 #-----------------------------------------------------------------------------
140 140
141 141
142 142 _notebook_id_regex = r"(?P<notebook_id>\w+-\w+-\w+-\w+-\w+)"
143 143 _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
144 144
145 145 default_handlers = [
146 146 (r"/notebooks", NotebookRootHandler),
147 147 (r"/notebooks/%s" % _notebook_id_regex, NotebookHandler),
148 148 (r"/notebooks/%s/checkpoints" % _notebook_id_regex, NotebookCheckpointsHandler),
149 149 (r"/notebooks/%s/checkpoints/%s" % (_notebook_id_regex, _checkpoint_id_regex),
150 150 ModifyNotebookCheckpointsHandler
151 151 ),
152 152 ]
153 153
154 154
155 155
156 156
@@ -1,445 +1,441 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2008-2011 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // CodeCell
10 10 //============================================================================
11 11 /**
12 12 * An extendable module that provide base functionnality to create cell for notebook.
13 13 * @module IPython
14 14 * @namespace IPython
15 15 * @submodule CodeCell
16 16 */
17 17
18 18
19 19 /* local util for codemirror */
20 20 var posEq = function(a, b) {return a.line == b.line && a.ch == b.ch;}
21 21
22 22 /**
23 23 *
24 24 * function to delete until previous non blanking space character
25 25 * or first multiple of 4 tabstop.
26 26 * @private
27 27 */
28 28 CodeMirror.commands.delSpaceToPrevTabStop = function(cm){
29 29 var from = cm.getCursor(true), to = cm.getCursor(false), sel = !posEq(from, to);
30 30 if (!posEq(from, to)) {cm.replaceRange("", from, to); return}
31 31 var cur = cm.getCursor(), line = cm.getLine(cur.line);
32 32 var tabsize = cm.getOption('tabSize');
33 33 var chToPrevTabStop = cur.ch-(Math.ceil(cur.ch/tabsize)-1)*tabsize;
34 34 var from = {ch:cur.ch-chToPrevTabStop,line:cur.line}
35 35 var select = cm.getRange(from,cur)
36 36 if( select.match(/^\ +$/) != null){
37 37 cm.replaceRange("",from,cur)
38 38 } else {
39 39 cm.deleteH(-1,"char")
40 40 }
41 41 };
42 42
43 43
44 44 var IPython = (function (IPython) {
45 45 "use strict";
46 46
47 47 var utils = IPython.utils;
48 48 var key = IPython.utils.keycodes;
49 49
50 50 /**
51 51 * A Cell conceived to write code.
52 52 *
53 53 * The kernel doesn't have to be set at creation time, in that case
54 54 * it will be null and set_kernel has to be called later.
55 55 * @class CodeCell
56 56 * @extends IPython.Cell
57 57 *
58 58 * @constructor
59 59 * @param {Object|null} kernel
60 60 * @param {object|undefined} [options]
61 61 * @param [options.cm_config] {object} config to pass to CodeMirror
62 62 */
63 63 var CodeCell = function (kernel, options) {
64 64 this.kernel = kernel || null;
65 65 this.code_mirror = null;
66 66 this.input_prompt_number = null;
67 67 this.collapsed = false;
68 68 this.default_mode = 'ipython';
69 69 this.cell_type = "code";
70 70
71 71
72 72 var cm_overwrite_options = {
73 73 onKeyEvent: $.proxy(this.handle_codemirror_keyevent,this)
74 74 };
75 75
76 76 options = this.mergeopt(CodeCell, options, {cm_config:cm_overwrite_options});
77 77
78 78 IPython.Cell.apply(this,[options]);
79 79
80 80 var that = this;
81 81 this.element.focusout(
82 82 function() { that.auto_highlight(); }
83 83 );
84 84 };
85 85
86 86 CodeCell.options_default = {
87 87 cm_config : {
88 88 extraKeys: {
89 89 "Tab" : "indentMore",
90 90 "Shift-Tab" : "indentLess",
91 91 "Backspace" : "delSpaceToPrevTabStop",
92 92 "Cmd-/" : "toggleComment",
93 93 "Ctrl-/" : "toggleComment"
94 94 },
95 95 mode: 'ipython',
96 96 theme: 'ipython',
97 97 matchBrackets: true
98 98 }
99 99 };
100 100
101 101
102 102 CodeCell.prototype = new IPython.Cell();
103 103
104 104 /**
105 105 * @method auto_highlight
106 106 */
107 107 CodeCell.prototype.auto_highlight = function () {
108 108 this._auto_highlight(IPython.config.cell_magic_highlight)
109 109 };
110 110
111 111 /** @method create_element */
112 112 CodeCell.prototype.create_element = function () {
113 113 IPython.Cell.prototype.create_element.apply(this, arguments);
114 114
115 115 var cell = $('<div></div>').addClass('cell border-box-sizing code_cell');
116 116 cell.attr('tabindex','2');
117 117
118 118 this.celltoolbar = new IPython.CellToolbar(this);
119 119
120 120 var input = $('<div></div>').addClass('input');
121 121 var vbox = $('<div/>').addClass('vbox box-flex1')
122 122 input.append($('<div/>').addClass('prompt input_prompt'));
123 123 vbox.append(this.celltoolbar.element);
124 124 var input_area = $('<div/>').addClass('input_area');
125 125 this.code_mirror = CodeMirror(input_area.get(0), this.cm_config);
126 126 $(this.code_mirror.getInputField()).attr("spellcheck", "false");
127 127 vbox.append(input_area);
128 128 input.append(vbox);
129 129 var output = $('<div></div>');
130 130 cell.append(input).append(output);
131 131 this.element = cell;
132 132 this.output_area = new IPython.OutputArea(output, true);
133 133
134 134 // construct a completer only if class exist
135 135 // otherwise no print view
136 136 if (IPython.Completer !== undefined)
137 137 {
138 138 this.completer = new IPython.Completer(this);
139 139 }
140 140 };
141 141
142 142 /**
143 143 * This method gets called in CodeMirror's onKeyDown/onKeyPress
144 144 * handlers and is used to provide custom key handling. Its return
145 145 * value is used to determine if CodeMirror should ignore the event:
146 146 * true = ignore, false = don't ignore.
147 147 * @method handle_codemirror_keyevent
148 148 */
149 149 CodeCell.prototype.handle_codemirror_keyevent = function (editor, event) {
150 150
151 if (this.read_only){
152 return false;
153 }
154
155 151 var that = this;
156 152 // whatever key is pressed, first, cancel the tooltip request before
157 153 // they are sent, and remove tooltip if any, except for tab again
158 154 if (event.type === 'keydown' && event.which != key.TAB ) {
159 155 IPython.tooltip.remove_and_cancel_tooltip();
160 156 };
161 157
162 158 var cur = editor.getCursor();
163 159 if (event.keyCode === key.ENTER){
164 160 this.auto_highlight();
165 161 }
166 162
167 163 if (event.keyCode === key.ENTER && (event.shiftKey || event.ctrlKey)) {
168 164 // Always ignore shift-enter in CodeMirror as we handle it.
169 165 return true;
170 166 } else if (event.which === 40 && event.type === 'keypress' && IPython.tooltip.time_before_tooltip >= 0) {
171 167 // triger on keypress (!) otherwise inconsistent event.which depending on plateform
172 168 // browser and keyboard layout !
173 169 // Pressing '(' , request tooltip, don't forget to reappend it
174 170 IPython.tooltip.pending(that);
175 171 } else if (event.which === key.UPARROW && event.type === 'keydown') {
176 172 // If we are not at the top, let CM handle the up arrow and
177 173 // prevent the global keydown handler from handling it.
178 174 if (!that.at_top()) {
179 175 event.stop();
180 176 return false;
181 177 } else {
182 178 return true;
183 179 };
184 180 } else if (event.which === key.ESC) {
185 181 IPython.tooltip.remove_and_cancel_tooltip(true);
186 182 return true;
187 183 } else if (event.which === key.DOWNARROW && event.type === 'keydown') {
188 184 // If we are not at the bottom, let CM handle the down arrow and
189 185 // prevent the global keydown handler from handling it.
190 186 if (!that.at_bottom()) {
191 187 event.stop();
192 188 return false;
193 189 } else {
194 190 return true;
195 191 };
196 192 } else if (event.keyCode === key.TAB && event.type == 'keydown' && event.shiftKey) {
197 193 if (editor.somethingSelected()){
198 194 var anchor = editor.getCursor("anchor");
199 195 var head = editor.getCursor("head");
200 196 if( anchor.line != head.line){
201 197 return false;
202 198 }
203 199 }
204 200 IPython.tooltip.request(that);
205 201 event.stop();
206 202 return true;
207 203 } else if (event.keyCode === key.TAB && event.type == 'keydown') {
208 204 // Tab completion.
209 205 //Do not trim here because of tooltip
210 206 if (editor.somethingSelected()){return false}
211 207 var pre_cursor = editor.getRange({line:cur.line,ch:0},cur);
212 208 if (pre_cursor.trim() === "") {
213 209 // Don't autocomplete if the part of the line before the cursor
214 210 // is empty. In this case, let CodeMirror handle indentation.
215 211 return false;
216 212 } else if ((pre_cursor.substr(-1) === "("|| pre_cursor.substr(-1) === " ") && IPython.config.tooltip_on_tab ) {
217 213 IPython.tooltip.request(that);
218 214 // Prevent the event from bubbling up.
219 215 event.stop();
220 216 // Prevent CodeMirror from handling the tab.
221 217 return true;
222 218 } else {
223 219 event.stop();
224 220 this.completer.startCompletion();
225 221 return true;
226 222 };
227 223 } else {
228 224 // keypress/keyup also trigger on TAB press, and we don't want to
229 225 // use those to disable tab completion.
230 226 return false;
231 227 };
232 228 return false;
233 229 };
234 230
235 231
236 232 // Kernel related calls.
237 233
238 234 CodeCell.prototype.set_kernel = function (kernel) {
239 235 this.kernel = kernel;
240 236 }
241 237
242 238 /**
243 239 * Execute current code cell to the kernel
244 240 * @method execute
245 241 */
246 242 CodeCell.prototype.execute = function () {
247 243 this.output_area.clear_output(true, true, true);
248 244 this.set_input_prompt('*');
249 245 this.element.addClass("running");
250 246 var callbacks = {
251 247 'execute_reply': $.proxy(this._handle_execute_reply, this),
252 248 'output': $.proxy(this.output_area.handle_output, this.output_area),
253 249 'clear_output': $.proxy(this.output_area.handle_clear_output, this.output_area),
254 250 'set_next_input': $.proxy(this._handle_set_next_input, this),
255 251 'input_request': $.proxy(this._handle_input_request, this)
256 252 };
257 253 var msg_id = this.kernel.execute(this.get_text(), callbacks, {silent: false});
258 254 };
259 255
260 256 /**
261 257 * @method _handle_execute_reply
262 258 * @private
263 259 */
264 260 CodeCell.prototype._handle_execute_reply = function (content) {
265 261 this.set_input_prompt(content.execution_count);
266 262 this.element.removeClass("running");
267 263 $([IPython.events]).trigger('set_dirty.Notebook', {value: true});
268 264 }
269 265
270 266 /**
271 267 * @method _handle_set_next_input
272 268 * @private
273 269 */
274 270 CodeCell.prototype._handle_set_next_input = function (text) {
275 271 var data = {'cell': this, 'text': text}
276 272 $([IPython.events]).trigger('set_next_input.Notebook', data);
277 273 }
278 274
279 275 /**
280 276 * @method _handle_input_request
281 277 * @private
282 278 */
283 279 CodeCell.prototype._handle_input_request = function (content) {
284 280 this.output_area.append_raw_input(content);
285 281 }
286 282
287 283
288 284 // Basic cell manipulation.
289 285
290 286 CodeCell.prototype.select = function () {
291 287 IPython.Cell.prototype.select.apply(this);
292 288 this.code_mirror.refresh();
293 289 this.code_mirror.focus();
294 290 this.auto_highlight();
295 291 // We used to need an additional refresh() after the focus, but
296 292 // it appears that this has been fixed in CM. This bug would show
297 293 // up on FF when a newly loaded markdown cell was edited.
298 294 };
299 295
300 296
301 297 CodeCell.prototype.select_all = function () {
302 298 var start = {line: 0, ch: 0};
303 299 var nlines = this.code_mirror.lineCount();
304 300 var last_line = this.code_mirror.getLine(nlines-1);
305 301 var end = {line: nlines-1, ch: last_line.length};
306 302 this.code_mirror.setSelection(start, end);
307 303 };
308 304
309 305
310 306 CodeCell.prototype.collapse = function () {
311 307 this.collapsed = true;
312 308 this.output_area.collapse();
313 309 };
314 310
315 311
316 312 CodeCell.prototype.expand = function () {
317 313 this.collapsed = false;
318 314 this.output_area.expand();
319 315 };
320 316
321 317
322 318 CodeCell.prototype.toggle_output = function () {
323 319 this.collapsed = Boolean(1 - this.collapsed);
324 320 this.output_area.toggle_output();
325 321 };
326 322
327 323
328 324 CodeCell.prototype.toggle_output_scroll = function () {
329 325 this.output_area.toggle_scroll();
330 326 };
331 327
332 328
333 329 CodeCell.input_prompt_classical = function (prompt_value, lines_number) {
334 330 var ns = prompt_value || "&nbsp;";
335 331 return 'In&nbsp;[' + ns + ']:'
336 332 };
337 333
338 334 CodeCell.input_prompt_continuation = function (prompt_value, lines_number) {
339 335 var html = [CodeCell.input_prompt_classical(prompt_value, lines_number)];
340 336 for(var i=1; i < lines_number; i++){html.push(['...:'])};
341 337 return html.join('</br>')
342 338 };
343 339
344 340 CodeCell.input_prompt_function = CodeCell.input_prompt_classical;
345 341
346 342
347 343 CodeCell.prototype.set_input_prompt = function (number) {
348 344 var nline = 1
349 345 if( this.code_mirror != undefined) {
350 346 nline = this.code_mirror.lineCount();
351 347 }
352 348 this.input_prompt_number = number;
353 349 var prompt_html = CodeCell.input_prompt_function(this.input_prompt_number, nline);
354 350 this.element.find('div.input_prompt').html(prompt_html);
355 351 };
356 352
357 353
358 354 CodeCell.prototype.clear_input = function () {
359 355 this.code_mirror.setValue('');
360 356 };
361 357
362 358
363 359 CodeCell.prototype.get_text = function () {
364 360 return this.code_mirror.getValue();
365 361 };
366 362
367 363
368 364 CodeCell.prototype.set_text = function (code) {
369 365 return this.code_mirror.setValue(code);
370 366 };
371 367
372 368
373 369 CodeCell.prototype.at_top = function () {
374 370 var cursor = this.code_mirror.getCursor();
375 371 if (cursor.line === 0 && cursor.ch === 0) {
376 372 return true;
377 373 } else {
378 374 return false;
379 375 }
380 376 };
381 377
382 378
383 379 CodeCell.prototype.at_bottom = function () {
384 380 var cursor = this.code_mirror.getCursor();
385 381 if (cursor.line === (this.code_mirror.lineCount()-1) && cursor.ch === this.code_mirror.getLine(cursor.line).length) {
386 382 return true;
387 383 } else {
388 384 return false;
389 385 }
390 386 };
391 387
392 388
393 389 CodeCell.prototype.clear_output = function (stdout, stderr, other) {
394 390 this.output_area.clear_output(stdout, stderr, other);
395 391 };
396 392
397 393
398 394 // JSON serialization
399 395
400 396 CodeCell.prototype.fromJSON = function (data) {
401 397 IPython.Cell.prototype.fromJSON.apply(this, arguments);
402 398 if (data.cell_type === 'code') {
403 399 if (data.input !== undefined) {
404 400 this.set_text(data.input);
405 401 // make this value the starting point, so that we can only undo
406 402 // to this state, instead of a blank cell
407 403 this.code_mirror.clearHistory();
408 404 this.auto_highlight();
409 405 }
410 406 if (data.prompt_number !== undefined) {
411 407 this.set_input_prompt(data.prompt_number);
412 408 } else {
413 409 this.set_input_prompt();
414 410 };
415 411 this.output_area.fromJSON(data.outputs);
416 412 if (data.collapsed !== undefined) {
417 413 if (data.collapsed) {
418 414 this.collapse();
419 415 } else {
420 416 this.expand();
421 417 };
422 418 };
423 419 };
424 420 };
425 421
426 422
427 423 CodeCell.prototype.toJSON = function () {
428 424 var data = IPython.Cell.prototype.toJSON.apply(this);
429 425 data.input = this.get_text();
430 426 data.cell_type = 'code';
431 427 if (this.input_prompt_number) {
432 428 data.prompt_number = this.input_prompt_number;
433 429 };
434 430 var outputs = this.output_area.toJSON();
435 431 data.outputs = outputs;
436 432 data.language = 'python';
437 433 data.collapsed = this.collapsed;
438 434 return data;
439 435 };
440 436
441 437
442 438 IPython.CodeCell = CodeCell;
443 439
444 440 return IPython;
445 441 }(IPython));
@@ -1,124 +1,114 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2011 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // On document ready
10 10 //============================================================================
11 11 "use strict";
12 12
13 13 // for the time beeing, we have to pass marked as a parameter here,
14 14 // as injecting require.js make marked not to put itself in the globals,
15 15 // which make both this file fail at setting marked configuration, and textcell.js
16 16 // which search marked into global.
17 17 require(['components/marked/lib/marked'],
18 18
19 19 function (marked) {
20 20
21 21 window.marked = marked
22 22
23 23 // monkey patch CM to be able to syntax highlight cell magics
24 24 // bug reported upstream,
25 25 // see https://github.com/marijnh/CodeMirror2/issues/670
26 26 if(CodeMirror.getMode(1,'text/plain').indent == undefined ){
27 27 console.log('patching CM for undefined indent');
28 28 CodeMirror.modes.null = function() {
29 29 return {token: function(stream) {stream.skipToEnd();},indent : function(){return 0}}
30 30 }
31 31 }
32 32
33 33 CodeMirror.patchedGetMode = function(config, mode){
34 34 var cmmode = CodeMirror.getMode(config, mode);
35 35 if(cmmode.indent == null)
36 36 {
37 37 console.log('patch mode "' , mode, '" on the fly');
38 38 cmmode.indent = function(){return 0};
39 39 }
40 40 return cmmode;
41 41 }
42 42 // end monkey patching CodeMirror
43 43
44 44 IPython.mathjaxutils.init();
45 45
46 IPython.read_only = $('body').data('readOnly') === 'True';
47 46 $('#ipython-main-app').addClass('border-box-sizing');
48 47 $('div#notebook_panel').addClass('border-box-sizing');
49 48
50 49 var baseProjectUrl = $('body').data('baseProjectUrl')
51 50
52 51 IPython.page = new IPython.Page();
53 52 IPython.layout_manager = new IPython.LayoutManager();
54 53 IPython.pager = new IPython.Pager('div#pager', 'div#pager_splitter');
55 54 IPython.quick_help = new IPython.QuickHelp();
56 55 IPython.login_widget = new IPython.LoginWidget('span#login_widget',{baseProjectUrl:baseProjectUrl});
57 IPython.notebook = new IPython.Notebook('div#notebook',{baseProjectUrl:baseProjectUrl, read_only:IPython.read_only});
56 IPython.notebook = new IPython.Notebook('div#notebook',{baseProjectUrl:baseProjectUrl});
58 57 IPython.save_widget = new IPython.SaveWidget('span#save_widget');
59 58 IPython.menubar = new IPython.MenuBar('#menubar',{baseProjectUrl:baseProjectUrl})
60 59 IPython.toolbar = new IPython.MainToolBar('#maintoolbar-container')
61 60 IPython.tooltip = new IPython.Tooltip()
62 61 IPython.notification_area = new IPython.NotificationArea('#notification_area')
63 62 IPython.notification_area.init_notification_widgets();
64 63
65 64 IPython.layout_manager.do_resize();
66 65
67 66 $('body').append('<div id="fonttest"><pre><span id="test1">x</span>'+
68 67 '<span id="test2" style="font-weight: bold;">x</span>'+
69 68 '<span id="test3" style="font-style: italic;">x</span></pre></div>')
70 69 var nh = $('#test1').innerHeight();
71 70 var bh = $('#test2').innerHeight();
72 71 var ih = $('#test3').innerHeight();
73 72 if(nh != bh || nh != ih) {
74 73 $('head').append('<style>.CodeMirror span { vertical-align: bottom; }</style>');
75 74 }
76 75 $('#fonttest').remove();
77 76
78 if(IPython.read_only){
79 // hide various elements from read-only view
80 $('div#pager').remove();
81 $('div#pager_splitter').remove();
82
83 // set the notebook name field as not modifiable
84 $('#notebook_name').attr('disabled','disabled')
85 }
86
87 77 IPython.page.show();
88 78
89 79 IPython.layout_manager.do_resize();
90 80 var first_load = function () {
91 81 IPython.layout_manager.do_resize();
92 82 var hash = document.location.hash;
93 83 if (hash) {
94 84 document.location.hash = '';
95 85 document.location.hash = hash;
96 86 }
97 87 IPython.notebook.set_autosave_interval(IPython.notebook.minimum_autosave_interval);
98 88 // only do this once
99 89 $([IPython.events]).off('notebook_loaded.Notebook', first_load);
100 90 };
101 91
102 92 $([IPython.events]).on('notebook_loaded.Notebook', first_load);
103 93 $([IPython.events]).trigger('app_initialized.NotebookApp');
104 94 IPython.notebook.load_notebook($('body').data('notebookId'));
105 95
106 96 if (marked) {
107 97 marked.setOptions({
108 98 gfm : true,
109 99 tables: true,
110 100 langPrefix: "language-",
111 101 highlight: function(code, lang) {
112 102 var highlighted;
113 103 try {
114 104 highlighted = hljs.highlight(lang, code, false);
115 105 } catch(err) {
116 106 highlighted = hljs.highlightAuto(code);
117 107 }
118 108 return highlighted.value;
119 109 }
120 110 })
121 111 }
122 112 }
123 113
124 114 );
@@ -1,2045 +1,2040 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2008-2011 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // Notebook
10 10 //============================================================================
11 11
12 12 var IPython = (function (IPython) {
13 13
14 14 var utils = IPython.utils;
15 15 var key = IPython.utils.keycodes;
16 16
17 17 /**
18 18 * A notebook contains and manages cells.
19 19 *
20 20 * @class Notebook
21 21 * @constructor
22 22 * @param {String} selector A jQuery selector for the notebook's DOM element
23 23 * @param {Object} [options] A config object
24 24 */
25 25 var Notebook = function (selector, options) {
26 26 var options = options || {};
27 27 this._baseProjectUrl = options.baseProjectUrl;
28 this.read_only = options.read_only || IPython.read_only;
29 28
30 29 this.element = $(selector);
31 30 this.element.scroll();
32 31 this.element.data("notebook", this);
33 32 this.next_prompt_number = 1;
34 33 this.kernel = null;
35 34 this.clipboard = null;
36 35 this.undelete_backup = null;
37 36 this.undelete_index = null;
38 37 this.undelete_below = false;
39 38 this.paste_enabled = false;
40 39 this.set_dirty(false);
41 40 this.metadata = {};
42 41 this._checkpoint_after_save = false;
43 42 this.last_checkpoint = null;
44 43 this.autosave_interval = 0;
45 44 this.autosave_timer = null;
46 45 // autosave *at most* every two minutes
47 46 this.minimum_autosave_interval = 120000;
48 47 // single worksheet for now
49 48 this.worksheet_metadata = {};
50 49 this.control_key_active = false;
51 50 this.notebook_id = null;
52 51 this.notebook_name = null;
53 52 this.notebook_name_blacklist_re = /[\/\\:]/;
54 53 this.nbformat = 3 // Increment this when changing the nbformat
55 54 this.nbformat_minor = 0 // Increment this when changing the nbformat
56 55 this.style();
57 56 this.create_elements();
58 57 this.bind_events();
59 58 };
60 59
61 60 /**
62 61 * Tweak the notebook's CSS style.
63 62 *
64 63 * @method style
65 64 */
66 65 Notebook.prototype.style = function () {
67 66 $('div#notebook').addClass('border-box-sizing');
68 67 };
69 68
70 69 /**
71 70 * Get the root URL of the notebook server.
72 71 *
73 72 * @method baseProjectUrl
74 73 * @return {String} The base project URL
75 74 */
76 75 Notebook.prototype.baseProjectUrl = function(){
77 76 return this._baseProjectUrl || $('body').data('baseProjectUrl');
78 77 };
79 78
80 79 /**
81 80 * Create an HTML and CSS representation of the notebook.
82 81 *
83 82 * @method create_elements
84 83 */
85 84 Notebook.prototype.create_elements = function () {
86 85 // We add this end_space div to the end of the notebook div to:
87 86 // i) provide a margin between the last cell and the end of the notebook
88 87 // ii) to prevent the div from scrolling up when the last cell is being
89 88 // edited, but is too low on the page, which browsers will do automatically.
90 89 var that = this;
91 90 this.container = $("<div/>").addClass("container").attr("id", "notebook-container");
92 91 var end_space = $('<div/>').addClass('end_space');
93 92 end_space.dblclick(function (e) {
94 if (that.read_only) return;
95 93 var ncells = that.ncells();
96 94 that.insert_cell_below('code',ncells-1);
97 95 });
98 96 this.element.append(this.container);
99 97 this.container.append(end_space);
100 98 $('div#notebook').addClass('border-box-sizing');
101 99 };
102 100
103 101 /**
104 102 * Bind JavaScript events: key presses and custom IPython events.
105 103 *
106 104 * @method bind_events
107 105 */
108 106 Notebook.prototype.bind_events = function () {
109 107 var that = this;
110 108
111 109 $([IPython.events]).on('set_next_input.Notebook', function (event, data) {
112 110 var index = that.find_cell_index(data.cell);
113 111 var new_cell = that.insert_cell_below('code',index);
114 112 new_cell.set_text(data.text);
115 113 that.dirty = true;
116 114 });
117 115
118 116 $([IPython.events]).on('set_dirty.Notebook', function (event, data) {
119 117 that.dirty = data.value;
120 118 });
121 119
122 120 $([IPython.events]).on('select.Cell', function (event, data) {
123 121 var index = that.find_cell_index(data.cell);
124 122 that.select(index);
125 123 });
126 124
127 125 $([IPython.events]).on('status_autorestarting.Kernel', function () {
128 126 IPython.dialog.modal({
129 127 title: "Kernel Restarting",
130 128 body: "The kernel appears to have died. It will restart automatically.",
131 129 buttons: {
132 130 OK : {
133 131 class : "btn-primary"
134 132 }
135 133 }
136 134 });
137 135 });
138 136
139 137
140 138 $(document).keydown(function (event) {
141 // console.log(event);
142 if (that.read_only) return true;
143 139
144 140 // Save (CTRL+S) or (AppleKey+S)
145 141 //metaKey = applekey on mac
146 142 if ((event.ctrlKey || event.metaKey) && event.keyCode==83) {
147 143 that.save_checkpoint();
148 144 event.preventDefault();
149 145 return false;
150 146 } else if (event.which === key.ESC) {
151 147 // Intercept escape at highest level to avoid closing
152 148 // websocket connection with firefox
153 149 IPython.pager.collapse();
154 150 event.preventDefault();
155 151 } else if (event.which === key.SHIFT) {
156 152 // ignore shift keydown
157 153 return true;
158 154 }
159 155 if (event.which === key.UPARROW && !event.shiftKey) {
160 156 var cell = that.get_selected_cell();
161 157 if (cell && cell.at_top()) {
162 158 event.preventDefault();
163 159 that.select_prev();
164 160 };
165 161 } else if (event.which === key.DOWNARROW && !event.shiftKey) {
166 162 var cell = that.get_selected_cell();
167 163 if (cell && cell.at_bottom()) {
168 164 event.preventDefault();
169 165 that.select_next();
170 166 };
171 167 } else if (event.which === key.ENTER && event.shiftKey) {
172 168 that.execute_selected_cell();
173 169 return false;
174 170 } else if (event.which === key.ENTER && event.altKey) {
175 171 // Execute code cell, and insert new in place
176 172 that.execute_selected_cell();
177 173 // Only insert a new cell, if we ended up in an already populated cell
178 174 if (/\S/.test(that.get_selected_cell().get_text()) == true) {
179 175 that.insert_cell_above('code');
180 176 }
181 177 return false;
182 178 } else if (event.which === key.ENTER && event.ctrlKey) {
183 179 that.execute_selected_cell({terminal:true});
184 180 return false;
185 181 } else if (event.which === 77 && event.ctrlKey && that.control_key_active == false) {
186 182 that.control_key_active = true;
187 183 return false;
188 184 } else if (event.which === 88 && that.control_key_active) {
189 185 // Cut selected cell = x
190 186 that.cut_cell();
191 187 that.control_key_active = false;
192 188 return false;
193 189 } else if (event.which === 67 && that.control_key_active) {
194 190 // Copy selected cell = c
195 191 that.copy_cell();
196 192 that.control_key_active = false;
197 193 return false;
198 194 } else if (event.which === 86 && that.control_key_active) {
199 195 // Paste below selected cell = v
200 196 that.paste_cell_below();
201 197 that.control_key_active = false;
202 198 return false;
203 199 } else if (event.which === 68 && that.control_key_active) {
204 200 // Delete selected cell = d
205 201 that.delete_cell();
206 202 that.control_key_active = false;
207 203 return false;
208 204 } else if (event.which === 65 && that.control_key_active) {
209 205 // Insert code cell above selected = a
210 206 that.insert_cell_above('code');
211 207 that.control_key_active = false;
212 208 return false;
213 209 } else if (event.which === 66 && that.control_key_active) {
214 210 // Insert code cell below selected = b
215 211 that.insert_cell_below('code');
216 212 that.control_key_active = false;
217 213 return false;
218 214 } else if (event.which === 89 && that.control_key_active) {
219 215 // To code = y
220 216 that.to_code();
221 217 that.control_key_active = false;
222 218 return false;
223 219 } else if (event.which === 77 && that.control_key_active) {
224 220 // To markdown = m
225 221 that.to_markdown();
226 222 that.control_key_active = false;
227 223 return false;
228 224 } else if (event.which === 84 && that.control_key_active) {
229 225 // To Raw = t
230 226 that.to_raw();
231 227 that.control_key_active = false;
232 228 return false;
233 229 } else if (event.which === 49 && that.control_key_active) {
234 230 // To Heading 1 = 1
235 231 that.to_heading(undefined, 1);
236 232 that.control_key_active = false;
237 233 return false;
238 234 } else if (event.which === 50 && that.control_key_active) {
239 235 // To Heading 2 = 2
240 236 that.to_heading(undefined, 2);
241 237 that.control_key_active = false;
242 238 return false;
243 239 } else if (event.which === 51 && that.control_key_active) {
244 240 // To Heading 3 = 3
245 241 that.to_heading(undefined, 3);
246 242 that.control_key_active = false;
247 243 return false;
248 244 } else if (event.which === 52 && that.control_key_active) {
249 245 // To Heading 4 = 4
250 246 that.to_heading(undefined, 4);
251 247 that.control_key_active = false;
252 248 return false;
253 249 } else if (event.which === 53 && that.control_key_active) {
254 250 // To Heading 5 = 5
255 251 that.to_heading(undefined, 5);
256 252 that.control_key_active = false;
257 253 return false;
258 254 } else if (event.which === 54 && that.control_key_active) {
259 255 // To Heading 6 = 6
260 256 that.to_heading(undefined, 6);
261 257 that.control_key_active = false;
262 258 return false;
263 259 } else if (event.which === 79 && that.control_key_active) {
264 260 // Toggle output = o
265 261 if (event.shiftKey){
266 262 that.toggle_output_scroll();
267 263 } else {
268 264 that.toggle_output();
269 265 }
270 266 that.control_key_active = false;
271 267 return false;
272 268 } else if (event.which === 83 && that.control_key_active) {
273 269 // Save notebook = s
274 270 that.save_checkpoint();
275 271 that.control_key_active = false;
276 272 return false;
277 273 } else if (event.which === 74 && that.control_key_active) {
278 274 // Move cell down = j
279 275 that.move_cell_down();
280 276 that.control_key_active = false;
281 277 return false;
282 278 } else if (event.which === 75 && that.control_key_active) {
283 279 // Move cell up = k
284 280 that.move_cell_up();
285 281 that.control_key_active = false;
286 282 return false;
287 283 } else if (event.which === 80 && that.control_key_active) {
288 284 // Select previous = p
289 285 that.select_prev();
290 286 that.control_key_active = false;
291 287 return false;
292 288 } else if (event.which === 78 && that.control_key_active) {
293 289 // Select next = n
294 290 that.select_next();
295 291 that.control_key_active = false;
296 292 return false;
297 293 } else if (event.which === 76 && that.control_key_active) {
298 294 // Toggle line numbers = l
299 295 that.cell_toggle_line_numbers();
300 296 that.control_key_active = false;
301 297 return false;
302 298 } else if (event.which === 73 && that.control_key_active) {
303 299 // Interrupt kernel = i
304 300 that.kernel.interrupt();
305 301 that.control_key_active = false;
306 302 return false;
307 303 } else if (event.which === 190 && that.control_key_active) {
308 304 // Restart kernel = . # matches qt console
309 305 that.restart_kernel();
310 306 that.control_key_active = false;
311 307 return false;
312 308 } else if (event.which === 72 && that.control_key_active) {
313 309 // Show keyboard shortcuts = h
314 310 IPython.quick_help.show_keyboard_shortcuts();
315 311 that.control_key_active = false;
316 312 return false;
317 313 } else if (event.which === 90 && that.control_key_active) {
318 314 // Undo last cell delete = z
319 315 that.undelete();
320 316 that.control_key_active = false;
321 317 return false;
322 318 } else if (event.which === 189 && that.control_key_active) {
323 319 // Split cell = -
324 320 that.split_cell();
325 321 that.control_key_active = false;
326 322 return false;
327 323 } else if (that.control_key_active) {
328 324 that.control_key_active = false;
329 325 return true;
330 326 }
331 327 return true;
332 328 });
333 329
334 330 var collapse_time = function(time){
335 331 var app_height = $('#ipython-main-app').height(); // content height
336 332 var splitter_height = $('div#pager_splitter').outerHeight(true);
337 333 var new_height = app_height - splitter_height;
338 334 that.element.animate({height : new_height + 'px'}, time);
339 335 }
340 336
341 337 this.element.bind('collapse_pager', function (event,extrap) {
342 338 var time = (extrap != undefined) ? ((extrap.duration != undefined ) ? extrap.duration : 'fast') : 'fast';
343 339 collapse_time(time);
344 340 });
345 341
346 342 var expand_time = function(time) {
347 343 var app_height = $('#ipython-main-app').height(); // content height
348 344 var splitter_height = $('div#pager_splitter').outerHeight(true);
349 345 var pager_height = $('div#pager').outerHeight(true);
350 346 var new_height = app_height - pager_height - splitter_height;
351 347 that.element.animate({height : new_height + 'px'}, time);
352 348 }
353 349
354 350 this.element.bind('expand_pager', function (event, extrap) {
355 351 var time = (extrap != undefined) ? ((extrap.duration != undefined ) ? extrap.duration : 'fast') : 'fast';
356 352 expand_time(time);
357 353 });
358 354
359 355 // Firefox 22 broke $(window).on("beforeunload")
360 356 // I'm not sure why or how.
361 357 window.onbeforeunload = function (e) {
362 358 // TODO: Make killing the kernel configurable.
363 359 var kill_kernel = false;
364 360 if (kill_kernel) {
365 361 that.kernel.kill();
366 362 }
367 363 // if we are autosaving, trigger an autosave on nav-away.
368 364 // still warn, because if we don't the autosave may fail.
369 if (that.dirty && ! that.read_only) {
365 if (that.dirty) {
370 366 if ( that.autosave_interval ) {
371 367 // schedule autosave in a timeout
372 368 // this gives you a chance to forcefully discard changes
373 369 // by reloading the page if you *really* want to.
374 370 // the timer doesn't start until you *dismiss* the dialog.
375 371 setTimeout(function () {
376 372 if (that.dirty) {
377 373 that.save_notebook();
378 374 }
379 375 }, 1000);
380 376 return "Autosave in progress, latest changes may be lost.";
381 377 } else {
382 378 return "Unsaved changes will be lost.";
383 379 }
384 380 };
385 381 // Null is the *only* return value that will make the browser not
386 382 // pop up the "don't leave" dialog.
387 383 return null;
388 384 };
389 385 };
390 386
391 387 /**
392 388 * Set the dirty flag, and trigger the set_dirty.Notebook event
393 389 *
394 390 * @method set_dirty
395 391 */
396 392 Notebook.prototype.set_dirty = function (value) {
397 393 if (value === undefined) {
398 394 value = true;
399 395 }
400 396 if (this.dirty == value) {
401 397 return;
402 398 }
403 399 $([IPython.events]).trigger('set_dirty.Notebook', {value: value});
404 400 };
405 401
406 402 /**
407 403 * Scroll the top of the page to a given cell.
408 404 *
409 405 * @method scroll_to_cell
410 406 * @param {Number} cell_number An index of the cell to view
411 407 * @param {Number} time Animation time in milliseconds
412 408 * @return {Number} Pixel offset from the top of the container
413 409 */
414 410 Notebook.prototype.scroll_to_cell = function (cell_number, time) {
415 411 var cells = this.get_cells();
416 412 var time = time || 0;
417 413 cell_number = Math.min(cells.length-1,cell_number);
418 414 cell_number = Math.max(0 ,cell_number);
419 415 var scroll_value = cells[cell_number].element.position().top-cells[0].element.position().top ;
420 416 this.element.animate({scrollTop:scroll_value}, time);
421 417 return scroll_value;
422 418 };
423 419
424 420 /**
425 421 * Scroll to the bottom of the page.
426 422 *
427 423 * @method scroll_to_bottom
428 424 */
429 425 Notebook.prototype.scroll_to_bottom = function () {
430 426 this.element.animate({scrollTop:this.element.get(0).scrollHeight}, 0);
431 427 };
432 428
433 429 /**
434 430 * Scroll to the top of the page.
435 431 *
436 432 * @method scroll_to_top
437 433 */
438 434 Notebook.prototype.scroll_to_top = function () {
439 435 this.element.animate({scrollTop:0}, 0);
440 436 };
441 437
442 438
443 439 // Cell indexing, retrieval, etc.
444 440
445 441 /**
446 442 * Get all cell elements in the notebook.
447 443 *
448 444 * @method get_cell_elements
449 445 * @return {jQuery} A selector of all cell elements
450 446 */
451 447 Notebook.prototype.get_cell_elements = function () {
452 448 return this.container.children("div.cell");
453 449 };
454 450
455 451 /**
456 452 * Get a particular cell element.
457 453 *
458 454 * @method get_cell_element
459 455 * @param {Number} index An index of a cell to select
460 456 * @return {jQuery} A selector of the given cell.
461 457 */
462 458 Notebook.prototype.get_cell_element = function (index) {
463 459 var result = null;
464 460 var e = this.get_cell_elements().eq(index);
465 461 if (e.length !== 0) {
466 462 result = e;
467 463 }
468 464 return result;
469 465 };
470 466
471 467 /**
472 468 * Count the cells in this notebook.
473 469 *
474 470 * @method ncells
475 471 * @return {Number} The number of cells in this notebook
476 472 */
477 473 Notebook.prototype.ncells = function () {
478 474 return this.get_cell_elements().length;
479 475 };
480 476
481 477 /**
482 478 * Get all Cell objects in this notebook.
483 479 *
484 480 * @method get_cells
485 481 * @return {Array} This notebook's Cell objects
486 482 */
487 483 // TODO: we are often calling cells as cells()[i], which we should optimize
488 484 // to cells(i) or a new method.
489 485 Notebook.prototype.get_cells = function () {
490 486 return this.get_cell_elements().toArray().map(function (e) {
491 487 return $(e).data("cell");
492 488 });
493 489 };
494 490
495 491 /**
496 492 * Get a Cell object from this notebook.
497 493 *
498 494 * @method get_cell
499 495 * @param {Number} index An index of a cell to retrieve
500 496 * @return {Cell} A particular cell
501 497 */
502 498 Notebook.prototype.get_cell = function (index) {
503 499 var result = null;
504 500 var ce = this.get_cell_element(index);
505 501 if (ce !== null) {
506 502 result = ce.data('cell');
507 503 }
508 504 return result;
509 505 }
510 506
511 507 /**
512 508 * Get the cell below a given cell.
513 509 *
514 510 * @method get_next_cell
515 511 * @param {Cell} cell The provided cell
516 512 * @return {Cell} The next cell
517 513 */
518 514 Notebook.prototype.get_next_cell = function (cell) {
519 515 var result = null;
520 516 var index = this.find_cell_index(cell);
521 517 if (this.is_valid_cell_index(index+1)) {
522 518 result = this.get_cell(index+1);
523 519 }
524 520 return result;
525 521 }
526 522
527 523 /**
528 524 * Get the cell above a given cell.
529 525 *
530 526 * @method get_prev_cell
531 527 * @param {Cell} cell The provided cell
532 528 * @return {Cell} The previous cell
533 529 */
534 530 Notebook.prototype.get_prev_cell = function (cell) {
535 531 // TODO: off-by-one
536 532 // nb.get_prev_cell(nb.get_cell(1)) is null
537 533 var result = null;
538 534 var index = this.find_cell_index(cell);
539 535 if (index !== null && index > 1) {
540 536 result = this.get_cell(index-1);
541 537 }
542 538 return result;
543 539 }
544 540
545 541 /**
546 542 * Get the numeric index of a given cell.
547 543 *
548 544 * @method find_cell_index
549 545 * @param {Cell} cell The provided cell
550 546 * @return {Number} The cell's numeric index
551 547 */
552 548 Notebook.prototype.find_cell_index = function (cell) {
553 549 var result = null;
554 550 this.get_cell_elements().filter(function (index) {
555 551 if ($(this).data("cell") === cell) {
556 552 result = index;
557 553 };
558 554 });
559 555 return result;
560 556 };
561 557
562 558 /**
563 559 * Get a given index , or the selected index if none is provided.
564 560 *
565 561 * @method index_or_selected
566 562 * @param {Number} index A cell's index
567 563 * @return {Number} The given index, or selected index if none is provided.
568 564 */
569 565 Notebook.prototype.index_or_selected = function (index) {
570 566 var i;
571 567 if (index === undefined || index === null) {
572 568 i = this.get_selected_index();
573 569 if (i === null) {
574 570 i = 0;
575 571 }
576 572 } else {
577 573 i = index;
578 574 }
579 575 return i;
580 576 };
581 577
582 578 /**
583 579 * Get the currently selected cell.
584 580 * @method get_selected_cell
585 581 * @return {Cell} The selected cell
586 582 */
587 583 Notebook.prototype.get_selected_cell = function () {
588 584 var index = this.get_selected_index();
589 585 return this.get_cell(index);
590 586 };
591 587
592 588 /**
593 589 * Check whether a cell index is valid.
594 590 *
595 591 * @method is_valid_cell_index
596 592 * @param {Number} index A cell index
597 593 * @return True if the index is valid, false otherwise
598 594 */
599 595 Notebook.prototype.is_valid_cell_index = function (index) {
600 596 if (index !== null && index >= 0 && index < this.ncells()) {
601 597 return true;
602 598 } else {
603 599 return false;
604 600 };
605 601 }
606 602
607 603 /**
608 604 * Get the index of the currently selected cell.
609 605
610 606 * @method get_selected_index
611 607 * @return {Number} The selected cell's numeric index
612 608 */
613 609 Notebook.prototype.get_selected_index = function () {
614 610 var result = null;
615 611 this.get_cell_elements().filter(function (index) {
616 612 if ($(this).data("cell").selected === true) {
617 613 result = index;
618 614 };
619 615 });
620 616 return result;
621 617 };
622 618
623 619
624 620 // Cell selection.
625 621
626 622 /**
627 623 * Programmatically select a cell.
628 624 *
629 625 * @method select
630 626 * @param {Number} index A cell's index
631 627 * @return {Notebook} This notebook
632 628 */
633 629 Notebook.prototype.select = function (index) {
634 630 if (this.is_valid_cell_index(index)) {
635 631 var sindex = this.get_selected_index()
636 632 if (sindex !== null && index !== sindex) {
637 633 this.get_cell(sindex).unselect();
638 634 };
639 635 var cell = this.get_cell(index);
640 636 cell.select();
641 637 if (cell.cell_type === 'heading') {
642 638 $([IPython.events]).trigger('selected_cell_type_changed.Notebook',
643 639 {'cell_type':cell.cell_type,level:cell.level}
644 640 );
645 641 } else {
646 642 $([IPython.events]).trigger('selected_cell_type_changed.Notebook',
647 643 {'cell_type':cell.cell_type}
648 644 );
649 645 };
650 646 };
651 647 return this;
652 648 };
653 649
654 650 /**
655 651 * Programmatically select the next cell.
656 652 *
657 653 * @method select_next
658 654 * @return {Notebook} This notebook
659 655 */
660 656 Notebook.prototype.select_next = function () {
661 657 var index = this.get_selected_index();
662 658 this.select(index+1);
663 659 return this;
664 660 };
665 661
666 662 /**
667 663 * Programmatically select the previous cell.
668 664 *
669 665 * @method select_prev
670 666 * @return {Notebook} This notebook
671 667 */
672 668 Notebook.prototype.select_prev = function () {
673 669 var index = this.get_selected_index();
674 670 this.select(index-1);
675 671 return this;
676 672 };
677 673
678 674
679 675 // Cell movement
680 676
681 677 /**
682 678 * Move given (or selected) cell up and select it.
683 679 *
684 680 * @method move_cell_up
685 681 * @param [index] {integer} cell index
686 682 * @return {Notebook} This notebook
687 683 **/
688 684 Notebook.prototype.move_cell_up = function (index) {
689 685 var i = this.index_or_selected(index);
690 686 if (this.is_valid_cell_index(i) && i > 0) {
691 687 var pivot = this.get_cell_element(i-1);
692 688 var tomove = this.get_cell_element(i);
693 689 if (pivot !== null && tomove !== null) {
694 690 tomove.detach();
695 691 pivot.before(tomove);
696 692 this.select(i-1);
697 693 };
698 694 this.set_dirty(true);
699 695 };
700 696 return this;
701 697 };
702 698
703 699
704 700 /**
705 701 * Move given (or selected) cell down and select it
706 702 *
707 703 * @method move_cell_down
708 704 * @param [index] {integer} cell index
709 705 * @return {Notebook} This notebook
710 706 **/
711 707 Notebook.prototype.move_cell_down = function (index) {
712 708 var i = this.index_or_selected(index);
713 709 if ( this.is_valid_cell_index(i) && this.is_valid_cell_index(i+1)) {
714 710 var pivot = this.get_cell_element(i+1);
715 711 var tomove = this.get_cell_element(i);
716 712 if (pivot !== null && tomove !== null) {
717 713 tomove.detach();
718 714 pivot.after(tomove);
719 715 this.select(i+1);
720 716 };
721 717 };
722 718 this.set_dirty();
723 719 return this;
724 720 };
725 721
726 722
727 723 // Insertion, deletion.
728 724
729 725 /**
730 726 * Delete a cell from the notebook.
731 727 *
732 728 * @method delete_cell
733 729 * @param [index] A cell's numeric index
734 730 * @return {Notebook} This notebook
735 731 */
736 732 Notebook.prototype.delete_cell = function (index) {
737 733 var i = this.index_or_selected(index);
738 734 var cell = this.get_selected_cell();
739 735 this.undelete_backup = cell.toJSON();
740 736 $('#undelete_cell').removeClass('disabled');
741 737 if (this.is_valid_cell_index(i)) {
742 738 var ce = this.get_cell_element(i);
743 739 ce.remove();
744 740 if (i === (this.ncells())) {
745 741 this.select(i-1);
746 742 this.undelete_index = i - 1;
747 743 this.undelete_below = true;
748 744 } else {
749 745 this.select(i);
750 746 this.undelete_index = i;
751 747 this.undelete_below = false;
752 748 };
753 749 $([IPython.events]).trigger('delete.Cell', {'cell': cell, 'index': i});
754 750 this.set_dirty(true);
755 751 };
756 752 return this;
757 753 };
758 754
759 755 /**
760 756 * Insert a cell so that after insertion the cell is at given index.
761 757 *
762 758 * Similar to insert_above, but index parameter is mandatory
763 759 *
764 760 * Index will be brought back into the accissible range [0,n]
765 761 *
766 762 * @method insert_cell_at_index
767 763 * @param type {string} in ['code','markdown','heading']
768 764 * @param [index] {int} a valid index where to inser cell
769 765 *
770 766 * @return cell {cell|null} created cell or null
771 767 **/
772 768 Notebook.prototype.insert_cell_at_index = function(type, index){
773 769
774 770 var ncells = this.ncells();
775 771 var index = Math.min(index,ncells);
776 772 index = Math.max(index,0);
777 773 var cell = null;
778 774
779 775 if (ncells === 0 || this.is_valid_cell_index(index) || index === ncells) {
780 776 if (type === 'code') {
781 777 cell = new IPython.CodeCell(this.kernel);
782 778 cell.set_input_prompt();
783 779 } else if (type === 'markdown') {
784 780 cell = new IPython.MarkdownCell();
785 781 } else if (type === 'raw') {
786 782 cell = new IPython.RawCell();
787 783 } else if (type === 'heading') {
788 784 cell = new IPython.HeadingCell();
789 785 }
790 786
791 787 if(this._insert_element_at_index(cell.element,index)){
792 788 cell.render();
793 789 this.select(this.find_cell_index(cell));
794 790 $([IPython.events]).trigger('create.Cell', {'cell': cell, 'index': index});
795 791 this.set_dirty(true);
796 792 }
797 793 }
798 794 return cell;
799 795
800 796 };
801 797
802 798 /**
803 799 * Insert an element at given cell index.
804 800 *
805 801 * @method _insert_element_at_index
806 802 * @param element {dom element} a cell element
807 803 * @param [index] {int} a valid index where to inser cell
808 804 * @private
809 805 *
810 806 * return true if everything whent fine.
811 807 **/
812 808 Notebook.prototype._insert_element_at_index = function(element, index){
813 809 if (element === undefined){
814 810 return false;
815 811 }
816 812
817 813 var ncells = this.ncells();
818 814
819 815 if (ncells === 0) {
820 816 // special case append if empty
821 817 this.element.find('div.end_space').before(element);
822 818 } else if ( ncells === index ) {
823 819 // special case append it the end, but not empty
824 820 this.get_cell_element(index-1).after(element);
825 821 } else if (this.is_valid_cell_index(index)) {
826 822 // otherwise always somewhere to append to
827 823 this.get_cell_element(index).before(element);
828 824 } else {
829 825 return false;
830 826 }
831 827
832 828 if (this.undelete_index !== null && index <= this.undelete_index) {
833 829 this.undelete_index = this.undelete_index + 1;
834 830 this.set_dirty(true);
835 831 }
836 832 return true;
837 833 };
838 834
839 835 /**
840 836 * Insert a cell of given type above given index, or at top
841 837 * of notebook if index smaller than 0.
842 838 *
843 839 * default index value is the one of currently selected cell
844 840 *
845 841 * @method insert_cell_above
846 842 * @param type {string} cell type
847 843 * @param [index] {integer}
848 844 *
849 845 * @return handle to created cell or null
850 846 **/
851 847 Notebook.prototype.insert_cell_above = function (type, index) {
852 848 index = this.index_or_selected(index);
853 849 return this.insert_cell_at_index(type, index);
854 850 };
855 851
856 852 /**
857 853 * Insert a cell of given type below given index, or at bottom
858 854 * of notebook if index greater thatn number of cell
859 855 *
860 856 * default index value is the one of currently selected cell
861 857 *
862 858 * @method insert_cell_below
863 859 * @param type {string} cell type
864 860 * @param [index] {integer}
865 861 *
866 862 * @return handle to created cell or null
867 863 *
868 864 **/
869 865 Notebook.prototype.insert_cell_below = function (type, index) {
870 866 index = this.index_or_selected(index);
871 867 return this.insert_cell_at_index(type, index+1);
872 868 };
873 869
874 870
875 871 /**
876 872 * Insert cell at end of notebook
877 873 *
878 874 * @method insert_cell_at_bottom
879 875 * @param {String} type cell type
880 876 *
881 877 * @return the added cell; or null
882 878 **/
883 879 Notebook.prototype.insert_cell_at_bottom = function (type){
884 880 var len = this.ncells();
885 881 return this.insert_cell_below(type,len-1);
886 882 };
887 883
888 884 /**
889 885 * Turn a cell into a code cell.
890 886 *
891 887 * @method to_code
892 888 * @param {Number} [index] A cell's index
893 889 */
894 890 Notebook.prototype.to_code = function (index) {
895 891 var i = this.index_or_selected(index);
896 892 if (this.is_valid_cell_index(i)) {
897 893 var source_element = this.get_cell_element(i);
898 894 var source_cell = source_element.data("cell");
899 895 if (!(source_cell instanceof IPython.CodeCell)) {
900 896 var target_cell = this.insert_cell_below('code',i);
901 897 var text = source_cell.get_text();
902 898 if (text === source_cell.placeholder) {
903 899 text = '';
904 900 }
905 901 target_cell.set_text(text);
906 902 // make this value the starting point, so that we can only undo
907 903 // to this state, instead of a blank cell
908 904 target_cell.code_mirror.clearHistory();
909 905 source_element.remove();
910 906 this.set_dirty(true);
911 907 };
912 908 };
913 909 };
914 910
915 911 /**
916 912 * Turn a cell into a Markdown cell.
917 913 *
918 914 * @method to_markdown
919 915 * @param {Number} [index] A cell's index
920 916 */
921 917 Notebook.prototype.to_markdown = function (index) {
922 918 var i = this.index_or_selected(index);
923 919 if (this.is_valid_cell_index(i)) {
924 920 var source_element = this.get_cell_element(i);
925 921 var source_cell = source_element.data("cell");
926 922 if (!(source_cell instanceof IPython.MarkdownCell)) {
927 923 var target_cell = this.insert_cell_below('markdown',i);
928 924 var text = source_cell.get_text();
929 925 if (text === source_cell.placeholder) {
930 926 text = '';
931 927 };
932 928 // The edit must come before the set_text.
933 929 target_cell.edit();
934 930 target_cell.set_text(text);
935 931 // make this value the starting point, so that we can only undo
936 932 // to this state, instead of a blank cell
937 933 target_cell.code_mirror.clearHistory();
938 934 source_element.remove();
939 935 this.set_dirty(true);
940 936 };
941 937 };
942 938 };
943 939
944 940 /**
945 941 * Turn a cell into a raw text cell.
946 942 *
947 943 * @method to_raw
948 944 * @param {Number} [index] A cell's index
949 945 */
950 946 Notebook.prototype.to_raw = function (index) {
951 947 var i = this.index_or_selected(index);
952 948 if (this.is_valid_cell_index(i)) {
953 949 var source_element = this.get_cell_element(i);
954 950 var source_cell = source_element.data("cell");
955 951 var target_cell = null;
956 952 if (!(source_cell instanceof IPython.RawCell)) {
957 953 target_cell = this.insert_cell_below('raw',i);
958 954 var text = source_cell.get_text();
959 955 if (text === source_cell.placeholder) {
960 956 text = '';
961 957 };
962 958 // The edit must come before the set_text.
963 959 target_cell.edit();
964 960 target_cell.set_text(text);
965 961 // make this value the starting point, so that we can only undo
966 962 // to this state, instead of a blank cell
967 963 target_cell.code_mirror.clearHistory();
968 964 source_element.remove();
969 965 this.set_dirty(true);
970 966 };
971 967 };
972 968 };
973 969
974 970 /**
975 971 * Turn a cell into a heading cell.
976 972 *
977 973 * @method to_heading
978 974 * @param {Number} [index] A cell's index
979 975 * @param {Number} [level] A heading level (e.g., 1 becomes &lt;h1&gt;)
980 976 */
981 977 Notebook.prototype.to_heading = function (index, level) {
982 978 level = level || 1;
983 979 var i = this.index_or_selected(index);
984 980 if (this.is_valid_cell_index(i)) {
985 981 var source_element = this.get_cell_element(i);
986 982 var source_cell = source_element.data("cell");
987 983 var target_cell = null;
988 984 if (source_cell instanceof IPython.HeadingCell) {
989 985 source_cell.set_level(level);
990 986 } else {
991 987 target_cell = this.insert_cell_below('heading',i);
992 988 var text = source_cell.get_text();
993 989 if (text === source_cell.placeholder) {
994 990 text = '';
995 991 };
996 992 // The edit must come before the set_text.
997 993 target_cell.set_level(level);
998 994 target_cell.edit();
999 995 target_cell.set_text(text);
1000 996 // make this value the starting point, so that we can only undo
1001 997 // to this state, instead of a blank cell
1002 998 target_cell.code_mirror.clearHistory();
1003 999 source_element.remove();
1004 1000 this.set_dirty(true);
1005 1001 };
1006 1002 $([IPython.events]).trigger('selected_cell_type_changed.Notebook',
1007 1003 {'cell_type':'heading',level:level}
1008 1004 );
1009 1005 };
1010 1006 };
1011 1007
1012 1008
1013 1009 // Cut/Copy/Paste
1014 1010
1015 1011 /**
1016 1012 * Enable UI elements for pasting cells.
1017 1013 *
1018 1014 * @method enable_paste
1019 1015 */
1020 1016 Notebook.prototype.enable_paste = function () {
1021 1017 var that = this;
1022 1018 if (!this.paste_enabled) {
1023 1019 $('#paste_cell_replace').removeClass('disabled')
1024 1020 .on('click', function () {that.paste_cell_replace();});
1025 1021 $('#paste_cell_above').removeClass('disabled')
1026 1022 .on('click', function () {that.paste_cell_above();});
1027 1023 $('#paste_cell_below').removeClass('disabled')
1028 1024 .on('click', function () {that.paste_cell_below();});
1029 1025 this.paste_enabled = true;
1030 1026 };
1031 1027 };
1032 1028
1033 1029 /**
1034 1030 * Disable UI elements for pasting cells.
1035 1031 *
1036 1032 * @method disable_paste
1037 1033 */
1038 1034 Notebook.prototype.disable_paste = function () {
1039 1035 if (this.paste_enabled) {
1040 1036 $('#paste_cell_replace').addClass('disabled').off('click');
1041 1037 $('#paste_cell_above').addClass('disabled').off('click');
1042 1038 $('#paste_cell_below').addClass('disabled').off('click');
1043 1039 this.paste_enabled = false;
1044 1040 };
1045 1041 };
1046 1042
1047 1043 /**
1048 1044 * Cut a cell.
1049 1045 *
1050 1046 * @method cut_cell
1051 1047 */
1052 1048 Notebook.prototype.cut_cell = function () {
1053 1049 this.copy_cell();
1054 1050 this.delete_cell();
1055 1051 }
1056 1052
1057 1053 /**
1058 1054 * Copy a cell.
1059 1055 *
1060 1056 * @method copy_cell
1061 1057 */
1062 1058 Notebook.prototype.copy_cell = function () {
1063 1059 var cell = this.get_selected_cell();
1064 1060 this.clipboard = cell.toJSON();
1065 1061 this.enable_paste();
1066 1062 };
1067 1063
1068 1064 /**
1069 1065 * Replace the selected cell with a cell in the clipboard.
1070 1066 *
1071 1067 * @method paste_cell_replace
1072 1068 */
1073 1069 Notebook.prototype.paste_cell_replace = function () {
1074 1070 if (this.clipboard !== null && this.paste_enabled) {
1075 1071 var cell_data = this.clipboard;
1076 1072 var new_cell = this.insert_cell_above(cell_data.cell_type);
1077 1073 new_cell.fromJSON(cell_data);
1078 1074 var old_cell = this.get_next_cell(new_cell);
1079 1075 this.delete_cell(this.find_cell_index(old_cell));
1080 1076 this.select(this.find_cell_index(new_cell));
1081 1077 };
1082 1078 };
1083 1079
1084 1080 /**
1085 1081 * Paste a cell from the clipboard above the selected cell.
1086 1082 *
1087 1083 * @method paste_cell_above
1088 1084 */
1089 1085 Notebook.prototype.paste_cell_above = function () {
1090 1086 if (this.clipboard !== null && this.paste_enabled) {
1091 1087 var cell_data = this.clipboard;
1092 1088 var new_cell = this.insert_cell_above(cell_data.cell_type);
1093 1089 new_cell.fromJSON(cell_data);
1094 1090 };
1095 1091 };
1096 1092
1097 1093 /**
1098 1094 * Paste a cell from the clipboard below the selected cell.
1099 1095 *
1100 1096 * @method paste_cell_below
1101 1097 */
1102 1098 Notebook.prototype.paste_cell_below = function () {
1103 1099 if (this.clipboard !== null && this.paste_enabled) {
1104 1100 var cell_data = this.clipboard;
1105 1101 var new_cell = this.insert_cell_below(cell_data.cell_type);
1106 1102 new_cell.fromJSON(cell_data);
1107 1103 };
1108 1104 };
1109 1105
1110 1106 // Cell undelete
1111 1107
1112 1108 /**
1113 1109 * Restore the most recently deleted cell.
1114 1110 *
1115 1111 * @method undelete
1116 1112 */
1117 1113 Notebook.prototype.undelete = function() {
1118 1114 if (this.undelete_backup !== null && this.undelete_index !== null) {
1119 1115 var current_index = this.get_selected_index();
1120 1116 if (this.undelete_index < current_index) {
1121 1117 current_index = current_index + 1;
1122 1118 }
1123 1119 if (this.undelete_index >= this.ncells()) {
1124 1120 this.select(this.ncells() - 1);
1125 1121 }
1126 1122 else {
1127 1123 this.select(this.undelete_index);
1128 1124 }
1129 1125 var cell_data = this.undelete_backup;
1130 1126 var new_cell = null;
1131 1127 if (this.undelete_below) {
1132 1128 new_cell = this.insert_cell_below(cell_data.cell_type);
1133 1129 } else {
1134 1130 new_cell = this.insert_cell_above(cell_data.cell_type);
1135 1131 }
1136 1132 new_cell.fromJSON(cell_data);
1137 1133 this.select(current_index);
1138 1134 this.undelete_backup = null;
1139 1135 this.undelete_index = null;
1140 1136 }
1141 1137 $('#undelete_cell').addClass('disabled');
1142 1138 }
1143 1139
1144 1140 // Split/merge
1145 1141
1146 1142 /**
1147 1143 * Split the selected cell into two, at the cursor.
1148 1144 *
1149 1145 * @method split_cell
1150 1146 */
1151 1147 Notebook.prototype.split_cell = function () {
1152 1148 // Todo: implement spliting for other cell types.
1153 1149 var cell = this.get_selected_cell();
1154 1150 if (cell.is_splittable()) {
1155 1151 var texta = cell.get_pre_cursor();
1156 1152 var textb = cell.get_post_cursor();
1157 1153 if (cell instanceof IPython.CodeCell) {
1158 1154 cell.set_text(texta);
1159 1155 var new_cell = this.insert_cell_below('code');
1160 1156 new_cell.set_text(textb);
1161 1157 } else if (cell instanceof IPython.MarkdownCell) {
1162 1158 cell.set_text(texta);
1163 1159 cell.render();
1164 1160 var new_cell = this.insert_cell_below('markdown');
1165 1161 new_cell.edit(); // editor must be visible to call set_text
1166 1162 new_cell.set_text(textb);
1167 1163 new_cell.render();
1168 1164 }
1169 1165 };
1170 1166 };
1171 1167
1172 1168 /**
1173 1169 * Combine the selected cell into the cell above it.
1174 1170 *
1175 1171 * @method merge_cell_above
1176 1172 */
1177 1173 Notebook.prototype.merge_cell_above = function () {
1178 1174 var index = this.get_selected_index();
1179 1175 var cell = this.get_cell(index);
1180 1176 if (index > 0) {
1181 1177 var upper_cell = this.get_cell(index-1);
1182 1178 var upper_text = upper_cell.get_text();
1183 1179 var text = cell.get_text();
1184 1180 if (cell instanceof IPython.CodeCell) {
1185 1181 cell.set_text(upper_text+'\n'+text);
1186 1182 } else if (cell instanceof IPython.MarkdownCell) {
1187 1183 cell.edit();
1188 1184 cell.set_text(upper_text+'\n'+text);
1189 1185 cell.render();
1190 1186 };
1191 1187 this.delete_cell(index-1);
1192 1188 this.select(this.find_cell_index(cell));
1193 1189 };
1194 1190 };
1195 1191
1196 1192 /**
1197 1193 * Combine the selected cell into the cell below it.
1198 1194 *
1199 1195 * @method merge_cell_below
1200 1196 */
1201 1197 Notebook.prototype.merge_cell_below = function () {
1202 1198 var index = this.get_selected_index();
1203 1199 var cell = this.get_cell(index);
1204 1200 if (index < this.ncells()-1) {
1205 1201 var lower_cell = this.get_cell(index+1);
1206 1202 var lower_text = lower_cell.get_text();
1207 1203 var text = cell.get_text();
1208 1204 if (cell instanceof IPython.CodeCell) {
1209 1205 cell.set_text(text+'\n'+lower_text);
1210 1206 } else if (cell instanceof IPython.MarkdownCell) {
1211 1207 cell.edit();
1212 1208 cell.set_text(text+'\n'+lower_text);
1213 1209 cell.render();
1214 1210 };
1215 1211 this.delete_cell(index+1);
1216 1212 this.select(this.find_cell_index(cell));
1217 1213 };
1218 1214 };
1219 1215
1220 1216
1221 1217 // Cell collapsing and output clearing
1222 1218
1223 1219 /**
1224 1220 * Hide a cell's output.
1225 1221 *
1226 1222 * @method collapse
1227 1223 * @param {Number} index A cell's numeric index
1228 1224 */
1229 1225 Notebook.prototype.collapse = function (index) {
1230 1226 var i = this.index_or_selected(index);
1231 1227 this.get_cell(i).collapse();
1232 1228 this.set_dirty(true);
1233 1229 };
1234 1230
1235 1231 /**
1236 1232 * Show a cell's output.
1237 1233 *
1238 1234 * @method expand
1239 1235 * @param {Number} index A cell's numeric index
1240 1236 */
1241 1237 Notebook.prototype.expand = function (index) {
1242 1238 var i = this.index_or_selected(index);
1243 1239 this.get_cell(i).expand();
1244 1240 this.set_dirty(true);
1245 1241 };
1246 1242
1247 1243 /** Toggle whether a cell's output is collapsed or expanded.
1248 1244 *
1249 1245 * @method toggle_output
1250 1246 * @param {Number} index A cell's numeric index
1251 1247 */
1252 1248 Notebook.prototype.toggle_output = function (index) {
1253 1249 var i = this.index_or_selected(index);
1254 1250 this.get_cell(i).toggle_output();
1255 1251 this.set_dirty(true);
1256 1252 };
1257 1253
1258 1254 /**
1259 1255 * Toggle a scrollbar for long cell outputs.
1260 1256 *
1261 1257 * @method toggle_output_scroll
1262 1258 * @param {Number} index A cell's numeric index
1263 1259 */
1264 1260 Notebook.prototype.toggle_output_scroll = function (index) {
1265 1261 var i = this.index_or_selected(index);
1266 1262 this.get_cell(i).toggle_output_scroll();
1267 1263 };
1268 1264
1269 1265 /**
1270 1266 * Hide each code cell's output area.
1271 1267 *
1272 1268 * @method collapse_all_output
1273 1269 */
1274 1270 Notebook.prototype.collapse_all_output = function () {
1275 1271 var ncells = this.ncells();
1276 1272 var cells = this.get_cells();
1277 1273 for (var i=0; i<ncells; i++) {
1278 1274 if (cells[i] instanceof IPython.CodeCell) {
1279 1275 cells[i].output_area.collapse();
1280 1276 }
1281 1277 };
1282 1278 // this should not be set if the `collapse` key is removed from nbformat
1283 1279 this.set_dirty(true);
1284 1280 };
1285 1281
1286 1282 /**
1287 1283 * Expand each code cell's output area, and add a scrollbar for long output.
1288 1284 *
1289 1285 * @method scroll_all_output
1290 1286 */
1291 1287 Notebook.prototype.scroll_all_output = function () {
1292 1288 var ncells = this.ncells();
1293 1289 var cells = this.get_cells();
1294 1290 for (var i=0; i<ncells; i++) {
1295 1291 if (cells[i] instanceof IPython.CodeCell) {
1296 1292 cells[i].output_area.expand();
1297 1293 cells[i].output_area.scroll_if_long();
1298 1294 }
1299 1295 };
1300 1296 // this should not be set if the `collapse` key is removed from nbformat
1301 1297 this.set_dirty(true);
1302 1298 };
1303 1299
1304 1300 /**
1305 1301 * Expand each code cell's output area, and remove scrollbars.
1306 1302 *
1307 1303 * @method expand_all_output
1308 1304 */
1309 1305 Notebook.prototype.expand_all_output = function () {
1310 1306 var ncells = this.ncells();
1311 1307 var cells = this.get_cells();
1312 1308 for (var i=0; i<ncells; i++) {
1313 1309 if (cells[i] instanceof IPython.CodeCell) {
1314 1310 cells[i].output_area.expand();
1315 1311 cells[i].output_area.unscroll_area();
1316 1312 }
1317 1313 };
1318 1314 // this should not be set if the `collapse` key is removed from nbformat
1319 1315 this.set_dirty(true);
1320 1316 };
1321 1317
1322 1318 /**
1323 1319 * Clear each code cell's output area.
1324 1320 *
1325 1321 * @method clear_all_output
1326 1322 */
1327 1323 Notebook.prototype.clear_all_output = function () {
1328 1324 var ncells = this.ncells();
1329 1325 var cells = this.get_cells();
1330 1326 for (var i=0; i<ncells; i++) {
1331 1327 if (cells[i] instanceof IPython.CodeCell) {
1332 1328 cells[i].clear_output(true,true,true);
1333 1329 // Make all In[] prompts blank, as well
1334 1330 // TODO: make this configurable (via checkbox?)
1335 1331 cells[i].set_input_prompt();
1336 1332 }
1337 1333 };
1338 1334 this.set_dirty(true);
1339 1335 };
1340 1336
1341 1337
1342 1338 // Other cell functions: line numbers, ...
1343 1339
1344 1340 /**
1345 1341 * Toggle line numbers in the selected cell's input area.
1346 1342 *
1347 1343 * @method cell_toggle_line_numbers
1348 1344 */
1349 1345 Notebook.prototype.cell_toggle_line_numbers = function() {
1350 1346 this.get_selected_cell().toggle_line_numbers();
1351 1347 };
1352 1348
1353 1349 // Kernel related things
1354 1350
1355 1351 /**
1356 1352 * Start a new kernel and set it on each code cell.
1357 1353 *
1358 1354 * @method start_kernel
1359 1355 */
1360 1356 Notebook.prototype.start_kernel = function () {
1361 1357 var base_url = $('body').data('baseKernelUrl') + "kernels";
1362 1358 this.kernel = new IPython.Kernel(base_url);
1363 1359 this.kernel.start(this.notebook_id);
1364 1360 // Now that the kernel has been created, tell the CodeCells about it.
1365 1361 var ncells = this.ncells();
1366 1362 for (var i=0; i<ncells; i++) {
1367 1363 var cell = this.get_cell(i);
1368 1364 if (cell instanceof IPython.CodeCell) {
1369 1365 cell.set_kernel(this.kernel)
1370 1366 };
1371 1367 };
1372 1368 };
1373 1369
1374 1370 /**
1375 1371 * Prompt the user to restart the IPython kernel.
1376 1372 *
1377 1373 * @method restart_kernel
1378 1374 */
1379 1375 Notebook.prototype.restart_kernel = function () {
1380 1376 var that = this;
1381 1377 IPython.dialog.modal({
1382 1378 title : "Restart kernel or continue running?",
1383 1379 body : $("<p/>").html(
1384 1380 'Do you want to restart the current kernel? You will lose all variables defined in it.'
1385 1381 ),
1386 1382 buttons : {
1387 1383 "Continue running" : {},
1388 1384 "Restart" : {
1389 1385 "class" : "btn-danger",
1390 1386 "click" : function() {
1391 1387 that.kernel.restart();
1392 1388 }
1393 1389 }
1394 1390 }
1395 1391 });
1396 1392 };
1397 1393
1398 1394 /**
1399 1395 * Run the selected cell.
1400 1396 *
1401 1397 * Execute or render cell outputs.
1402 1398 *
1403 1399 * @method execute_selected_cell
1404 1400 * @param {Object} options Customize post-execution behavior
1405 1401 */
1406 1402 Notebook.prototype.execute_selected_cell = function (options) {
1407 1403 // add_new: should a new cell be added if we are at the end of the nb
1408 1404 // terminal: execute in terminal mode, which stays in the current cell
1409 1405 var default_options = {terminal: false, add_new: true};
1410 1406 $.extend(default_options, options);
1411 1407 var that = this;
1412 1408 var cell = that.get_selected_cell();
1413 1409 var cell_index = that.find_cell_index(cell);
1414 1410 if (cell instanceof IPython.CodeCell) {
1415 1411 cell.execute();
1416 1412 }
1417 1413 if (default_options.terminal) {
1418 1414 cell.select_all();
1419 1415 } else {
1420 1416 if ((cell_index === (that.ncells()-1)) && default_options.add_new) {
1421 1417 that.insert_cell_below('code');
1422 1418 // If we are adding a new cell at the end, scroll down to show it.
1423 1419 that.scroll_to_bottom();
1424 1420 } else {
1425 1421 that.select(cell_index+1);
1426 1422 };
1427 1423 };
1428 1424 this.set_dirty(true);
1429 1425 };
1430 1426
1431 1427 /**
1432 1428 * Execute all cells below the selected cell.
1433 1429 *
1434 1430 * @method execute_cells_below
1435 1431 */
1436 1432 Notebook.prototype.execute_cells_below = function () {
1437 1433 this.execute_cell_range(this.get_selected_index(), this.ncells());
1438 1434 this.scroll_to_bottom();
1439 1435 };
1440 1436
1441 1437 /**
1442 1438 * Execute all cells above the selected cell.
1443 1439 *
1444 1440 * @method execute_cells_above
1445 1441 */
1446 1442 Notebook.prototype.execute_cells_above = function () {
1447 1443 this.execute_cell_range(0, this.get_selected_index());
1448 1444 };
1449 1445
1450 1446 /**
1451 1447 * Execute all cells.
1452 1448 *
1453 1449 * @method execute_all_cells
1454 1450 */
1455 1451 Notebook.prototype.execute_all_cells = function () {
1456 1452 this.execute_cell_range(0, this.ncells());
1457 1453 this.scroll_to_bottom();
1458 1454 };
1459 1455
1460 1456 /**
1461 1457 * Execute a contiguous range of cells.
1462 1458 *
1463 1459 * @method execute_cell_range
1464 1460 * @param {Number} start Index of the first cell to execute (inclusive)
1465 1461 * @param {Number} end Index of the last cell to execute (exclusive)
1466 1462 */
1467 1463 Notebook.prototype.execute_cell_range = function (start, end) {
1468 1464 for (var i=start; i<end; i++) {
1469 1465 this.select(i);
1470 1466 this.execute_selected_cell({add_new:false});
1471 1467 };
1472 1468 };
1473 1469
1474 1470 // Persistance and loading
1475 1471
1476 1472 /**
1477 1473 * Getter method for this notebook's ID.
1478 1474 *
1479 1475 * @method get_notebook_id
1480 1476 * @return {String} This notebook's ID
1481 1477 */
1482 1478 Notebook.prototype.get_notebook_id = function () {
1483 1479 return this.notebook_id;
1484 1480 };
1485 1481
1486 1482 /**
1487 1483 * Getter method for this notebook's name.
1488 1484 *
1489 1485 * @method get_notebook_name
1490 1486 * @return {String} This notebook's name
1491 1487 */
1492 1488 Notebook.prototype.get_notebook_name = function () {
1493 1489 return this.notebook_name;
1494 1490 };
1495 1491
1496 1492 /**
1497 1493 * Setter method for this notebook's name.
1498 1494 *
1499 1495 * @method set_notebook_name
1500 1496 * @param {String} name A new name for this notebook
1501 1497 */
1502 1498 Notebook.prototype.set_notebook_name = function (name) {
1503 1499 this.notebook_name = name;
1504 1500 };
1505 1501
1506 1502 /**
1507 1503 * Check that a notebook's name is valid.
1508 1504 *
1509 1505 * @method test_notebook_name
1510 1506 * @param {String} nbname A name for this notebook
1511 1507 * @return {Boolean} True if the name is valid, false if invalid
1512 1508 */
1513 1509 Notebook.prototype.test_notebook_name = function (nbname) {
1514 1510 nbname = nbname || '';
1515 1511 if (this.notebook_name_blacklist_re.test(nbname) == false && nbname.length>0) {
1516 1512 return true;
1517 1513 } else {
1518 1514 return false;
1519 1515 };
1520 1516 };
1521 1517
1522 1518 /**
1523 1519 * Load a notebook from JSON (.ipynb).
1524 1520 *
1525 1521 * This currently handles one worksheet: others are deleted.
1526 1522 *
1527 1523 * @method fromJSON
1528 1524 * @param {Object} data JSON representation of a notebook
1529 1525 */
1530 1526 Notebook.prototype.fromJSON = function (data) {
1531 1527 var ncells = this.ncells();
1532 1528 var i;
1533 1529 for (i=0; i<ncells; i++) {
1534 1530 // Always delete cell 0 as they get renumbered as they are deleted.
1535 1531 this.delete_cell(0);
1536 1532 };
1537 1533 // Save the metadata and name.
1538 1534 this.metadata = data.metadata;
1539 1535 this.notebook_name = data.metadata.name;
1540 1536 // Only handle 1 worksheet for now.
1541 1537 var worksheet = data.worksheets[0];
1542 1538 if (worksheet !== undefined) {
1543 1539 if (worksheet.metadata) {
1544 1540 this.worksheet_metadata = worksheet.metadata;
1545 1541 }
1546 1542 var new_cells = worksheet.cells;
1547 1543 ncells = new_cells.length;
1548 1544 var cell_data = null;
1549 1545 var new_cell = null;
1550 1546 for (i=0; i<ncells; i++) {
1551 1547 cell_data = new_cells[i];
1552 1548 // VERSIONHACK: plaintext -> raw
1553 1549 // handle never-released plaintext name for raw cells
1554 1550 if (cell_data.cell_type === 'plaintext'){
1555 1551 cell_data.cell_type = 'raw';
1556 1552 }
1557 1553
1558 1554 new_cell = this.insert_cell_below(cell_data.cell_type);
1559 1555 new_cell.fromJSON(cell_data);
1560 1556 };
1561 1557 };
1562 1558 if (data.worksheets.length > 1) {
1563 1559 IPython.dialog.modal({
1564 1560 title : "Multiple worksheets",
1565 1561 body : "This notebook has " + data.worksheets.length + " worksheets, " +
1566 1562 "but this version of IPython can only handle the first. " +
1567 1563 "If you save this notebook, worksheets after the first will be lost.",
1568 1564 buttons : {
1569 1565 OK : {
1570 1566 class : "btn-danger"
1571 1567 }
1572 1568 }
1573 1569 });
1574 1570 }
1575 1571 };
1576 1572
1577 1573 /**
1578 1574 * Dump this notebook into a JSON-friendly object.
1579 1575 *
1580 1576 * @method toJSON
1581 1577 * @return {Object} A JSON-friendly representation of this notebook.
1582 1578 */
1583 1579 Notebook.prototype.toJSON = function () {
1584 1580 var cells = this.get_cells();
1585 1581 var ncells = cells.length;
1586 1582 var cell_array = new Array(ncells);
1587 1583 for (var i=0; i<ncells; i++) {
1588 1584 cell_array[i] = cells[i].toJSON();
1589 1585 };
1590 1586 var data = {
1591 1587 // Only handle 1 worksheet for now.
1592 1588 worksheets : [{
1593 1589 cells: cell_array,
1594 1590 metadata: this.worksheet_metadata
1595 1591 }],
1596 1592 metadata : this.metadata
1597 1593 };
1598 1594 return data;
1599 1595 };
1600 1596
1601 1597 /**
1602 1598 * Start an autosave timer, for periodically saving the notebook.
1603 1599 *
1604 1600 * @method set_autosave_interval
1605 1601 * @param {Integer} interval the autosave interval in milliseconds
1606 1602 */
1607 1603 Notebook.prototype.set_autosave_interval = function (interval) {
1608 1604 var that = this;
1609 1605 // clear previous interval, so we don't get simultaneous timers
1610 1606 if (this.autosave_timer) {
1611 1607 clearInterval(this.autosave_timer);
1612 1608 }
1613 1609
1614 1610 this.autosave_interval = this.minimum_autosave_interval = interval;
1615 1611 if (interval) {
1616 1612 this.autosave_timer = setInterval(function() {
1617 1613 if (that.dirty) {
1618 1614 that.save_notebook();
1619 1615 }
1620 1616 }, interval);
1621 1617 $([IPython.events]).trigger("autosave_enabled.Notebook", interval);
1622 1618 } else {
1623 1619 this.autosave_timer = null;
1624 1620 $([IPython.events]).trigger("autosave_disabled.Notebook");
1625 1621 };
1626 1622 };
1627 1623
1628 1624 /**
1629 1625 * Save this notebook on the server.
1630 1626 *
1631 1627 * @method save_notebook
1632 1628 */
1633 1629 Notebook.prototype.save_notebook = function () {
1634 1630 // We may want to move the name/id/nbformat logic inside toJSON?
1635 1631 var data = this.toJSON();
1636 1632 data.metadata.name = this.notebook_name;
1637 1633 data.nbformat = this.nbformat;
1638 1634 data.nbformat_minor = this.nbformat_minor;
1639 1635
1640 1636 // time the ajax call for autosave tuning purposes.
1641 1637 var start = new Date().getTime();
1642 1638
1643 1639 // We do the call with settings so we can set cache to false.
1644 1640 var settings = {
1645 1641 processData : false,
1646 1642 cache : false,
1647 1643 type : "PUT",
1648 1644 data : JSON.stringify(data),
1649 1645 headers : {'Content-Type': 'application/json'},
1650 1646 success : $.proxy(this.save_notebook_success, this, start),
1651 1647 error : $.proxy(this.save_notebook_error, this)
1652 1648 };
1653 1649 $([IPython.events]).trigger('notebook_saving.Notebook');
1654 1650 var url = this.baseProjectUrl() + 'notebooks/' + this.notebook_id;
1655 1651 $.ajax(url, settings);
1656 1652 };
1657 1653
1658 1654 /**
1659 1655 * Success callback for saving a notebook.
1660 1656 *
1661 1657 * @method save_notebook_success
1662 1658 * @param {Integer} start the time when the save request started
1663 1659 * @param {Object} data JSON representation of a notebook
1664 1660 * @param {String} status Description of response status
1665 1661 * @param {jqXHR} xhr jQuery Ajax object
1666 1662 */
1667 1663 Notebook.prototype.save_notebook_success = function (start, data, status, xhr) {
1668 1664 this.set_dirty(false);
1669 1665 $([IPython.events]).trigger('notebook_saved.Notebook');
1670 1666 this._update_autosave_interval(start);
1671 1667 if (this._checkpoint_after_save) {
1672 1668 this.create_checkpoint();
1673 1669 this._checkpoint_after_save = false;
1674 1670 };
1675 1671 };
1676 1672
1677 1673 /**
1678 1674 * update the autosave interval based on how long the last save took
1679 1675 *
1680 1676 * @method _update_autosave_interval
1681 1677 * @param {Integer} timestamp when the save request started
1682 1678 */
1683 1679 Notebook.prototype._update_autosave_interval = function (start) {
1684 1680 var duration = (new Date().getTime() - start);
1685 1681 if (this.autosave_interval) {
1686 1682 // new save interval: higher of 10x save duration or parameter (default 30 seconds)
1687 1683 var interval = Math.max(10 * duration, this.minimum_autosave_interval);
1688 1684 // round to 10 seconds, otherwise we will be setting a new interval too often
1689 1685 interval = 10000 * Math.round(interval / 10000);
1690 1686 // set new interval, if it's changed
1691 1687 if (interval != this.autosave_interval) {
1692 1688 this.set_autosave_interval(interval);
1693 1689 }
1694 1690 }
1695 1691 };
1696 1692
1697 1693 /**
1698 1694 * Failure callback for saving a notebook.
1699 1695 *
1700 1696 * @method save_notebook_error
1701 1697 * @param {jqXHR} xhr jQuery Ajax object
1702 1698 * @param {String} status Description of response status
1703 1699 * @param {String} error_msg HTTP error message
1704 1700 */
1705 1701 Notebook.prototype.save_notebook_error = function (xhr, status, error_msg) {
1706 1702 $([IPython.events]).trigger('notebook_save_failed.Notebook');
1707 1703 };
1708 1704
1709 1705 /**
1710 1706 * Request a notebook's data from the server.
1711 1707 *
1712 1708 * @method load_notebook
1713 1709 * @param {String} notebook_id A notebook to load
1714 1710 */
1715 1711 Notebook.prototype.load_notebook = function (notebook_id) {
1716 1712 var that = this;
1717 1713 this.notebook_id = notebook_id;
1718 1714 // We do the call with settings so we can set cache to false.
1719 1715 var settings = {
1720 1716 processData : false,
1721 1717 cache : false,
1722 1718 type : "GET",
1723 1719 dataType : "json",
1724 1720 success : $.proxy(this.load_notebook_success,this),
1725 1721 error : $.proxy(this.load_notebook_error,this),
1726 1722 };
1727 1723 $([IPython.events]).trigger('notebook_loading.Notebook');
1728 1724 var url = this.baseProjectUrl() + 'notebooks/' + this.notebook_id;
1729 1725 $.ajax(url, settings);
1730 1726 };
1731 1727
1732 1728 /**
1733 1729 * Success callback for loading a notebook from the server.
1734 1730 *
1735 1731 * Load notebook data from the JSON response.
1736 1732 *
1737 1733 * @method load_notebook_success
1738 1734 * @param {Object} data JSON representation of a notebook
1739 1735 * @param {String} status Description of response status
1740 1736 * @param {jqXHR} xhr jQuery Ajax object
1741 1737 */
1742 1738 Notebook.prototype.load_notebook_success = function (data, status, xhr) {
1743 1739 this.fromJSON(data);
1744 1740 if (this.ncells() === 0) {
1745 1741 this.insert_cell_below('code');
1746 1742 };
1747 1743 this.set_dirty(false);
1748 1744 this.select(0);
1749 1745 this.scroll_to_top();
1750 1746 if (data.orig_nbformat !== undefined && data.nbformat !== data.orig_nbformat) {
1751 1747 var msg = "This notebook has been converted from an older " +
1752 1748 "notebook format (v"+data.orig_nbformat+") to the current notebook " +
1753 1749 "format (v"+data.nbformat+"). The next time you save this notebook, the " +
1754 1750 "newer notebook format will be used and older versions of IPython " +
1755 1751 "may not be able to read it. To keep the older version, close the " +
1756 1752 "notebook without saving it.";
1757 1753 IPython.dialog.modal({
1758 1754 title : "Notebook converted",
1759 1755 body : msg,
1760 1756 buttons : {
1761 1757 OK : {
1762 1758 class : "btn-primary"
1763 1759 }
1764 1760 }
1765 1761 });
1766 1762 } else if (data.orig_nbformat_minor !== undefined && data.nbformat_minor !== data.orig_nbformat_minor) {
1767 1763 var that = this;
1768 1764 var orig_vs = 'v' + data.nbformat + '.' + data.orig_nbformat_minor;
1769 1765 var this_vs = 'v' + data.nbformat + '.' + this.nbformat_minor;
1770 1766 var msg = "This notebook is version " + orig_vs + ", but we only fully support up to " +
1771 1767 this_vs + ". You can still work with this notebook, but some features " +
1772 1768 "introduced in later notebook versions may not be available."
1773 1769
1774 1770 IPython.dialog.modal({
1775 1771 title : "Newer Notebook",
1776 1772 body : msg,
1777 1773 buttons : {
1778 1774 OK : {
1779 1775 class : "btn-danger"
1780 1776 }
1781 1777 }
1782 1778 });
1783 1779
1784 1780 }
1785 1781
1786 1782 // Create the kernel after the notebook is completely loaded to prevent
1787 1783 // code execution upon loading, which is a security risk.
1788 if (! this.read_only) {
1789 this.start_kernel();
1790 // load our checkpoint list
1791 IPython.notebook.list_checkpoints();
1792 }
1784 this.start_kernel();
1785 // load our checkpoint list
1786 IPython.notebook.list_checkpoints();
1787
1793 1788 $([IPython.events]).trigger('notebook_loaded.Notebook');
1794 1789 };
1795 1790
1796 1791 /**
1797 1792 * Failure callback for loading a notebook from the server.
1798 1793 *
1799 1794 * @method load_notebook_error
1800 1795 * @param {jqXHR} xhr jQuery Ajax object
1801 1796 * @param {String} textStatus Description of response status
1802 1797 * @param {String} errorThrow HTTP error message
1803 1798 */
1804 1799 Notebook.prototype.load_notebook_error = function (xhr, textStatus, errorThrow) {
1805 1800 if (xhr.status === 400) {
1806 1801 var msg = errorThrow;
1807 1802 } else if (xhr.status === 500) {
1808 1803 var msg = "An unknown error occurred while loading this notebook. " +
1809 1804 "This version can load notebook formats " +
1810 1805 "v" + this.nbformat + " or earlier.";
1811 1806 }
1812 1807 IPython.dialog.modal({
1813 1808 title: "Error loading notebook",
1814 1809 body : msg,
1815 1810 buttons : {
1816 1811 "OK": {}
1817 1812 }
1818 1813 });
1819 1814 }
1820 1815
1821 1816 /********************* checkpoint-related *********************/
1822 1817
1823 1818 /**
1824 1819 * Save the notebook then immediately create a checkpoint.
1825 1820 *
1826 1821 * @method save_checkpoint
1827 1822 */
1828 1823 Notebook.prototype.save_checkpoint = function () {
1829 1824 this._checkpoint_after_save = true;
1830 1825 this.save_notebook();
1831 1826 };
1832 1827
1833 1828 /**
1834 1829 * List checkpoints for this notebook.
1835 1830 *
1836 1831 * @method list_checkpoint
1837 1832 */
1838 1833 Notebook.prototype.list_checkpoints = function () {
1839 1834 var url = this.baseProjectUrl() + 'notebooks/' + this.notebook_id + '/checkpoints';
1840 1835 $.get(url).done(
1841 1836 $.proxy(this.list_checkpoints_success, this)
1842 1837 ).fail(
1843 1838 $.proxy(this.list_checkpoints_error, this)
1844 1839 );
1845 1840 };
1846 1841
1847 1842 /**
1848 1843 * Success callback for listing checkpoints.
1849 1844 *
1850 1845 * @method list_checkpoint_success
1851 1846 * @param {Object} data JSON representation of a checkpoint
1852 1847 * @param {String} status Description of response status
1853 1848 * @param {jqXHR} xhr jQuery Ajax object
1854 1849 */
1855 1850 Notebook.prototype.list_checkpoints_success = function (data, status, xhr) {
1856 1851 var data = $.parseJSON(data);
1857 1852 if (data.length) {
1858 1853 this.last_checkpoint = data[0];
1859 1854 } else {
1860 1855 this.last_checkpoint = null;
1861 1856 }
1862 1857 $([IPython.events]).trigger('checkpoints_listed.Notebook', [data]);
1863 1858 };
1864 1859
1865 1860 /**
1866 1861 * Failure callback for listing a checkpoint.
1867 1862 *
1868 1863 * @method list_checkpoint_error
1869 1864 * @param {jqXHR} xhr jQuery Ajax object
1870 1865 * @param {String} status Description of response status
1871 1866 * @param {String} error_msg HTTP error message
1872 1867 */
1873 1868 Notebook.prototype.list_checkpoints_error = function (xhr, status, error_msg) {
1874 1869 $([IPython.events]).trigger('list_checkpoints_failed.Notebook');
1875 1870 };
1876 1871
1877 1872 /**
1878 1873 * Create a checkpoint of this notebook on the server from the most recent save.
1879 1874 *
1880 1875 * @method create_checkpoint
1881 1876 */
1882 1877 Notebook.prototype.create_checkpoint = function () {
1883 1878 var url = this.baseProjectUrl() + 'notebooks/' + this.notebook_id + '/checkpoints';
1884 1879 $.post(url).done(
1885 1880 $.proxy(this.create_checkpoint_success, this)
1886 1881 ).fail(
1887 1882 $.proxy(this.create_checkpoint_error, this)
1888 1883 );
1889 1884 };
1890 1885
1891 1886 /**
1892 1887 * Success callback for creating a checkpoint.
1893 1888 *
1894 1889 * @method create_checkpoint_success
1895 1890 * @param {Object} data JSON representation of a checkpoint
1896 1891 * @param {String} status Description of response status
1897 1892 * @param {jqXHR} xhr jQuery Ajax object
1898 1893 */
1899 1894 Notebook.prototype.create_checkpoint_success = function (data, status, xhr) {
1900 1895 var data = $.parseJSON(data);
1901 1896 this.last_checkpoint = data;
1902 1897 $([IPython.events]).trigger('checkpoint_created.Notebook', data);
1903 1898 };
1904 1899
1905 1900 /**
1906 1901 * Failure callback for creating a checkpoint.
1907 1902 *
1908 1903 * @method create_checkpoint_error
1909 1904 * @param {jqXHR} xhr jQuery Ajax object
1910 1905 * @param {String} status Description of response status
1911 1906 * @param {String} error_msg HTTP error message
1912 1907 */
1913 1908 Notebook.prototype.create_checkpoint_error = function (xhr, status, error_msg) {
1914 1909 $([IPython.events]).trigger('checkpoint_failed.Notebook');
1915 1910 };
1916 1911
1917 1912 Notebook.prototype.restore_checkpoint_dialog = function (checkpoint) {
1918 1913 var that = this;
1919 1914 var checkpoint = checkpoint || this.last_checkpoint;
1920 1915 if ( ! checkpoint ) {
1921 1916 console.log("restore dialog, but no checkpoint to restore to!");
1922 1917 return;
1923 1918 }
1924 1919 var body = $('<div/>').append(
1925 1920 $('<p/>').addClass("p-space").text(
1926 1921 "Are you sure you want to revert the notebook to " +
1927 1922 "the latest checkpoint?"
1928 1923 ).append(
1929 1924 $("<strong/>").text(
1930 1925 " This cannot be undone."
1931 1926 )
1932 1927 )
1933 1928 ).append(
1934 1929 $('<p/>').addClass("p-space").text("The checkpoint was last updated at:")
1935 1930 ).append(
1936 1931 $('<p/>').addClass("p-space").text(
1937 1932 Date(checkpoint.last_modified)
1938 1933 ).css("text-align", "center")
1939 1934 );
1940 1935
1941 1936 IPython.dialog.modal({
1942 1937 title : "Revert notebook to checkpoint",
1943 1938 body : body,
1944 1939 buttons : {
1945 1940 Revert : {
1946 1941 class : "btn-danger",
1947 1942 click : function () {
1948 1943 that.restore_checkpoint(checkpoint.checkpoint_id);
1949 1944 }
1950 1945 },
1951 1946 Cancel : {}
1952 1947 }
1953 1948 });
1954 1949 }
1955 1950
1956 1951 /**
1957 1952 * Restore the notebook to a checkpoint state.
1958 1953 *
1959 1954 * @method restore_checkpoint
1960 1955 * @param {String} checkpoint ID
1961 1956 */
1962 1957 Notebook.prototype.restore_checkpoint = function (checkpoint) {
1963 1958 $([IPython.events]).trigger('checkpoint_restoring.Notebook', checkpoint);
1964 1959 var url = this.baseProjectUrl() + 'notebooks/' + this.notebook_id + '/checkpoints/' + checkpoint;
1965 1960 $.post(url).done(
1966 1961 $.proxy(this.restore_checkpoint_success, this)
1967 1962 ).fail(
1968 1963 $.proxy(this.restore_checkpoint_error, this)
1969 1964 );
1970 1965 };
1971 1966
1972 1967 /**
1973 1968 * Success callback for restoring a notebook to a checkpoint.
1974 1969 *
1975 1970 * @method restore_checkpoint_success
1976 1971 * @param {Object} data (ignored, should be empty)
1977 1972 * @param {String} status Description of response status
1978 1973 * @param {jqXHR} xhr jQuery Ajax object
1979 1974 */
1980 1975 Notebook.prototype.restore_checkpoint_success = function (data, status, xhr) {
1981 1976 $([IPython.events]).trigger('checkpoint_restored.Notebook');
1982 1977 this.load_notebook(this.notebook_id);
1983 1978 };
1984 1979
1985 1980 /**
1986 1981 * Failure callback for restoring a notebook to a checkpoint.
1987 1982 *
1988 1983 * @method restore_checkpoint_error
1989 1984 * @param {jqXHR} xhr jQuery Ajax object
1990 1985 * @param {String} status Description of response status
1991 1986 * @param {String} error_msg HTTP error message
1992 1987 */
1993 1988 Notebook.prototype.restore_checkpoint_error = function (xhr, status, error_msg) {
1994 1989 $([IPython.events]).trigger('checkpoint_restore_failed.Notebook');
1995 1990 };
1996 1991
1997 1992 /**
1998 1993 * Delete a notebook checkpoint.
1999 1994 *
2000 1995 * @method delete_checkpoint
2001 1996 * @param {String} checkpoint ID
2002 1997 */
2003 1998 Notebook.prototype.delete_checkpoint = function (checkpoint) {
2004 1999 $([IPython.events]).trigger('checkpoint_deleting.Notebook', checkpoint);
2005 2000 var url = this.baseProjectUrl() + 'notebooks/' + this.notebook_id + '/checkpoints/' + checkpoint;
2006 2001 $.ajax(url, {
2007 2002 type: 'DELETE',
2008 2003 success: $.proxy(this.delete_checkpoint_success, this),
2009 2004 error: $.proxy(this.delete_notebook_error,this)
2010 2005 });
2011 2006 };
2012 2007
2013 2008 /**
2014 2009 * Success callback for deleting a notebook checkpoint
2015 2010 *
2016 2011 * @method delete_checkpoint_success
2017 2012 * @param {Object} data (ignored, should be empty)
2018 2013 * @param {String} status Description of response status
2019 2014 * @param {jqXHR} xhr jQuery Ajax object
2020 2015 */
2021 2016 Notebook.prototype.delete_checkpoint_success = function (data, status, xhr) {
2022 2017 $([IPython.events]).trigger('checkpoint_deleted.Notebook', data);
2023 2018 this.load_notebook(this.notebook_id);
2024 2019 };
2025 2020
2026 2021 /**
2027 2022 * Failure callback for deleting a notebook checkpoint.
2028 2023 *
2029 2024 * @method delete_checkpoint_error
2030 2025 * @param {jqXHR} xhr jQuery Ajax object
2031 2026 * @param {String} status Description of response status
2032 2027 * @param {String} error_msg HTTP error message
2033 2028 */
2034 2029 Notebook.prototype.delete_checkpoint_error = function (xhr, status, error_msg) {
2035 2030 $([IPython.events]).trigger('checkpoint_delete_failed.Notebook');
2036 2031 };
2037 2032
2038 2033
2039 2034 IPython.Notebook = Notebook;
2040 2035
2041 2036
2042 2037 return IPython;
2043 2038
2044 2039 }(IPython));
2045 2040
@@ -1,557 +1,556 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2008-2012 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // TextCell
10 10 //============================================================================
11 11
12 12
13 13
14 14 /**
15 15 A module that allow to create different type of Text Cell
16 16 @module IPython
17 17 @namespace IPython
18 18 */
19 19 var IPython = (function (IPython) {
20 20 "use strict";
21 21
22 22 // TextCell base class
23 23 var key = IPython.utils.keycodes;
24 24
25 25 /**
26 26 * Construct a new TextCell, codemirror mode is by default 'htmlmixed', and cell type is 'text'
27 27 * cell start as not redered.
28 28 *
29 29 * @class TextCell
30 30 * @constructor TextCell
31 31 * @extend IPython.Cell
32 32 * @param {object|undefined} [options]
33 33 * @param [options.cm_config] {object} config to pass to CodeMirror, will extend/overwrite default config
34 34 * @param [options.placeholder] {string} default string to use when souce in empty for rendering (only use in some TextCell subclass)
35 35 */
36 36 var TextCell = function (options) {
37 37 // in all TextCell/Cell subclasses
38 38 // do not assign most of members here, just pass it down
39 39 // in the options dict potentially overwriting what you wish.
40 40 // they will be assigned in the base class.
41 41
42 42 // we cannot put this as a class key as it has handle to "this".
43 43 var cm_overwrite_options = {
44 44 onKeyEvent: $.proxy(this.handle_codemirror_keyevent,this)
45 45 };
46 46
47 47 options = this.mergeopt(TextCell,options,{cm_config:cm_overwrite_options});
48 48
49 49 IPython.Cell.apply(this, [options]);
50 50
51 51
52 52 this.rendered = false;
53 53 this.cell_type = this.cell_type || 'text';
54 54 };
55 55
56 56 TextCell.prototype = new IPython.Cell();
57 57
58 58 TextCell.options_default = {
59 59 cm_config : {
60 60 extraKeys: {"Tab": "indentMore","Shift-Tab" : "indentLess"},
61 61 mode: 'htmlmixed',
62 62 lineWrapping : true,
63 63 }
64 64 };
65 65
66 66
67 67
68 68 /**
69 69 * Create the DOM element of the TextCell
70 70 * @method create_element
71 71 * @private
72 72 */
73 73 TextCell.prototype.create_element = function () {
74 74 IPython.Cell.prototype.create_element.apply(this, arguments);
75 75 var cell = $("<div>").addClass('cell text_cell border-box-sizing');
76 76 cell.attr('tabindex','2');
77 77
78 78 this.celltoolbar = new IPython.CellToolbar(this);
79 79 cell.append(this.celltoolbar.element);
80 80
81 81 var input_area = $('<div/>').addClass('text_cell_input border-box-sizing');
82 82 this.code_mirror = CodeMirror(input_area.get(0), this.cm_config);
83 83
84 84 // The tabindex=-1 makes this div focusable.
85 85 var render_area = $('<div/>').addClass('text_cell_render border-box-sizing').
86 86 addClass('rendered_html').attr('tabindex','-1');
87 87 cell.append(input_area).append(render_area);
88 88 this.element = cell;
89 89 };
90 90
91 91
92 92 /**
93 93 * Bind the DOM evet to cell actions
94 94 * Need to be called after TextCell.create_element
95 95 * @private
96 96 * @method bind_event
97 97 */
98 98 TextCell.prototype.bind_events = function () {
99 99 IPython.Cell.prototype.bind_events.apply(this);
100 100 var that = this;
101 101 this.element.keydown(function (event) {
102 102 if (event.which === 13 && !event.shiftKey) {
103 103 if (that.rendered) {
104 104 that.edit();
105 105 return false;
106 106 };
107 107 };
108 108 });
109 109 this.element.dblclick(function () {
110 110 that.edit();
111 111 });
112 112 };
113 113
114 114 /**
115 115 * This method gets called in CodeMirror's onKeyDown/onKeyPress
116 116 * handlers and is used to provide custom key handling.
117 117 *
118 118 * Subclass should override this method to have custom handeling
119 119 *
120 120 * @method handle_codemirror_keyevent
121 121 * @param {CodeMirror} editor - The codemirror instance bound to the cell
122 122 * @param {event} event -
123 123 * @return {Boolean} `true` if CodeMirror should ignore the event, `false` Otherwise
124 124 */
125 125 TextCell.prototype.handle_codemirror_keyevent = function (editor, event) {
126 126
127 127 if (event.keyCode === 13 && (event.shiftKey || event.ctrlKey)) {
128 128 // Always ignore shift-enter in CodeMirror as we handle it.
129 129 return true;
130 130 }
131 131 return false;
132 132 };
133 133
134 134 /**
135 135 * Select the current cell and trigger 'focus'
136 136 * @method select
137 137 */
138 138 TextCell.prototype.select = function () {
139 139 IPython.Cell.prototype.select.apply(this);
140 140 var output = this.element.find("div.text_cell_render");
141 141 output.trigger('focus');
142 142 };
143 143
144 144 /**
145 145 * unselect the current cell and `render` it
146 146 * @method unselect
147 147 */
148 148 TextCell.prototype.unselect = function() {
149 149 // render on selection of another cell
150 150 this.render();
151 151 IPython.Cell.prototype.unselect.apply(this);
152 152 };
153 153
154 154 /**
155 155 *
156 156 * put the current cell in edition mode
157 157 * @method edit
158 158 */
159 159 TextCell.prototype.edit = function () {
160 if ( this.read_only ) return;
161 160 if (this.rendered === true) {
162 161 var text_cell = this.element;
163 162 var output = text_cell.find("div.text_cell_render");
164 163 output.hide();
165 164 text_cell.find('div.text_cell_input').show();
166 165 this.code_mirror.refresh();
167 166 this.code_mirror.focus();
168 167 // We used to need an additional refresh() after the focus, but
169 168 // it appears that this has been fixed in CM. This bug would show
170 169 // up on FF when a newly loaded markdown cell was edited.
171 170 this.rendered = false;
172 171 if (this.get_text() === this.placeholder) {
173 172 this.set_text('');
174 173 this.refresh();
175 174 }
176 175 }
177 176 };
178 177
179 178
180 179 /**
181 180 * Empty, Subclasses must define render.
182 181 * @method render
183 182 */
184 183 TextCell.prototype.render = function () {};
185 184
186 185
187 186 /**
188 187 * setter: {{#crossLink "TextCell/set_text"}}{{/crossLink}}
189 188 * @method get_text
190 189 * @retrun {string} CodeMirror current text value
191 190 */
192 191 TextCell.prototype.get_text = function() {
193 192 return this.code_mirror.getValue();
194 193 };
195 194
196 195 /**
197 196 * @param {string} text - Codemiror text value
198 197 * @see TextCell#get_text
199 198 * @method set_text
200 199 * */
201 200 TextCell.prototype.set_text = function(text) {
202 201 this.code_mirror.setValue(text);
203 202 this.code_mirror.refresh();
204 203 };
205 204
206 205 /**
207 206 * setter :{{#crossLink "TextCell/set_rendered"}}{{/crossLink}}
208 207 * @method get_rendered
209 208 * @return {html} html of rendered element
210 209 * */
211 210 TextCell.prototype.get_rendered = function() {
212 211 return this.element.find('div.text_cell_render').html();
213 212 };
214 213
215 214 /**
216 215 * @method set_rendered
217 216 */
218 217 TextCell.prototype.set_rendered = function(text) {
219 218 this.element.find('div.text_cell_render').html(text);
220 219 };
221 220
222 221 /**
223 222 * not deprecated, but implementation wrong
224 223 * @method at_top
225 224 * @deprecated
226 225 * @return {Boolean} true is cell rendered, false otherwise
227 226 * I doubt this is what it is supposed to do
228 227 * this implementation is completly false
229 228 */
230 229 TextCell.prototype.at_top = function () {
231 230 if (this.rendered) {
232 231 return true;
233 232 } else {
234 233 return false;
235 234 }
236 235 };
237 236
238 237
239 238 /**
240 239 * not deprecated, but implementation wrong
241 240 * @method at_bottom
242 241 * @deprecated
243 242 * @return {Boolean} true is cell rendered, false otherwise
244 243 * I doubt this is what it is supposed to do
245 244 * this implementation is completly false
246 245 * */
247 246 TextCell.prototype.at_bottom = function () {
248 247 if (this.rendered) {
249 248 return true;
250 249 } else {
251 250 return false;
252 251 }
253 252 };
254 253
255 254 /**
256 255 * Create Text cell from JSON
257 256 * @param {json} data - JSON serialized text-cell
258 257 * @method fromJSON
259 258 */
260 259 TextCell.prototype.fromJSON = function (data) {
261 260 IPython.Cell.prototype.fromJSON.apply(this, arguments);
262 261 if (data.cell_type === this.cell_type) {
263 262 if (data.source !== undefined) {
264 263 this.set_text(data.source);
265 264 // make this value the starting point, so that we can only undo
266 265 // to this state, instead of a blank cell
267 266 this.code_mirror.clearHistory();
268 267 this.set_rendered(data.rendered || '');
269 268 this.rendered = false;
270 269 this.render();
271 270 }
272 271 }
273 272 };
274 273
275 274 /** Generate JSON from cell
276 275 * @return {object} cell data serialised to json
277 276 */
278 277 TextCell.prototype.toJSON = function () {
279 278 var data = IPython.Cell.prototype.toJSON.apply(this);
280 279 data.cell_type = this.cell_type;
281 280 data.source = this.get_text();
282 281 return data;
283 282 };
284 283
285 284
286 285 /**
287 286 * @class MarkdownCell
288 287 * @constructor MarkdownCell
289 288 * @extends IPython.HTMLCell
290 289 */
291 290 var MarkdownCell = function (options) {
292 291 var options = options || {};
293 292
294 293 options = this.mergeopt(MarkdownCell,options);
295 294 TextCell.apply(this, [options]);
296 295
297 296 this.cell_type = 'markdown';
298 297 };
299 298
300 299 MarkdownCell.options_default = {
301 300 cm_config: {
302 301 mode: 'gfm'
303 302 },
304 303 placeholder: "Type *Markdown* and LaTeX: $\\alpha^2$"
305 304 }
306 305
307 306
308 307
309 308
310 309 MarkdownCell.prototype = new TextCell();
311 310
312 311 /**
313 312 * @method render
314 313 */
315 314 MarkdownCell.prototype.render = function () {
316 315 if (this.rendered === false) {
317 316 var text = this.get_text();
318 317 if (text === "") { text = this.placeholder; }
319 318 var text_math = IPython.mathjaxutils.remove_math(text);
320 319 var text = text_math[0];
321 320 var math = text_math[1];
322 321 var html = marked.parser(marked.lexer(text));
323 322 html = $(IPython.mathjaxutils.replace_math(html, math));
324 323 // links in markdown cells should open in new tabs
325 324 html.find("a[href]").attr("target", "_blank");
326 325 try {
327 326 this.set_rendered(html);
328 327 } catch (e) {
329 328 console.log("Error running Javascript in Markdown:");
330 329 console.log(e);
331 330 this.set_rendered($("<div/>").addClass("js-error").html(
332 331 "Error rendering Markdown!<br/>" + e.toString())
333 332 );
334 333 }
335 334 this.element.find('div.text_cell_input').hide();
336 335 this.element.find("div.text_cell_render").show();
337 336 this.typeset()
338 337 this.rendered = true;
339 338 }
340 339 };
341 340
342 341
343 342 // RawCell
344 343
345 344 /**
346 345 * @class RawCell
347 346 * @constructor RawCell
348 347 * @extends IPython.TextCell
349 348 */
350 349 var RawCell = function (options) {
351 350
352 351 options = this.mergeopt(RawCell,options)
353 352 TextCell.apply(this, [options]);
354 353
355 354 this.cell_type = 'raw';
356 355
357 356 var that = this
358 357 this.element.focusout(
359 358 function() { that.auto_highlight(); }
360 359 );
361 360 };
362 361
363 362 RawCell.options_default = {
364 363 placeholder : "Type plain text and LaTeX: $\\alpha^2$"
365 364 };
366 365
367 366
368 367
369 368 RawCell.prototype = new TextCell();
370 369
371 370 /**
372 371 * Trigger autodetection of highlight scheme for current cell
373 372 * @method auto_highlight
374 373 */
375 374 RawCell.prototype.auto_highlight = function () {
376 375 this._auto_highlight(IPython.config.raw_cell_highlight);
377 376 };
378 377
379 378 /** @method render **/
380 379 RawCell.prototype.render = function () {
381 380 this.rendered = true;
382 381 this.edit();
383 382 };
384 383
385 384
386 385 /** @method handle_codemirror_keyevent **/
387 386 RawCell.prototype.handle_codemirror_keyevent = function (editor, event) {
388 387
389 388 var that = this;
390 389 if (event.which === key.UPARROW && event.type === 'keydown') {
391 390 // If we are not at the top, let CM handle the up arrow and
392 391 // prevent the global keydown handler from handling it.
393 392 if (!that.at_top()) {
394 393 event.stop();
395 394 return false;
396 395 } else {
397 396 return true;
398 397 };
399 398 } else if (event.which === key.DOWNARROW && event.type === 'keydown') {
400 399 // If we are not at the bottom, let CM handle the down arrow and
401 400 // prevent the global keydown handler from handling it.
402 401 if (!that.at_bottom()) {
403 402 event.stop();
404 403 return false;
405 404 } else {
406 405 return true;
407 406 };
408 407 };
409 408 return false;
410 409 };
411 410
412 411 /** @method select **/
413 412 RawCell.prototype.select = function () {
414 413 IPython.Cell.prototype.select.apply(this);
415 414 this.code_mirror.refresh();
416 415 this.code_mirror.focus();
417 416 };
418 417
419 418 /** @method at_top **/
420 419 RawCell.prototype.at_top = function () {
421 420 var cursor = this.code_mirror.getCursor();
422 421 if (cursor.line === 0 && cursor.ch === 0) {
423 422 return true;
424 423 } else {
425 424 return false;
426 425 }
427 426 };
428 427
429 428
430 429 /** @method at_bottom **/
431 430 RawCell.prototype.at_bottom = function () {
432 431 var cursor = this.code_mirror.getCursor();
433 432 if (cursor.line === (this.code_mirror.lineCount()-1) && cursor.ch === this.code_mirror.getLine(cursor.line).length) {
434 433 return true;
435 434 } else {
436 435 return false;
437 436 }
438 437 };
439 438
440 439
441 440 /**
442 441 * @class HeadingCell
443 442 * @extends IPython.TextCell
444 443 */
445 444
446 445 /**
447 446 * @constructor HeadingCell
448 447 * @extends IPython.TextCell
449 448 */
450 449 var HeadingCell = function (options) {
451 450
452 451 options = this.mergeopt(HeadingCell,options)
453 452 TextCell.apply(this, [options]);
454 453
455 454 /**
456 455 * heading level of the cell, use getter and setter to access
457 456 * @property level
458 457 */
459 458 this.level = 1;
460 459 this.cell_type = 'heading';
461 460 };
462 461
463 462 HeadingCell.options_default = {
464 463 placeholder: "Type Heading Here"
465 464 };
466 465
467 466 HeadingCell.prototype = new TextCell();
468 467
469 468 /** @method fromJSON */
470 469 HeadingCell.prototype.fromJSON = function (data) {
471 470 if (data.level != undefined){
472 471 this.level = data.level;
473 472 }
474 473 TextCell.prototype.fromJSON.apply(this, arguments);
475 474 };
476 475
477 476
478 477 /** @method toJSON */
479 478 HeadingCell.prototype.toJSON = function () {
480 479 var data = TextCell.prototype.toJSON.apply(this);
481 480 data.level = this.get_level();
482 481 return data;
483 482 };
484 483
485 484
486 485 /**
487 486 * Change heading level of cell, and re-render
488 487 * @method set_level
489 488 */
490 489 HeadingCell.prototype.set_level = function (level) {
491 490 this.level = level;
492 491 if (this.rendered) {
493 492 this.rendered = false;
494 493 this.render();
495 494 };
496 495 };
497 496
498 497 /** The depth of header cell, based on html (h1 to h6)
499 498 * @method get_level
500 499 * @return {integer} level - for 1 to 6
501 500 */
502 501 HeadingCell.prototype.get_level = function () {
503 502 return this.level;
504 503 };
505 504
506 505
507 506 HeadingCell.prototype.set_rendered = function (html) {
508 507 this.element.find("div.text_cell_render").html(html);
509 508 };
510 509
511 510
512 511 HeadingCell.prototype.get_rendered = function () {
513 512 var r = this.element.find("div.text_cell_render");
514 513 return r.children().first().html();
515 514 };
516 515
517 516
518 517 HeadingCell.prototype.render = function () {
519 518 if (this.rendered === false) {
520 519 var text = this.get_text();
521 520 // Markdown headings must be a single line
522 521 text = text.replace(/\n/g, ' ');
523 522 if (text === "") { text = this.placeholder; }
524 523 text = Array(this.level + 1).join("#") + " " + text;
525 524 var text_and_math = IPython.mathjaxutils.remove_math(text);
526 525 var text = text_and_math[0];
527 526 var math = text_and_math[1];
528 527 var html = marked.parser(marked.lexer(text));
529 528 var h = $(IPython.mathjaxutils.replace_math(html, math));
530 529 // add id and linkback anchor
531 530 var hash = h.text().replace(/ /g, '-');
532 531 h.attr('id', hash);
533 532 h.append(
534 533 $('<a/>')
535 534 .addClass('anchor-link')
536 535 .attr('href', '#' + hash)
537 536 .text('ΒΆ')
538 537 );
539 538
540 539 this.set_rendered(h);
541 540 this.typeset();
542 541 this.element.find('div.text_cell_input').hide();
543 542 this.element.find("div.text_cell_render").show();
544 543 this.rendered = true;
545 544 };
546 545 };
547 546
548 547 IPython.TextCell = TextCell;
549 548 IPython.MarkdownCell = MarkdownCell;
550 549 IPython.RawCell = RawCell;
551 550 IPython.HeadingCell = HeadingCell;
552 551
553 552
554 553 return IPython;
555 554
556 555 }(IPython));
557 556
@@ -1,85 +1,84 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2008-2011 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // On document ready
10 10 //============================================================================
11 11
12 12
13 13 $(document).ready(function () {
14 14
15 15 IPython.page = new IPython.Page();
16 16 $('#new_notebook').click(function (e) {
17 17 window.open($('body').data('baseProjectUrl')+'new');
18 18 });
19 19
20 IPython.read_only = $('body').data('readOnly') === 'True';
21 20 IPython.notebook_list = new IPython.NotebookList('#notebook_list');
22 21 IPython.cluster_list = new IPython.ClusterList('#cluster_list');
23 22 IPython.login_widget = new IPython.LoginWidget('#login_widget');
24 23
25 24 var interval_id=0;
26 25 // auto refresh every xx secondes, no need to be fast,
27 26 // update is done at least when page get focus
28 27 var time_refresh = 60; // in sec
29 28
30 29 var enable_autorefresh = function(){
31 30 //refresh immediately , then start interval
32 31 if($('.upload_button').length == 0)
33 32 {
34 33 IPython.notebook_list.load_list();
35 34 IPython.cluster_list.load_list();
36 35 }
37 36 if (!interval_id){
38 37 interval_id = setInterval(function(){
39 38 if($('.upload_button').length == 0)
40 39 {
41 40 IPython.notebook_list.load_list();
42 41 IPython.cluster_list.load_list();
43 42 }
44 43 }, time_refresh*1000);
45 44 }
46 45 }
47 46
48 47 var disable_autorefresh = function(){
49 48 clearInterval(interval_id);
50 49 interval_id = 0;
51 50 }
52 51
53 52 // stop autorefresh when page lose focus
54 53 $(window).blur(function() {
55 54 disable_autorefresh();
56 55 })
57 56
58 57 //re-enable when page get focus back
59 58 $(window).focus(function() {
60 59 enable_autorefresh();
61 60 });
62 61
63 62 // finally start it, it will refresh immediately
64 63 enable_autorefresh();
65 64
66 65 IPython.page.show();
67 66
68 67 // bound the upload method to the on change of the file select list
69 68 $("#alternate_upload").change(function (event){
70 69 IPython.notebook_list.handelFilesUpload(event,'form');
71 70 });
72 71
73 72 // set hash on tab click
74 73 $("#tabs").find("a").click(function() {
75 74 window.location.hash = $(this).attr("href");
76 75 })
77 76
78 77 // load tab if url hash
79 78 if (window.location.hash) {
80 79 $("#tabs").find("a[href=" + window.location.hash + "]").click();
81 80 }
82 81
83 82
84 83 });
85 84
@@ -1,305 +1,300 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2008-2011 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // NotebookList
10 10 //============================================================================
11 11
12 12 var IPython = (function (IPython) {
13 13
14 14 var NotebookList = function (selector) {
15 15 this.selector = selector;
16 16 if (this.selector !== undefined) {
17 17 this.element = $(selector);
18 18 this.style();
19 19 this.bind_events();
20 20 }
21 21 };
22 22
23 23 NotebookList.prototype.baseProjectUrl = function () {
24 24 return $('body').data('baseProjectUrl')
25 25 };
26 26
27 27 NotebookList.prototype.style = function () {
28 28 $('#notebook_toolbar').addClass('list_toolbar');
29 29 $('#drag_info').addClass('toolbar_info');
30 30 $('#notebook_buttons').addClass('toolbar_buttons');
31 31 $('#notebook_list_header').addClass('list_header');
32 32 this.element.addClass("list_container");
33 33 };
34 34
35 35
36 36 NotebookList.prototype.bind_events = function () {
37 if (IPython.read_only){
38 return;
39 }
40 37 var that = this;
41 38 $('#refresh_notebook_list').click(function () {
42 39 that.load_list();
43 40 });
44 41 this.element.bind('dragover', function () {
45 42 return false;
46 43 });
47 44 this.element.bind('drop', function(event){
48 45 that.handelFilesUpload(event,'drop');
49 46 return false;
50 47 });
51 48 };
52 49
53 50 NotebookList.prototype.handelFilesUpload = function(event, dropOrForm) {
54 51 var that = this;
55 52 var files;
56 53 if(dropOrForm =='drop'){
57 54 files = event.originalEvent.dataTransfer.files;
58 55 } else
59 56 {
60 57 files = event.originalEvent.target.files
61 58 }
62 59 for (var i = 0, f; f = files[i]; i++) {
63 60 var reader = new FileReader();
64 61 reader.readAsText(f);
65 62 var fname = f.name.split('.');
66 63 var nbname = fname.slice(0,-1).join('.');
67 64 var nbformat = fname.slice(-1)[0];
68 65 if (nbformat === 'ipynb') {nbformat = 'json';};
69 66 if (nbformat === 'py' || nbformat === 'json') {
70 67 var item = that.new_notebook_item(0);
71 68 that.add_name_input(nbname, item);
72 69 item.data('nbformat', nbformat);
73 70 // Store the notebook item in the reader so we can use it later
74 71 // to know which item it belongs to.
75 72 $(reader).data('item', item);
76 73 reader.onload = function (event) {
77 74 var nbitem = $(event.target).data('item');
78 75 that.add_notebook_data(event.target.result, nbitem);
79 76 that.add_upload_button(nbitem);
80 77 };
81 78 };
82 79 }
83 80 return false;
84 81 };
85 82
86 83 NotebookList.prototype.clear_list = function () {
87 84 this.element.children('.list_item').remove();
88 85 };
89 86
90 87
91 88 NotebookList.prototype.load_list = function () {
92 89 var that = this;
93 90 var settings = {
94 91 processData : false,
95 92 cache : false,
96 93 type : "GET",
97 94 dataType : "json",
98 95 success : $.proxy(this.list_loaded, this),
99 96 error : $.proxy( function(){
100 97 that.list_loaded([], null, null, {msg:"Error connecting to server."});
101 98 },this)
102 99 };
103 100
104 101 var url = this.baseProjectUrl() + 'notebooks';
105 102 $.ajax(url, settings);
106 103 };
107 104
108 105
109 106 NotebookList.prototype.list_loaded = function (data, status, xhr, param) {
110 107 var message = 'Notebook list empty.';
111 108 if (param !== undefined && param.msg) {
112 109 var message = param.msg;
113 110 }
114 111 var len = data.length;
115 112 this.clear_list();
116 113
117 114 if(len == 0)
118 115 {
119 116 $(this.new_notebook_item(0))
120 117 .append(
121 118 $('<div style="margin:auto;text-align:center;color:grey"/>')
122 119 .text(message)
123 120 )
124 121 }
125 122
126 123 for (var i=0; i<len; i++) {
127 124 var notebook_id = data[i].notebook_id;
128 125 var nbname = data[i].name;
129 126 var kernel = data[i].kernel_id;
130 127 var item = this.new_notebook_item(i);
131 128 this.add_link(notebook_id, nbname, item);
132 if (!IPython.read_only){
133 // hide delete buttons when readonly
134 if(kernel == null){
135 this.add_delete_button(item);
136 } else {
137 this.add_shutdown_button(item,kernel);
138 }
129 // hide delete buttons when readonly
130 if(kernel == null){
131 this.add_delete_button(item);
132 } else {
133 this.add_shutdown_button(item,kernel);
139 134 }
140 135 };
141 136 };
142 137
143 138
144 139 NotebookList.prototype.new_notebook_item = function (index) {
145 140 var item = $('<div/>').addClass("list_item").addClass("row-fluid");
146 141 // item.addClass('list_item ui-widget ui-widget-content ui-helper-clearfix');
147 142 // item.css('border-top-style','none');
148 143 item.append($("<div/>").addClass("span12").append(
149 144 $("<a/>").addClass("item_link").append(
150 145 $("<span/>").addClass("item_name")
151 146 )
152 147 ).append(
153 148 $('<div/>').addClass("item_buttons btn-group pull-right")
154 149 ));
155 150
156 151 if (index === -1) {
157 152 this.element.append(item);
158 153 } else {
159 154 this.element.children().eq(index).after(item);
160 155 }
161 156 return item;
162 157 };
163 158
164 159
165 160 NotebookList.prototype.add_link = function (notebook_id, nbname, item) {
166 161 item.data('nbname', nbname);
167 162 item.data('notebook_id', notebook_id);
168 163 item.find(".item_name").text(nbname);
169 164 item.find("a.item_link")
170 165 .attr('href', this.baseProjectUrl()+notebook_id)
171 166 .attr('target','_blank');
172 167 };
173 168
174 169
175 170 NotebookList.prototype.add_name_input = function (nbname, item) {
176 171 item.data('nbname', nbname);
177 172 item.find(".item_name").empty().append(
178 173 $('<input/>')
179 174 .addClass("nbname_input")
180 175 .attr('value', nbname)
181 176 .attr('size', '30')
182 177 .attr('type', 'text')
183 178 );
184 179 };
185 180
186 181
187 182 NotebookList.prototype.add_notebook_data = function (data, item) {
188 183 item.data('nbdata',data);
189 184 };
190 185
191 186
192 187 NotebookList.prototype.add_shutdown_button = function (item, kernel) {
193 188 var that = this;
194 189 var shutdown_button = $("<button/>").text("Shutdown").addClass("btn btn-mini").
195 190 click(function (e) {
196 191 var settings = {
197 192 processData : false,
198 193 cache : false,
199 194 type : "DELETE",
200 195 dataType : "json",
201 196 success : function (data, status, xhr) {
202 197 that.load_list();
203 198 }
204 199 };
205 200 var url = that.baseProjectUrl() + 'kernels/'+kernel;
206 201 $.ajax(url, settings);
207 202 return false;
208 203 });
209 204 // var new_buttons = item.find('a'); // shutdown_button;
210 205 item.find(".item_buttons").html("").append(shutdown_button);
211 206 };
212 207
213 208 NotebookList.prototype.add_delete_button = function (item) {
214 209 var new_buttons = $('<span/>').addClass("btn-group pull-right");
215 210 var notebooklist = this;
216 211 var delete_button = $("<button/>").text("Delete").addClass("btn btn-mini").
217 212 click(function (e) {
218 213 // $(this) is the button that was clicked.
219 214 var that = $(this);
220 215 // We use the nbname and notebook_id from the parent notebook_item element's
221 216 // data because the outer scopes values change as we iterate through the loop.
222 217 var parent_item = that.parents('div.list_item');
223 218 var nbname = parent_item.data('nbname');
224 219 var notebook_id = parent_item.data('notebook_id');
225 220 var message = 'Are you sure you want to permanently delete the notebook: ' + nbname + '?';
226 221 IPython.dialog.modal({
227 222 title : "Delete notebook",
228 223 body : message,
229 224 buttons : {
230 225 Delete : {
231 226 class: "btn-danger",
232 227 click: function() {
233 228 var settings = {
234 229 processData : false,
235 230 cache : false,
236 231 type : "DELETE",
237 232 dataType : "json",
238 233 success : function (data, status, xhr) {
239 234 parent_item.remove();
240 235 }
241 236 };
242 237 var url = notebooklist.baseProjectUrl() + 'notebooks/' + notebook_id;
243 238 $.ajax(url, settings);
244 239 }
245 240 },
246 241 Cancel : {}
247 242 }
248 243 });
249 244 return false;
250 245 });
251 246 item.find(".item_buttons").html("").append(delete_button);
252 247 };
253 248
254 249
255 250 NotebookList.prototype.add_upload_button = function (item) {
256 251 var that = this;
257 252 var upload_button = $('<button/>').text("Upload")
258 253 .addClass('btn btn-primary btn-mini upload_button')
259 254 .click(function (e) {
260 255 var nbname = item.find('.item_name > input').attr('value');
261 256 var nbformat = item.data('nbformat');
262 257 var nbdata = item.data('nbdata');
263 258 var content_type = 'text/plain';
264 259 if (nbformat === 'json') {
265 260 content_type = 'application/json';
266 261 } else if (nbformat === 'py') {
267 262 content_type = 'application/x-python';
268 263 };
269 264 var settings = {
270 265 processData : false,
271 266 cache : false,
272 267 type : 'POST',
273 268 dataType : 'json',
274 269 data : nbdata,
275 270 headers : {'Content-Type': content_type},
276 271 success : function (data, status, xhr) {
277 272 that.add_link(data, nbname, item);
278 273 that.add_delete_button(item);
279 274 }
280 275 };
281 276
282 277 var qs = $.param({name:nbname, format:nbformat});
283 278 var url = that.baseProjectUrl() + 'notebooks?' + qs;
284 279 $.ajax(url, settings);
285 280 return false;
286 281 });
287 282 var cancel_button = $('<button/>').text("Cancel")
288 283 .addClass("btn btn-mini")
289 284 .click(function (e) {
290 285 console.log('cancel click');
291 286 item.remove();
292 287 return false;
293 288 });
294 289 item.find(".item_buttons").empty()
295 290 .append(upload_button)
296 291 .append(cancel_button);
297 292 };
298 293
299 294
300 295 IPython.NotebookList = NotebookList;
301 296
302 297 return IPython;
303 298
304 299 }(IPython));
305 300
@@ -1,38 +1,38 b''
1 1 {% extends "page.html" %}
2 2
3 3 {% block stylesheet %}
4 4 {{super()}}
5 5 <link rel="stylesheet" href="{{ static_url("auth/css/override.css") }}" type="text/css" />
6 6 {% endblock %}
7 7
8 8 {% block login_widget %}
9 9 {% endblock %}
10 10
11 11 {% block site %}
12 12
13 13 <div id="ipython-main-app" class="container">
14 14
15 15 {% if message %}
16 16 {% for key in message %}
17 17 <div class="message {{key}}">
18 18 {{message[key]}}
19 19 </div>
20 20 {% endfor %}
21 21 {% endif %}
22 22
23 {% if read_only or not login_available %}
23 {% if not login_available %}
24 24 Proceed to the <a href="{{base_project_url}}">dashboard</a>.
25 25 {% else %}
26 26 Proceed to the <a href="{{base_project_url}}login">login page</a>.
27 27 {% endif %}
28 28
29 29
30 30 <div/>
31 31
32 32 {% endblock %}
33 33
34 34 {% block script %}
35 35
36 36 <script src="{{static_url("auth/js/logoutmain.js") }}" type="text/javascript" charset="utf-8"></script>
37 37
38 38 {% endblock %}
@@ -1,258 +1,257 b''
1 1 {% extends "page.html" %}
2 2
3 3 {% block stylesheet %}
4 4
5 5 {% if mathjax_url %}
6 6 <script type="text/javascript" src="{{mathjax_url}}?config=TeX-AMS_HTML-full&delayStartupUntil=configured" charset="utf-8"></script>
7 7 {% endif %}
8 8 <script type="text/javascript">
9 9 // MathJax disabled, set as null to distingish from *missing* MathJax,
10 10 // where it will be undefined, and should prompt a dialog later.
11 11 window.mathjax_url = "{{mathjax_url}}";
12 12 </script>
13 13
14 14 <link rel="stylesheet" href="{{ static_url("components/codemirror/lib/codemirror.css") }}">
15 15
16 16 {{super()}}
17 17
18 18 <link rel="stylesheet" href="{{ static_url("notebook/css/override.css") }}" type="text/css" />
19 19
20 20 {% endblock %}
21 21
22 22 {% block params %}
23 23
24 24 data-project={{project}}
25 25 data-base-project-url={{base_project_url}}
26 26 data-base-kernel-url={{base_kernel_url}}
27 data-read-only={{read_only and not logged_in}}
28 27 data-notebook-id={{notebook_id}}
29 28 class="notebook_app"
30 29
31 30 {% endblock %}
32 31
33 32
34 33 {% block header %}
35 34
36 35 <span id="save_widget" class="nav pull-left">
37 36 <span id="notebook_name"></span>
38 37 <span id="checkpoint_status"></span>
39 38 <span id="autosave_status"></span>
40 39 </span>
41 40
42 41 {% endblock %}
43 42
44 43
45 44 {% block site %}
46 45
47 46 <div id="menubar-container" class="container">
48 47 <div id="menubar">
49 48 <div class="navbar">
50 49 <div class="navbar-inner">
51 50 <div class="container">
52 51 <ul id="menus" class="nav">
53 52 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">File</a>
54 53 <ul class="dropdown-menu">
55 54 <li id="new_notebook"><a href="#">New</a></li>
56 55 <li id="open_notebook"><a href="#">Open...</a></li>
57 56 <!-- <hr/> -->
58 57 <li class="divider"></li>
59 58 <li id="copy_notebook"><a href="#">Make a Copy...</a></li>
60 59 <li id="rename_notebook"><a href="#">Rename...</a></li>
61 60 <li id="save_checkpoint"><a href="#">Save and Checkpoint</a></li>
62 61 <!-- <hr/> -->
63 62 <li class="divider"></li>
64 63 <li id="restore_checkpoint" class="dropdown-submenu"><a href="#">Revert to Checkpoint</a>
65 64 <ul class="dropdown-menu">
66 65 <li><a href="#"></a></li>
67 66 <li><a href="#"></a></li>
68 67 <li><a href="#"></a></li>
69 68 <li><a href="#"></a></li>
70 69 <li><a href="#"></a></li>
71 70 </ul>
72 71 </li>
73 72 <li class="divider"></li>
74 73 <li class="dropdown-submenu"><a href="#">Download as</a>
75 74 <ul class="dropdown-menu">
76 75 <li id="download_ipynb"><a href="#">IPython (.ipynb)</a></li>
77 76 <li id="download_py"><a href="#">Python (.py)</a></li>
78 77 </ul>
79 78 </li>
80 79 <li class="divider"></li>
81 80
82 81 <li id="kill_and_exit"><a href="#" >Close and halt</a></li>
83 82 </ul>
84 83 </li>
85 84 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Edit</a>
86 85 <ul class="dropdown-menu">
87 86 <li id="cut_cell"><a href="#">Cut Cell</a></li>
88 87 <li id="copy_cell"><a href="#">Copy Cell</a></li>
89 88 <li id="paste_cell_above" class="disabled"><a href="#">Paste Cell Above</a></li>
90 89 <li id="paste_cell_below" class="disabled"><a href="#">Paste Cell Below</a></li>
91 90 <li id="paste_cell_replace" class="disabled"><a href="#">Paste Cell &amp; Replace</a></li>
92 91 <li id="delete_cell"><a href="#">Delete Cell</a></li>
93 92 <li id="undelete_cell" class="disabled"><a href="#">Undo Delete Cell</a></li>
94 93 <li class="divider"></li>
95 94 <li id="split_cell"><a href="#">Split Cell</a></li>
96 95 <li id="merge_cell_above"><a href="#">Merge Cell Above</a></li>
97 96 <li id="merge_cell_below"><a href="#">Merge Cell Below</a></li>
98 97 <li class="divider"></li>
99 98 <li id="move_cell_up"><a href="#">Move Cell Up</a></li>
100 99 <li id="move_cell_down"><a href="#">Move Cell Down</a></li>
101 100 <li class="divider"></li>
102 101 <li id="select_previous"><a href="#">Select Previous Cell</a></li>
103 102 <li id="select_next"><a href="#">Select Next Cell</a></li>
104 103 </ul>
105 104 </li>
106 105 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">View</a>
107 106 <ul class="dropdown-menu">
108 107 <li id="toggle_header"><a href="#">Toggle Header</a></li>
109 108 <li id="toggle_toolbar"><a href="#">Toggle Toolbar</a></li>
110 109 </ul>
111 110 </li>
112 111 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Insert</a>
113 112 <ul class="dropdown-menu">
114 113 <li id="insert_cell_above"><a href="#">Insert Cell Above</a></li>
115 114 <li id="insert_cell_below"><a href="#">Insert Cell Below</a></li>
116 115 </ul>
117 116 </li>
118 117 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Cell</a>
119 118 <ul class="dropdown-menu">
120 119 <li id="run_cell"><a href="#">Run</a></li>
121 120 <li id="run_cell_in_place"><a href="#">Run in Place</a></li>
122 121 <li id="run_all_cells"><a href="#">Run All</a></li>
123 122 <li id="run_all_cells_above"><a href="#">Run All Above</a></li>
124 123 <li id="run_all_cells_below"><a href="#">Run All Below</a></li>
125 124 <li class="divider"></li>
126 125 <li id="change_cell_type" class="dropdown-submenu"><a href="#">Cell Type</a>
127 126 <ul class="dropdown-menu">
128 127 <li id="to_code"><a href="#">Code</a></li>
129 128 <li id="to_markdown"><a href="#">Markdown </a></li>
130 129 <li id="to_raw"><a href="#">Raw Text</a></li>
131 130 <li id="to_heading1"><a href="#">Heading 1</a></li>
132 131 <li id="to_heading2"><a href="#">Heading 2</a></li>
133 132 <li id="to_heading3"><a href="#">Heading 3</a></li>
134 133 <li id="to_heading4"><a href="#">Heading 4</a></li>
135 134 <li id="to_heading5"><a href="#">Heading 5</a></li>
136 135 <li id="to_heading6"><a href="#">Heading 6</a></li>
137 136 </ul>
138 137 </li>
139 138 <li class="divider"></li>
140 139 <li id="toggle_output"><a href="#">Toggle Current Output</a></li>
141 140 <li id="all_outputs" class="dropdown-submenu"><a href="#">All Output</a>
142 141 <ul class="dropdown-menu">
143 142 <li id="expand_all_output"><a href="#">Expand</a></li>
144 143 <li id="scroll_all_output"><a href="#">Scroll Long</a></li>
145 144 <li id="collapse_all_output"><a href="#">Collapse</a></li>
146 145 <li id="clear_all_output"><a href="#">Clear</a></li>
147 146 </ul>
148 147 </li>
149 148 </ul>
150 149 </li>
151 150 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Kernel</a>
152 151 <ul class="dropdown-menu">
153 152 <li id="int_kernel"><a href="#">Interrupt</a></li>
154 153 <li id="restart_kernel"><a href="#">Restart</a></li>
155 154 </ul>
156 155 </li>
157 156 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Help</a>
158 157 <ul class="dropdown-menu">
159 158 <li><a href="http://ipython.org/documentation.html" target="_blank">IPython Help</a></li>
160 159 <li><a href="http://ipython.org/ipython-doc/stable/interactive/htmlnotebook.html" target="_blank">Notebook Help</a></li>
161 160 <li id="keyboard_shortcuts"><a href="#">Keyboard Shortcuts</a></li>
162 161 <li class="divider"></li>
163 162 <li><a href="http://docs.python.org" target="_blank">Python</a></li>
164 163 <li><a href="http://docs.scipy.org/doc/numpy/reference/" target="_blank">NumPy</a></li>
165 164 <li><a href="http://docs.scipy.org/doc/scipy/reference/" target="_blank">SciPy</a></li>
166 165 <li><a href="http://docs.sympy.org/dev/index.html" target="_blank">SymPy</a></li>
167 166 <li><a href="http://matplotlib.sourceforge.net/" target="_blank">Matplotlib</a></li>
168 167 </ul>
169 168 </li>
170 169 </ul>
171 170 <div id="notification_area"></div>
172 171 </div>
173 172 </div>
174 173 </div>
175 174 </div>
176 175 <div id="maintoolbar" class="navbar">
177 176 <div class="toolbar-inner navbar-inner navbar-nobg">
178 177 <div id="maintoolbar-container" class="container"></div>
179 178 </div>
180 179 </div>
181 180 </div>
182 181
183 182 <div id="ipython-main-app">
184 183
185 184 <div id="notebook_panel">
186 185 <div id="notebook"></div>
187 186 <div id="pager_splitter"></div>
188 187 <div id="pager">
189 188 <div id='pager_button_area'>
190 189 </div>
191 190 <div id="pager-container" class="container"></div>
192 191 </div>
193 192 </div>
194 193
195 194 </div>
196 195 <div id='tooltip' class='ipython_tooltip' style='display:none'></div>
197 196
198 197
199 198 {% endblock %}
200 199
201 200
202 201 {% block script %}
203 202
204 203 {{super()}}
205 204
206 205 <script src="{{ static_url("components/codemirror/lib/codemirror.js") }}" charset="utf-8"></script>
207 206 <script type="text/javascript">
208 207 CodeMirror.modeURL = "{{ static_url("components/codemirror/mode/%N/%N.js") }}";
209 208 </script>
210 209 <script src="{{ static_url("components/codemirror/addon/mode/loadmode.js") }}" charset="utf-8"></script>
211 210 <script src="{{ static_url("components/codemirror/addon/mode/multiplex.js") }}" charset="utf-8"></script>
212 211 <script src="{{ static_url("components/codemirror/addon/mode/overlay.js") }}" charset="utf-8"></script>
213 212 <script src="{{ static_url("components/codemirror/addon/edit/matchbrackets.js") }}" charset="utf-8"></script>
214 213 <script src="{{ static_url("components/codemirror/addon/comment/comment.js") }}" charset="utf-8"></script>
215 214 <script src="{{ static_url("notebook/js/codemirror-ipython.js") }}" charset="utf-8"></script>
216 215 <script src="{{ static_url("components/codemirror/mode/htmlmixed/htmlmixed.js") }}" charset="utf-8"></script>
217 216 <script src="{{ static_url("components/codemirror/mode/xml/xml.js") }}" charset="utf-8"></script>
218 217 <script src="{{ static_url("components/codemirror/mode/javascript/javascript.js") }}" charset="utf-8"></script>
219 218 <script src="{{ static_url("components/codemirror/mode/css/css.js") }}" charset="utf-8"></script>
220 219 <script src="{{ static_url("components/codemirror/mode/rst/rst.js") }}" charset="utf-8"></script>
221 220 <script src="{{ static_url("components/codemirror/mode/markdown/markdown.js") }}" charset="utf-8"></script>
222 221 <script src="{{ static_url("components/codemirror/mode/gfm/gfm.js") }}" charset="utf-8"></script>
223 222
224 223 <script src="{{ static_url("components/highlight.js/build/highlight.pack.js") }}" charset="utf-8"></script>
225 224
226 225 <script src="{{ static_url("dateformat/date.format.js") }}" charset="utf-8"></script>
227 226
228 227 <script src="{{ static_url("base/js/events.js") }}" type="text/javascript" charset="utf-8"></script>
229 228 <script src="{{ static_url("base/js/utils.js") }}" type="text/javascript" charset="utf-8"></script>
230 229 <script src="{{ static_url("base/js/dialog.js") }}" type="text/javascript" charset="utf-8"></script>
231 230 <script src="{{ static_url("notebook/js/layoutmanager.js") }}" type="text/javascript" charset="utf-8"></script>
232 231 <script src="{{ static_url("notebook/js/mathjaxutils.js") }}" type="text/javascript" charset="utf-8"></script>
233 232 <script src="{{ static_url("notebook/js/outputarea.js") }}" type="text/javascript" charset="utf-8"></script>
234 233 <script src="{{ static_url("notebook/js/cell.js") }}" type="text/javascript" charset="utf-8"></script>
235 234 <script src="{{ static_url("notebook/js/celltoolbar.js") }}" type="text/javascript" charset="utf-8"></script>
236 235 <script src="{{ static_url("notebook/js/codecell.js") }}" type="text/javascript" charset="utf-8"></script>
237 236 <script src="{{ static_url("notebook/js/completer.js") }}" type="text/javascript" charset="utf-8"></script>
238 237 <script src="{{ static_url("notebook/js/textcell.js") }}" type="text/javascript" charset="utf-8"></script>
239 238 <script src="{{ static_url("services/kernels/js/kernel.js") }}" type="text/javascript" charset="utf-8"></script>
240 239 <script src="{{ static_url("notebook/js/savewidget.js") }}" type="text/javascript" charset="utf-8"></script>
241 240 <script src="{{ static_url("notebook/js/quickhelp.js") }}" type="text/javascript" charset="utf-8"></script>
242 241 <script src="{{ static_url("notebook/js/pager.js") }}" type="text/javascript" charset="utf-8"></script>
243 242 <script src="{{ static_url("notebook/js/menubar.js") }}" type="text/javascript" charset="utf-8"></script>
244 243 <script src="{{ static_url("notebook/js/toolbar.js") }}" type="text/javascript" charset="utf-8"></script>
245 244 <script src="{{ static_url("notebook/js/maintoolbar.js") }}" type="text/javascript" charset="utf-8"></script>
246 245 <script src="{{ static_url("notebook/js/notebook.js") }}" type="text/javascript" charset="utf-8"></script>
247 246 <script src="{{ static_url("notebook/js/notificationwidget.js") }}" type="text/javascript" charset="utf-8"></script>
248 247 <script src="{{ static_url("notebook/js/notificationarea.js") }}" type="text/javascript" charset="utf-8"></script>
249 248 <script src="{{ static_url("notebook/js/tooltip.js") }}" type="text/javascript" charset="utf-8"></script>
250 249 <script src="{{ static_url("notebook/js/config.js") }}" type="text/javascript" charset="utf-8"></script>
251 250 <script src="{{ static_url("notebook/js/main.js") }}" type="text/javascript" charset="utf-8"></script>
252 251
253 252 <script src="{{ static_url("notebook/js/contexthint.js") }}" charset="utf-8"></script>
254 253
255 254 <script src="{{ static_url("notebook/js/celltoolbarpresets/default.js") }}" type="text/javascript" charset="utf-8"></script>
256 255 <script src="{{ static_url("notebook/js/celltoolbarpresets/slideshow.js") }}" type="text/javascript" charset="utf-8"></script>
257 256
258 257 {% endblock %}
@@ -1,92 +1,91 b''
1 1 {% extends "page.html" %}
2 2
3 3 {% block title %}IPython Dashboard{% endblock %}
4 4
5 5
6 6 {% block stylesheet %}
7 7 {{super()}}
8 8 <link rel="stylesheet" href="{{ static_url("tree/css/override.css") }}" type="text/css" />
9 9 {% endblock %}
10 10
11 11 {% block params %}
12 12
13 13 data-project={{project}}
14 14 data-base-project-url={{base_project_url}}
15 15 data-base-kernel-url={{base_kernel_url}}
16 data-read-only={{read_only}}
17 16
18 17 {% endblock %}
19 18
20 19
21 20 {% block site %}
22 21
23 22 <div id="ipython-main-app" class="container">
24 23
25 24 <div id="tabs" class="tabbable">
26 25 <ul class="nav nav-tabs" id="tabs">
27 26 <li class="active"><a href="#notebooks" data-toggle="tab">Notebooks</a></li>
28 27 <li><a href="#clusters" data-toggle="tab">Clusters</a></li>
29 28 </ul>
30 29
31 30 <div class="tab-content">
32 31 <div id="notebooks" class="tab-pane active">
33 {% if logged_in or not read_only %}
32 {% if logged_in %}
34 33 <div id="notebook_toolbar">
35 34 <form id='alternate_upload' class='alternate_upload' >
36 35 <span id="drag_info" style="position:absolute" >
37 36 To import a notebook, drag the file onto the listing below or <strong>click here</strong>.
38 37 </span>
39 38 <input type="file" name="datafile" class="fileinput" multiple='multiple'>
40 39 </form>
41 40 <span id="notebook_buttons">
42 41 <button id="refresh_notebook_list" title="Refresh notebook list" class="btn btn-small">Refresh</button>
43 42 <button id="new_notebook" title="Create new notebook" class="btn btn-small">New Notebook</button>
44 43 </span>
45 44 </div>
46 45 {% endif %}
47 46
48 47 <div id="notebook_list">
49 48 <div id="notebook_list_header" class="row-fluid list_header">
50 49 <div id="project_name">
51 50 <ul class="breadcrumb">
52 51 {% for component in project_component %}
53 52 <li>{{component}} <span>/</span></li>
54 53 {% endfor %}
55 54 </ul>
56 55 </div>
57 56 </div>
58 57 </div>
59 58 </div>
60 59
61 60 <div id="clusters" class="tab-pane">
62 61
63 62 <div id="cluster_toolbar">
64 63 <span id="cluster_list_info">IPython parallel computing clusters</span>
65 64
66 65 <span id="cluster_buttons">
67 66 <button id="refresh_cluster_list" title="Refresh cluster list" class="btn btn-small">Refresh</button>
68 67 </span>
69 68 </div>
70 69
71 70 <div id="cluster_list">
72 71 <div id="cluster_list_header" class="row-fluid list_header">
73 72 <span class="profile_col span4">profile</span>
74 73 <span class="status_col span3">status</span>
75 74 <span class="engines_col span3" title="Enter the number of engines to start or empty for default"># of engines</span>
76 75 <span class="action_col span2">action</span>
77 76 </div>
78 77 </div>
79 78 </div>
80 79 </div>
81 80
82 81 </div>
83 82
84 83 {% endblock %}
85 84
86 85 {% block script %}
87 86 {{super()}}
88 87 <script src="{{static_url("base/js/dialog.js") }}" type="text/javascript" charset="utf-8"></script>
89 88 <script src="{{static_url("tree/js/notebooklist.js") }}" type="text/javascript" charset="utf-8"></script>
90 89 <script src="{{static_url("tree/js/clusterlist.js") }}" type="text/javascript" charset="utf-8"></script>
91 90 <script src="{{static_url("tree/js/main.js") }}" type="text/javascript" charset="utf-8"></script>
92 91 {% endblock %}
@@ -1,41 +1,42 b''
1 1 """Tornado handlers for the tree view.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2011 The IPython Development Team
10 10 #
11 11 # Distributed under the terms of the BSD License. The full license is in
12 12 # the file COPYING, distributed as part of this software.
13 13 #-----------------------------------------------------------------------------
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18
19 from ..base.handlers import IPythonHandler, authenticate_unless_readonly
19 from tornado import web
20 from ..base.handlers import IPythonHandler
20 21
21 22 #-----------------------------------------------------------------------------
22 23 # Handlers
23 24 #-----------------------------------------------------------------------------
24 25
25 26
26 27 class ProjectDashboardHandler(IPythonHandler):
27 28
28 @authenticate_unless_readonly
29 @web.authenticated
29 30 def get(self):
30 31 self.write(self.render_template('tree.html',
31 32 project=self.project,
32 33 project_component=self.project.split('/'),
33 34 ))
34 35
35 36
36 37 #-----------------------------------------------------------------------------
37 38 # URL to handler mappings
38 39 #-----------------------------------------------------------------------------
39 40
40 41
41 42 default_handlers = [(r"/", ProjectDashboardHandler)] No newline at end of file
General Comments 0
You need to be logged in to leave comments. Login now