##// END OF EJS Templates
Merge pull request #5043 from minrk/base-url-unicode...
Brian E. Granger -
r15245:aa3ba1f8 merge
parent child Browse files
Show More
@@ -1,62 +1,62 b''
1 """Tornado handlers logging into the notebook.
1 """Tornado handlers logging into the notebook.
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2011 The IPython Development Team
9 # Copyright (C) 2011 The IPython Development Team
10 #
10 #
11 # Distributed under the terms of the BSD License. The full license is in
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19 import uuid
19 import uuid
20
20
21 from tornado.escape import url_escape
21 from tornado.escape import url_escape
22
22
23 from IPython.lib.security import passwd_check
23 from IPython.lib.security import passwd_check
24
24
25 from ..base.handlers import IPythonHandler
25 from ..base.handlers import IPythonHandler
26
26
27 #-----------------------------------------------------------------------------
27 #-----------------------------------------------------------------------------
28 # Handler
28 # Handler
29 #-----------------------------------------------------------------------------
29 #-----------------------------------------------------------------------------
30
30
31 class LoginHandler(IPythonHandler):
31 class LoginHandler(IPythonHandler):
32
32
33 def _render(self, message=None):
33 def _render(self, message=None):
34 self.write(self.render_template('login.html',
34 self.write(self.render_template('login.html',
35 next=url_escape(self.get_argument('next', default=self.base_project_url)),
35 next=url_escape(self.get_argument('next', default=self.base_url)),
36 message=message,
36 message=message,
37 ))
37 ))
38
38
39 def get(self):
39 def get(self):
40 if self.current_user:
40 if self.current_user:
41 self.redirect(self.get_argument('next', default=self.base_project_url))
41 self.redirect(self.get_argument('next', default=self.base_url))
42 else:
42 else:
43 self._render()
43 self._render()
44
44
45 def post(self):
45 def post(self):
46 pwd = self.get_argument('password', default=u'')
46 pwd = self.get_argument('password', default=u'')
47 if self.login_available:
47 if self.login_available:
48 if passwd_check(self.password, pwd):
48 if passwd_check(self.password, pwd):
49 self.set_secure_cookie(self.cookie_name, str(uuid.uuid4()))
49 self.set_secure_cookie(self.cookie_name, str(uuid.uuid4()))
50 else:
50 else:
51 self._render(message={'error': 'Invalid password'})
51 self._render(message={'error': 'Invalid password'})
52 return
52 return
53
53
54 self.redirect(self.get_argument('next', default=self.base_project_url))
54 self.redirect(self.get_argument('next', default=self.base_url))
55
55
56
56
57 #-----------------------------------------------------------------------------
57 #-----------------------------------------------------------------------------
58 # URL to handler mappings
58 # URL to handler mappings
59 #-----------------------------------------------------------------------------
59 #-----------------------------------------------------------------------------
60
60
61
61
62 default_handlers = [(r"/login", LoginHandler)]
62 default_handlers = [(r"/login", LoginHandler)]
@@ -1,387 +1,387 b''
1 """Base Tornado handlers for the notebook.
1 """Base Tornado handlers for the notebook.
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2011 The IPython Development Team
9 # Copyright (C) 2011 The IPython Development Team
10 #
10 #
11 # Distributed under the terms of the BSD License. The full license is in
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19
19
20 import functools
20 import functools
21 import json
21 import json
22 import logging
22 import logging
23 import os
23 import os
24 import re
24 import re
25 import sys
25 import sys
26 import traceback
26 import traceback
27 try:
27 try:
28 # py3
28 # py3
29 from http.client import responses
29 from http.client import responses
30 except ImportError:
30 except ImportError:
31 from httplib import responses
31 from httplib import responses
32
32
33 from jinja2 import TemplateNotFound
33 from jinja2 import TemplateNotFound
34 from tornado import web
34 from tornado import web
35
35
36 try:
36 try:
37 from tornado.log import app_log
37 from tornado.log import app_log
38 except ImportError:
38 except ImportError:
39 app_log = logging.getLogger()
39 app_log = logging.getLogger()
40
40
41 from IPython.config import Application
41 from IPython.config import Application
42 from IPython.utils.path import filefind
42 from IPython.utils.path import filefind
43 from IPython.utils.py3compat import string_types
43 from IPython.utils.py3compat import string_types
44 from IPython.html.utils import is_hidden
44 from IPython.html.utils import is_hidden
45
45
46 #-----------------------------------------------------------------------------
46 #-----------------------------------------------------------------------------
47 # Top-level handlers
47 # Top-level handlers
48 #-----------------------------------------------------------------------------
48 #-----------------------------------------------------------------------------
49 non_alphanum = re.compile(r'[^A-Za-z0-9]')
49 non_alphanum = re.compile(r'[^A-Za-z0-9]')
50
50
51 class AuthenticatedHandler(web.RequestHandler):
51 class AuthenticatedHandler(web.RequestHandler):
52 """A RequestHandler with an authenticated user."""
52 """A RequestHandler with an authenticated user."""
53
53
54 def clear_login_cookie(self):
54 def clear_login_cookie(self):
55 self.clear_cookie(self.cookie_name)
55 self.clear_cookie(self.cookie_name)
56
56
57 def get_current_user(self):
57 def get_current_user(self):
58 user_id = self.get_secure_cookie(self.cookie_name)
58 user_id = self.get_secure_cookie(self.cookie_name)
59 # For now the user_id should not return empty, but it could eventually
59 # For now the user_id should not return empty, but it could eventually
60 if user_id == '':
60 if user_id == '':
61 user_id = 'anonymous'
61 user_id = 'anonymous'
62 if user_id is None:
62 if user_id is None:
63 # prevent extra Invalid cookie sig warnings:
63 # prevent extra Invalid cookie sig warnings:
64 self.clear_login_cookie()
64 self.clear_login_cookie()
65 if not self.login_available:
65 if not self.login_available:
66 user_id = 'anonymous'
66 user_id = 'anonymous'
67 return user_id
67 return user_id
68
68
69 @property
69 @property
70 def cookie_name(self):
70 def cookie_name(self):
71 default_cookie_name = non_alphanum.sub('-', 'username-{}'.format(
71 default_cookie_name = non_alphanum.sub('-', 'username-{}'.format(
72 self.request.host
72 self.request.host
73 ))
73 ))
74 return self.settings.get('cookie_name', default_cookie_name)
74 return self.settings.get('cookie_name', default_cookie_name)
75
75
76 @property
76 @property
77 def password(self):
77 def password(self):
78 """our password"""
78 """our password"""
79 return self.settings.get('password', '')
79 return self.settings.get('password', '')
80
80
81 @property
81 @property
82 def logged_in(self):
82 def logged_in(self):
83 """Is a user currently logged in?
83 """Is a user currently logged in?
84
84
85 """
85 """
86 user = self.get_current_user()
86 user = self.get_current_user()
87 return (user and not user == 'anonymous')
87 return (user and not user == 'anonymous')
88
88
89 @property
89 @property
90 def login_available(self):
90 def login_available(self):
91 """May a user proceed to log in?
91 """May a user proceed to log in?
92
92
93 This returns True if login capability is available, irrespective of
93 This returns True if login capability is available, irrespective of
94 whether the user is already logged in or not.
94 whether the user is already logged in or not.
95
95
96 """
96 """
97 return bool(self.settings.get('password', ''))
97 return bool(self.settings.get('password', ''))
98
98
99
99
100 class IPythonHandler(AuthenticatedHandler):
100 class IPythonHandler(AuthenticatedHandler):
101 """IPython-specific extensions to authenticated handling
101 """IPython-specific extensions to authenticated handling
102
102
103 Mostly property shortcuts to IPython-specific settings.
103 Mostly property shortcuts to IPython-specific settings.
104 """
104 """
105
105
106 @property
106 @property
107 def config(self):
107 def config(self):
108 return self.settings.get('config', None)
108 return self.settings.get('config', None)
109
109
110 @property
110 @property
111 def log(self):
111 def log(self):
112 """use the IPython log by default, falling back on tornado's logger"""
112 """use the IPython log by default, falling back on tornado's logger"""
113 if Application.initialized():
113 if Application.initialized():
114 return Application.instance().log
114 return Application.instance().log
115 else:
115 else:
116 return app_log
116 return app_log
117
117
118 #---------------------------------------------------------------
118 #---------------------------------------------------------------
119 # URLs
119 # URLs
120 #---------------------------------------------------------------
120 #---------------------------------------------------------------
121
121
122 @property
122 @property
123 def ws_url(self):
123 def ws_url(self):
124 """websocket url matching the current request
124 """websocket url matching the current request
125
125
126 By default, this is just `''`, indicating that it should match
126 By default, this is just `''`, indicating that it should match
127 the same host, protocol, port, etc.
127 the same host, protocol, port, etc.
128 """
128 """
129 return self.settings.get('websocket_url', '')
129 return self.settings.get('websocket_url', '')
130
130
131 @property
131 @property
132 def mathjax_url(self):
132 def mathjax_url(self):
133 return self.settings.get('mathjax_url', '')
133 return self.settings.get('mathjax_url', '')
134
134
135 @property
135 @property
136 def base_project_url(self):
136 def base_url(self):
137 return self.settings.get('base_project_url', '/')
137 return self.settings.get('base_url', '/')
138
138
139 @property
139 @property
140 def base_kernel_url(self):
140 def base_kernel_url(self):
141 return self.settings.get('base_kernel_url', '/')
141 return self.settings.get('base_kernel_url', '/')
142
142
143 #---------------------------------------------------------------
143 #---------------------------------------------------------------
144 # Manager objects
144 # Manager objects
145 #---------------------------------------------------------------
145 #---------------------------------------------------------------
146
146
147 @property
147 @property
148 def kernel_manager(self):
148 def kernel_manager(self):
149 return self.settings['kernel_manager']
149 return self.settings['kernel_manager']
150
150
151 @property
151 @property
152 def notebook_manager(self):
152 def notebook_manager(self):
153 return self.settings['notebook_manager']
153 return self.settings['notebook_manager']
154
154
155 @property
155 @property
156 def cluster_manager(self):
156 def cluster_manager(self):
157 return self.settings['cluster_manager']
157 return self.settings['cluster_manager']
158
158
159 @property
159 @property
160 def session_manager(self):
160 def session_manager(self):
161 return self.settings['session_manager']
161 return self.settings['session_manager']
162
162
163 @property
163 @property
164 def project_dir(self):
164 def project_dir(self):
165 return self.notebook_manager.notebook_dir
165 return self.notebook_manager.notebook_dir
166
166
167 #---------------------------------------------------------------
167 #---------------------------------------------------------------
168 # template rendering
168 # template rendering
169 #---------------------------------------------------------------
169 #---------------------------------------------------------------
170
170
171 def get_template(self, name):
171 def get_template(self, name):
172 """Return the jinja template object for a given name"""
172 """Return the jinja template object for a given name"""
173 return self.settings['jinja2_env'].get_template(name)
173 return self.settings['jinja2_env'].get_template(name)
174
174
175 def render_template(self, name, **ns):
175 def render_template(self, name, **ns):
176 ns.update(self.template_namespace)
176 ns.update(self.template_namespace)
177 template = self.get_template(name)
177 template = self.get_template(name)
178 return template.render(**ns)
178 return template.render(**ns)
179
179
180 @property
180 @property
181 def template_namespace(self):
181 def template_namespace(self):
182 return dict(
182 return dict(
183 base_project_url=self.base_project_url,
183 base_url=self.base_url,
184 base_kernel_url=self.base_kernel_url,
184 base_kernel_url=self.base_kernel_url,
185 logged_in=self.logged_in,
185 logged_in=self.logged_in,
186 login_available=self.login_available,
186 login_available=self.login_available,
187 static_url=self.static_url,
187 static_url=self.static_url,
188 )
188 )
189
189
190 def get_json_body(self):
190 def get_json_body(self):
191 """Return the body of the request as JSON data."""
191 """Return the body of the request as JSON data."""
192 if not self.request.body:
192 if not self.request.body:
193 return None
193 return None
194 # Do we need to call body.decode('utf-8') here?
194 # Do we need to call body.decode('utf-8') here?
195 body = self.request.body.strip().decode(u'utf-8')
195 body = self.request.body.strip().decode(u'utf-8')
196 try:
196 try:
197 model = json.loads(body)
197 model = json.loads(body)
198 except Exception:
198 except Exception:
199 self.log.debug("Bad JSON: %r", body)
199 self.log.debug("Bad JSON: %r", body)
200 self.log.error("Couldn't parse JSON", exc_info=True)
200 self.log.error("Couldn't parse JSON", exc_info=True)
201 raise web.HTTPError(400, u'Invalid JSON in body of request')
201 raise web.HTTPError(400, u'Invalid JSON in body of request')
202 return model
202 return model
203
203
204 def get_error_html(self, status_code, **kwargs):
204 def get_error_html(self, status_code, **kwargs):
205 """render custom error pages"""
205 """render custom error pages"""
206 exception = kwargs.get('exception')
206 exception = kwargs.get('exception')
207 message = ''
207 message = ''
208 status_message = responses.get(status_code, 'Unknown HTTP Error')
208 status_message = responses.get(status_code, 'Unknown HTTP Error')
209 if exception:
209 if exception:
210 # get the custom message, if defined
210 # get the custom message, if defined
211 try:
211 try:
212 message = exception.log_message % exception.args
212 message = exception.log_message % exception.args
213 except Exception:
213 except Exception:
214 pass
214 pass
215
215
216 # construct the custom reason, if defined
216 # construct the custom reason, if defined
217 reason = getattr(exception, 'reason', '')
217 reason = getattr(exception, 'reason', '')
218 if reason:
218 if reason:
219 status_message = reason
219 status_message = reason
220
220
221 # build template namespace
221 # build template namespace
222 ns = dict(
222 ns = dict(
223 status_code=status_code,
223 status_code=status_code,
224 status_message=status_message,
224 status_message=status_message,
225 message=message,
225 message=message,
226 exception=exception,
226 exception=exception,
227 )
227 )
228
228
229 # render the template
229 # render the template
230 try:
230 try:
231 html = self.render_template('%s.html' % status_code, **ns)
231 html = self.render_template('%s.html' % status_code, **ns)
232 except TemplateNotFound:
232 except TemplateNotFound:
233 self.log.debug("No template for %d", status_code)
233 self.log.debug("No template for %d", status_code)
234 html = self.render_template('error.html', **ns)
234 html = self.render_template('error.html', **ns)
235 return html
235 return html
236
236
237
237
238 class Template404(IPythonHandler):
238 class Template404(IPythonHandler):
239 """Render our 404 template"""
239 """Render our 404 template"""
240 def prepare(self):
240 def prepare(self):
241 raise web.HTTPError(404)
241 raise web.HTTPError(404)
242
242
243
243
244 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
244 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
245 """static files should only be accessible when logged in"""
245 """static files should only be accessible when logged in"""
246
246
247 @web.authenticated
247 @web.authenticated
248 def get(self, path):
248 def get(self, path):
249 if os.path.splitext(path)[1] == '.ipynb':
249 if os.path.splitext(path)[1] == '.ipynb':
250 name = os.path.basename(path)
250 name = os.path.basename(path)
251 self.set_header('Content-Type', 'application/json')
251 self.set_header('Content-Type', 'application/json')
252 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
252 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
253
253
254 return web.StaticFileHandler.get(self, path)
254 return web.StaticFileHandler.get(self, path)
255
255
256 def compute_etag(self):
256 def compute_etag(self):
257 return None
257 return None
258
258
259 def validate_absolute_path(self, root, absolute_path):
259 def validate_absolute_path(self, root, absolute_path):
260 """Validate and return the absolute path.
260 """Validate and return the absolute path.
261
261
262 Requires tornado 3.1
262 Requires tornado 3.1
263
263
264 Adding to tornado's own handling, forbids the serving of hidden files.
264 Adding to tornado's own handling, forbids the serving of hidden files.
265 """
265 """
266 abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
266 abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
267 abs_root = os.path.abspath(root)
267 abs_root = os.path.abspath(root)
268 if is_hidden(abs_path, abs_root):
268 if is_hidden(abs_path, abs_root):
269 raise web.HTTPError(404)
269 raise web.HTTPError(404)
270 return abs_path
270 return abs_path
271
271
272
272
273 def json_errors(method):
273 def json_errors(method):
274 """Decorate methods with this to return GitHub style JSON errors.
274 """Decorate methods with this to return GitHub style JSON errors.
275
275
276 This should be used on any JSON API on any handler method that can raise HTTPErrors.
276 This should be used on any JSON API on any handler method that can raise HTTPErrors.
277
277
278 This will grab the latest HTTPError exception using sys.exc_info
278 This will grab the latest HTTPError exception using sys.exc_info
279 and then:
279 and then:
280
280
281 1. Set the HTTP status code based on the HTTPError
281 1. Set the HTTP status code based on the HTTPError
282 2. Create and return a JSON body with a message field describing
282 2. Create and return a JSON body with a message field describing
283 the error in a human readable form.
283 the error in a human readable form.
284 """
284 """
285 @functools.wraps(method)
285 @functools.wraps(method)
286 def wrapper(self, *args, **kwargs):
286 def wrapper(self, *args, **kwargs):
287 try:
287 try:
288 result = method(self, *args, **kwargs)
288 result = method(self, *args, **kwargs)
289 except web.HTTPError as e:
289 except web.HTTPError as e:
290 status = e.status_code
290 status = e.status_code
291 message = e.log_message
291 message = e.log_message
292 self.set_status(e.status_code)
292 self.set_status(e.status_code)
293 self.finish(json.dumps(dict(message=message)))
293 self.finish(json.dumps(dict(message=message)))
294 except Exception:
294 except Exception:
295 self.log.error("Unhandled error in API request", exc_info=True)
295 self.log.error("Unhandled error in API request", exc_info=True)
296 status = 500
296 status = 500
297 message = "Unknown server error"
297 message = "Unknown server error"
298 t, value, tb = sys.exc_info()
298 t, value, tb = sys.exc_info()
299 self.set_status(status)
299 self.set_status(status)
300 tb_text = ''.join(traceback.format_exception(t, value, tb))
300 tb_text = ''.join(traceback.format_exception(t, value, tb))
301 reply = dict(message=message, traceback=tb_text)
301 reply = dict(message=message, traceback=tb_text)
302 self.finish(json.dumps(reply))
302 self.finish(json.dumps(reply))
303 else:
303 else:
304 return result
304 return result
305 return wrapper
305 return wrapper
306
306
307
307
308
308
309 #-----------------------------------------------------------------------------
309 #-----------------------------------------------------------------------------
310 # File handler
310 # File handler
311 #-----------------------------------------------------------------------------
311 #-----------------------------------------------------------------------------
312
312
313 # to minimize subclass changes:
313 # to minimize subclass changes:
314 HTTPError = web.HTTPError
314 HTTPError = web.HTTPError
315
315
316 class FileFindHandler(web.StaticFileHandler):
316 class FileFindHandler(web.StaticFileHandler):
317 """subclass of StaticFileHandler for serving files from a search path"""
317 """subclass of StaticFileHandler for serving files from a search path"""
318
318
319 # cache search results, don't search for files more than once
319 # cache search results, don't search for files more than once
320 _static_paths = {}
320 _static_paths = {}
321
321
322 def initialize(self, path, default_filename=None):
322 def initialize(self, path, default_filename=None):
323 if isinstance(path, string_types):
323 if isinstance(path, string_types):
324 path = [path]
324 path = [path]
325
325
326 self.root = tuple(
326 self.root = tuple(
327 os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
327 os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
328 )
328 )
329 self.default_filename = default_filename
329 self.default_filename = default_filename
330
330
331 def compute_etag(self):
331 def compute_etag(self):
332 return None
332 return None
333
333
334 @classmethod
334 @classmethod
335 def get_absolute_path(cls, roots, path):
335 def get_absolute_path(cls, roots, path):
336 """locate a file to serve on our static file search path"""
336 """locate a file to serve on our static file search path"""
337 with cls._lock:
337 with cls._lock:
338 if path in cls._static_paths:
338 if path in cls._static_paths:
339 return cls._static_paths[path]
339 return cls._static_paths[path]
340 try:
340 try:
341 abspath = os.path.abspath(filefind(path, roots))
341 abspath = os.path.abspath(filefind(path, roots))
342 except IOError:
342 except IOError:
343 # IOError means not found
343 # IOError means not found
344 return ''
344 return ''
345
345
346 cls._static_paths[path] = abspath
346 cls._static_paths[path] = abspath
347 return abspath
347 return abspath
348
348
349 def validate_absolute_path(self, root, absolute_path):
349 def validate_absolute_path(self, root, absolute_path):
350 """check if the file should be served (raises 404, 403, etc.)"""
350 """check if the file should be served (raises 404, 403, etc.)"""
351 if absolute_path == '':
351 if absolute_path == '':
352 raise web.HTTPError(404)
352 raise web.HTTPError(404)
353
353
354 for root in self.root:
354 for root in self.root:
355 if (absolute_path + os.sep).startswith(root):
355 if (absolute_path + os.sep).startswith(root):
356 break
356 break
357
357
358 return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
358 return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
359
359
360
360
361 class TrailingSlashHandler(web.RequestHandler):
361 class TrailingSlashHandler(web.RequestHandler):
362 """Simple redirect handler that strips trailing slashes
362 """Simple redirect handler that strips trailing slashes
363
363
364 This should be the first, highest priority handler.
364 This should be the first, highest priority handler.
365 """
365 """
366
366
367 SUPPORTED_METHODS = ['GET']
367 SUPPORTED_METHODS = ['GET']
368
368
369 def get(self):
369 def get(self):
370 self.redirect(self.request.uri.rstrip('/'))
370 self.redirect(self.request.uri.rstrip('/'))
371
371
372 #-----------------------------------------------------------------------------
372 #-----------------------------------------------------------------------------
373 # URL pattern fragments for re-use
373 # URL pattern fragments for re-use
374 #-----------------------------------------------------------------------------
374 #-----------------------------------------------------------------------------
375
375
376 path_regex = r"(?P<path>(?:/.*)*)"
376 path_regex = r"(?P<path>(?:/.*)*)"
377 notebook_name_regex = r"(?P<name>[^/]+\.ipynb)"
377 notebook_name_regex = r"(?P<name>[^/]+\.ipynb)"
378 notebook_path_regex = "%s/%s" % (path_regex, notebook_name_regex)
378 notebook_path_regex = "%s/%s" % (path_regex, notebook_name_regex)
379
379
380 #-----------------------------------------------------------------------------
380 #-----------------------------------------------------------------------------
381 # URL to handler mappings
381 # URL to handler mappings
382 #-----------------------------------------------------------------------------
382 #-----------------------------------------------------------------------------
383
383
384
384
385 default_handlers = [
385 default_handlers = [
386 (r".*/", TrailingSlashHandler)
386 (r".*/", TrailingSlashHandler)
387 ]
387 ]
@@ -1,90 +1,90 b''
1 """Tornado handlers for the live notebook view.
1 """Tornado handlers for the live notebook view.
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2011 The IPython Development Team
9 # Copyright (C) 2011 The IPython Development Team
10 #
10 #
11 # Distributed under the terms of the BSD License. The full license is in
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19 import os
19 import os
20 from tornado import web
20 from tornado import web
21 HTTPError = web.HTTPError
21 HTTPError = web.HTTPError
22
22
23 from ..base.handlers import IPythonHandler, notebook_path_regex, path_regex
23 from ..base.handlers import IPythonHandler, notebook_path_regex, path_regex
24 from ..utils import url_path_join, url_escape
24 from ..utils import url_path_join, url_escape
25
25
26 #-----------------------------------------------------------------------------
26 #-----------------------------------------------------------------------------
27 # Handlers
27 # Handlers
28 #-----------------------------------------------------------------------------
28 #-----------------------------------------------------------------------------
29
29
30
30
31 class NotebookHandler(IPythonHandler):
31 class NotebookHandler(IPythonHandler):
32
32
33 @web.authenticated
33 @web.authenticated
34 def get(self, path='', name=None):
34 def get(self, path='', name=None):
35 """get renders the notebook template if a name is given, or
35 """get renders the notebook template if a name is given, or
36 redirects to the '/files/' handler if the name is not given."""
36 redirects to the '/files/' handler if the name is not given."""
37 path = path.strip('/')
37 path = path.strip('/')
38 nbm = self.notebook_manager
38 nbm = self.notebook_manager
39 if name is None:
39 if name is None:
40 raise web.HTTPError(500, "This shouldn't be accessible: %s" % self.request.uri)
40 raise web.HTTPError(500, "This shouldn't be accessible: %s" % self.request.uri)
41
41
42 # a .ipynb filename was given
42 # a .ipynb filename was given
43 if not nbm.notebook_exists(name, path):
43 if not nbm.notebook_exists(name, path):
44 raise web.HTTPError(404, u'Notebook does not exist: %s/%s' % (path, name))
44 raise web.HTTPError(404, u'Notebook does not exist: %s/%s' % (path, name))
45 name = url_escape(name)
45 name = url_escape(name)
46 path = url_escape(path)
46 path = url_escape(path)
47 self.write(self.render_template('notebook.html',
47 self.write(self.render_template('notebook.html',
48 project=self.project_dir,
48 project=self.project_dir,
49 notebook_path=path,
49 notebook_path=path,
50 notebook_name=name,
50 notebook_name=name,
51 kill_kernel=False,
51 kill_kernel=False,
52 mathjax_url=self.mathjax_url,
52 mathjax_url=self.mathjax_url,
53 )
53 )
54 )
54 )
55
55
56 class NotebookRedirectHandler(IPythonHandler):
56 class NotebookRedirectHandler(IPythonHandler):
57 def get(self, path=''):
57 def get(self, path=''):
58 nbm = self.notebook_manager
58 nbm = self.notebook_manager
59 if nbm.path_exists(path):
59 if nbm.path_exists(path):
60 # it's a *directory*, redirect to /tree
60 # it's a *directory*, redirect to /tree
61 url = url_path_join(self.base_project_url, 'tree', path)
61 url = url_path_join(self.base_url, 'tree', path)
62 else:
62 else:
63 # otherwise, redirect to /files
63 # otherwise, redirect to /files
64 if '/files/' in path:
64 if '/files/' in path:
65 # redirect without files/ iff it would 404
65 # redirect without files/ iff it would 404
66 # this preserves pre-2.0-style 'files/' links
66 # this preserves pre-2.0-style 'files/' links
67 # FIXME: this is hardcoded based on notebook_path,
67 # FIXME: this is hardcoded based on notebook_path,
68 # but so is the files handler itself,
68 # but so is the files handler itself,
69 # so it should work until both are cleaned up.
69 # so it should work until both are cleaned up.
70 parts = path.split('/')
70 parts = path.split('/')
71 files_path = os.path.join(nbm.notebook_dir, *parts)
71 files_path = os.path.join(nbm.notebook_dir, *parts)
72 self.log.warn("filespath: %s", files_path)
72 self.log.warn("filespath: %s", files_path)
73 if not os.path.exists(files_path):
73 if not os.path.exists(files_path):
74 path = path.replace('/files/', '/', 1)
74 path = path.replace('/files/', '/', 1)
75
75
76 url = url_path_join(self.base_project_url, 'files', path)
76 url = url_path_join(self.base_url, 'files', path)
77 url = url_escape(url)
77 url = url_escape(url)
78 self.log.debug("Redirecting %s to %s", self.request.path, url)
78 self.log.debug("Redirecting %s to %s", self.request.path, url)
79 self.redirect(url)
79 self.redirect(url)
80
80
81 #-----------------------------------------------------------------------------
81 #-----------------------------------------------------------------------------
82 # URL to handler mappings
82 # URL to handler mappings
83 #-----------------------------------------------------------------------------
83 #-----------------------------------------------------------------------------
84
84
85
85
86 default_handlers = [
86 default_handlers = [
87 (r"/notebooks%s" % notebook_path_regex, NotebookHandler),
87 (r"/notebooks%s" % notebook_path_regex, NotebookHandler),
88 (r"/notebooks%s" % path_regex, NotebookRedirectHandler),
88 (r"/notebooks%s" % path_regex, NotebookRedirectHandler),
89 ]
89 ]
90
90
@@ -1,837 +1,842 b''
1 # coding: utf-8
1 # coding: utf-8
2 """A tornado based IPython notebook server.
2 """A tornado based IPython notebook server.
3
3
4 Authors:
4 Authors:
5
5
6 * Brian Granger
6 * Brian Granger
7 """
7 """
8 from __future__ import print_function
8 from __future__ import print_function
9 #-----------------------------------------------------------------------------
9 #-----------------------------------------------------------------------------
10 # Copyright (C) 2013 The IPython Development Team
10 # Copyright (C) 2013 The IPython Development Team
11 #
11 #
12 # Distributed under the terms of the BSD License. The full license is in
12 # Distributed under the terms of the BSD License. The full license is in
13 # the file COPYING, distributed as part of this software.
13 # the file COPYING, distributed as part of this software.
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15
15
16 #-----------------------------------------------------------------------------
16 #-----------------------------------------------------------------------------
17 # Imports
17 # Imports
18 #-----------------------------------------------------------------------------
18 #-----------------------------------------------------------------------------
19
19
20 # stdlib
20 # stdlib
21 import errno
21 import errno
22 import io
22 import io
23 import json
23 import json
24 import logging
24 import logging
25 import os
25 import os
26 import random
26 import random
27 import select
27 import select
28 import signal
28 import signal
29 import socket
29 import socket
30 import sys
30 import sys
31 import threading
31 import threading
32 import time
32 import time
33 import webbrowser
33 import webbrowser
34
34
35
35
36 # Third party
36 # Third party
37 # check for pyzmq 2.1.11
37 # check for pyzmq 2.1.11
38 from IPython.utils.zmqrelated import check_for_zmq
38 from IPython.utils.zmqrelated import check_for_zmq
39 check_for_zmq('2.1.11', 'IPython.html')
39 check_for_zmq('2.1.11', 'IPython.html')
40
40
41 from jinja2 import Environment, FileSystemLoader
41 from jinja2 import Environment, FileSystemLoader
42
42
43 # Install the pyzmq ioloop. This has to be done before anything else from
43 # Install the pyzmq ioloop. This has to be done before anything else from
44 # tornado is imported.
44 # tornado is imported.
45 from zmq.eventloop import ioloop
45 from zmq.eventloop import ioloop
46 ioloop.install()
46 ioloop.install()
47
47
48 # check for tornado 3.1.0
48 # check for tornado 3.1.0
49 msg = "The IPython Notebook requires tornado >= 3.1.0"
49 msg = "The IPython Notebook requires tornado >= 3.1.0"
50 try:
50 try:
51 import tornado
51 import tornado
52 except ImportError:
52 except ImportError:
53 raise ImportError(msg)
53 raise ImportError(msg)
54 try:
54 try:
55 version_info = tornado.version_info
55 version_info = tornado.version_info
56 except AttributeError:
56 except AttributeError:
57 raise ImportError(msg + ", but you have < 1.1.0")
57 raise ImportError(msg + ", but you have < 1.1.0")
58 if version_info < (3,1,0):
58 if version_info < (3,1,0):
59 raise ImportError(msg + ", but you have %s" % tornado.version)
59 raise ImportError(msg + ", but you have %s" % tornado.version)
60
60
61 from tornado import httpserver
61 from tornado import httpserver
62 from tornado import web
62 from tornado import web
63
63
64 # Our own libraries
64 # Our own libraries
65 from IPython.html import DEFAULT_STATIC_FILES_PATH
65 from IPython.html import DEFAULT_STATIC_FILES_PATH
66 from .base.handlers import Template404
66 from .base.handlers import Template404
67 from .log import log_request
67 from .log import log_request
68 from .services.kernels.kernelmanager import MappingKernelManager
68 from .services.kernels.kernelmanager import MappingKernelManager
69 from .services.notebooks.nbmanager import NotebookManager
69 from .services.notebooks.nbmanager import NotebookManager
70 from .services.notebooks.filenbmanager import FileNotebookManager
70 from .services.notebooks.filenbmanager import FileNotebookManager
71 from .services.clusters.clustermanager import ClusterManager
71 from .services.clusters.clustermanager import ClusterManager
72 from .services.sessions.sessionmanager import SessionManager
72 from .services.sessions.sessionmanager import SessionManager
73
73
74 from .base.handlers import AuthenticatedFileHandler, FileFindHandler
74 from .base.handlers import AuthenticatedFileHandler, FileFindHandler
75
75
76 from IPython.config.application import catch_config_error, boolean_flag
76 from IPython.config.application import catch_config_error, boolean_flag
77 from IPython.core.application import BaseIPythonApplication
77 from IPython.core.application import BaseIPythonApplication
78 from IPython.core.profiledir import ProfileDir
78 from IPython.core.profiledir import ProfileDir
79 from IPython.consoleapp import IPythonConsoleApp
79 from IPython.consoleapp import IPythonConsoleApp
80 from IPython.kernel import swallow_argv
80 from IPython.kernel import swallow_argv
81 from IPython.kernel.zmq.session import default_secure
81 from IPython.kernel.zmq.session import default_secure
82 from IPython.kernel.zmq.kernelapp import (
82 from IPython.kernel.zmq.kernelapp import (
83 kernel_flags,
83 kernel_flags,
84 kernel_aliases,
84 kernel_aliases,
85 )
85 )
86 from IPython.utils.importstring import import_item
86 from IPython.utils.importstring import import_item
87 from IPython.utils.localinterfaces import localhost
87 from IPython.utils.localinterfaces import localhost
88 from IPython.utils import submodule
88 from IPython.utils import submodule
89 from IPython.utils.traitlets import (
89 from IPython.utils.traitlets import (
90 Dict, Unicode, Integer, List, Bool, Bytes,
90 Dict, Unicode, Integer, List, Bool, Bytes,
91 DottedObjectName
91 DottedObjectName
92 )
92 )
93 from IPython.utils import py3compat
93 from IPython.utils import py3compat
94 from IPython.utils.path import filefind, get_ipython_dir
94 from IPython.utils.path import filefind, get_ipython_dir
95
95
96 from .utils import url_path_join
96 from .utils import url_path_join
97
97
98 #-----------------------------------------------------------------------------
98 #-----------------------------------------------------------------------------
99 # Module globals
99 # Module globals
100 #-----------------------------------------------------------------------------
100 #-----------------------------------------------------------------------------
101
101
102 _examples = """
102 _examples = """
103 ipython notebook # start the notebook
103 ipython notebook # start the notebook
104 ipython notebook --profile=sympy # use the sympy profile
104 ipython notebook --profile=sympy # use the sympy profile
105 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
105 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
106 """
106 """
107
107
108 #-----------------------------------------------------------------------------
108 #-----------------------------------------------------------------------------
109 # Helper functions
109 # Helper functions
110 #-----------------------------------------------------------------------------
110 #-----------------------------------------------------------------------------
111
111
112 def random_ports(port, n):
112 def random_ports(port, n):
113 """Generate a list of n random ports near the given port.
113 """Generate a list of n random ports near the given port.
114
114
115 The first 5 ports will be sequential, and the remaining n-5 will be
115 The first 5 ports will be sequential, and the remaining n-5 will be
116 randomly selected in the range [port-2*n, port+2*n].
116 randomly selected in the range [port-2*n, port+2*n].
117 """
117 """
118 for i in range(min(5, n)):
118 for i in range(min(5, n)):
119 yield port + i
119 yield port + i
120 for i in range(n-5):
120 for i in range(n-5):
121 yield max(1, port + random.randint(-2*n, 2*n))
121 yield max(1, port + random.randint(-2*n, 2*n))
122
122
123 def load_handlers(name):
123 def load_handlers(name):
124 """Load the (URL pattern, handler) tuples for each component."""
124 """Load the (URL pattern, handler) tuples for each component."""
125 name = 'IPython.html.' + name
125 name = 'IPython.html.' + name
126 mod = __import__(name, fromlist=['default_handlers'])
126 mod = __import__(name, fromlist=['default_handlers'])
127 return mod.default_handlers
127 return mod.default_handlers
128
128
129 #-----------------------------------------------------------------------------
129 #-----------------------------------------------------------------------------
130 # The Tornado web application
130 # The Tornado web application
131 #-----------------------------------------------------------------------------
131 #-----------------------------------------------------------------------------
132
132
133 class NotebookWebApplication(web.Application):
133 class NotebookWebApplication(web.Application):
134
134
135 def __init__(self, ipython_app, kernel_manager, notebook_manager,
135 def __init__(self, ipython_app, kernel_manager, notebook_manager,
136 cluster_manager, session_manager, log, base_project_url,
136 cluster_manager, session_manager, log, base_url,
137 settings_overrides):
137 settings_overrides):
138
138
139 settings = self.init_settings(
139 settings = self.init_settings(
140 ipython_app, kernel_manager, notebook_manager, cluster_manager,
140 ipython_app, kernel_manager, notebook_manager, cluster_manager,
141 session_manager, log, base_project_url, settings_overrides)
141 session_manager, log, base_url, settings_overrides)
142 handlers = self.init_handlers(settings)
142 handlers = self.init_handlers(settings)
143
143
144 super(NotebookWebApplication, self).__init__(handlers, **settings)
144 super(NotebookWebApplication, self).__init__(handlers, **settings)
145
145
146 def init_settings(self, ipython_app, kernel_manager, notebook_manager,
146 def init_settings(self, ipython_app, kernel_manager, notebook_manager,
147 cluster_manager, session_manager, log, base_project_url,
147 cluster_manager, session_manager, log, base_url,
148 settings_overrides):
148 settings_overrides):
149 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
149 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
150 # base_project_url will always be unicode, which will in turn
150 # base_url will always be unicode, which will in turn
151 # make the patterns unicode, and ultimately result in unicode
151 # make the patterns unicode, and ultimately result in unicode
152 # keys in kwargs to handler._execute(**kwargs) in tornado.
152 # keys in kwargs to handler._execute(**kwargs) in tornado.
153 # This enforces that base_project_url be ascii in that situation.
153 # This enforces that base_url be ascii in that situation.
154 #
154 #
155 # Note that the URLs these patterns check against are escaped,
155 # Note that the URLs these patterns check against are escaped,
156 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
156 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
157 base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii')
157 base_url = py3compat.unicode_to_str(base_url, 'ascii')
158 template_path = settings_overrides.get("template_path", os.path.join(os.path.dirname(__file__), "templates"))
158 template_path = settings_overrides.get("template_path", os.path.join(os.path.dirname(__file__), "templates"))
159 settings = dict(
159 settings = dict(
160 # basics
160 # basics
161 log_function=log_request,
161 log_function=log_request,
162 base_project_url=base_project_url,
162 base_url=base_url,
163 base_kernel_url=ipython_app.base_kernel_url,
163 base_kernel_url=ipython_app.base_kernel_url,
164 template_path=template_path,
164 template_path=template_path,
165 static_path=ipython_app.static_file_path,
165 static_path=ipython_app.static_file_path,
166 static_handler_class = FileFindHandler,
166 static_handler_class = FileFindHandler,
167 static_url_prefix = url_path_join(base_project_url,'/static/'),
167 static_url_prefix = url_path_join(base_url,'/static/'),
168
168
169 # authentication
169 # authentication
170 cookie_secret=ipython_app.cookie_secret,
170 cookie_secret=ipython_app.cookie_secret,
171 login_url=url_path_join(base_project_url,'/login'),
171 login_url=url_path_join(base_url,'/login'),
172 password=ipython_app.password,
172 password=ipython_app.password,
173
173
174 # managers
174 # managers
175 kernel_manager=kernel_manager,
175 kernel_manager=kernel_manager,
176 notebook_manager=notebook_manager,
176 notebook_manager=notebook_manager,
177 cluster_manager=cluster_manager,
177 cluster_manager=cluster_manager,
178 session_manager=session_manager,
178 session_manager=session_manager,
179
179
180 # IPython stuff
180 # IPython stuff
181 nbextensions_path = ipython_app.nbextensions_path,
181 nbextensions_path = ipython_app.nbextensions_path,
182 mathjax_url=ipython_app.mathjax_url,
182 mathjax_url=ipython_app.mathjax_url,
183 config=ipython_app.config,
183 config=ipython_app.config,
184 jinja2_env=Environment(loader=FileSystemLoader(template_path)),
184 jinja2_env=Environment(loader=FileSystemLoader(template_path)),
185 )
185 )
186
186
187 # allow custom overrides for the tornado web app.
187 # allow custom overrides for the tornado web app.
188 settings.update(settings_overrides)
188 settings.update(settings_overrides)
189 return settings
189 return settings
190
190
191 def init_handlers(self, settings):
191 def init_handlers(self, settings):
192 # Load the (URL pattern, handler) tuples for each component.
192 # Load the (URL pattern, handler) tuples for each component.
193 handlers = []
193 handlers = []
194 handlers.extend(load_handlers('base.handlers'))
194 handlers.extend(load_handlers('base.handlers'))
195 handlers.extend(load_handlers('tree.handlers'))
195 handlers.extend(load_handlers('tree.handlers'))
196 handlers.extend(load_handlers('auth.login'))
196 handlers.extend(load_handlers('auth.login'))
197 handlers.extend(load_handlers('auth.logout'))
197 handlers.extend(load_handlers('auth.logout'))
198 handlers.extend(load_handlers('notebook.handlers'))
198 handlers.extend(load_handlers('notebook.handlers'))
199 handlers.extend(load_handlers('nbconvert.handlers'))
199 handlers.extend(load_handlers('nbconvert.handlers'))
200 handlers.extend(load_handlers('services.kernels.handlers'))
200 handlers.extend(load_handlers('services.kernels.handlers'))
201 handlers.extend(load_handlers('services.notebooks.handlers'))
201 handlers.extend(load_handlers('services.notebooks.handlers'))
202 handlers.extend(load_handlers('services.clusters.handlers'))
202 handlers.extend(load_handlers('services.clusters.handlers'))
203 handlers.extend(load_handlers('services.sessions.handlers'))
203 handlers.extend(load_handlers('services.sessions.handlers'))
204 handlers.extend(load_handlers('services.nbconvert.handlers'))
204 handlers.extend(load_handlers('services.nbconvert.handlers'))
205 handlers.extend([
205 handlers.extend([
206 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : settings['notebook_manager'].notebook_dir}),
206 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : settings['notebook_manager'].notebook_dir}),
207 (r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}),
207 (r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}),
208 ])
208 ])
209 # prepend base_project_url onto the patterns that we match
209 # prepend base_url onto the patterns that we match
210 new_handlers = []
210 new_handlers = []
211 for handler in handlers:
211 for handler in handlers:
212 pattern = url_path_join(settings['base_project_url'], handler[0])
212 pattern = url_path_join(settings['base_url'], handler[0])
213 new_handler = tuple([pattern] + list(handler[1:]))
213 new_handler = tuple([pattern] + list(handler[1:]))
214 new_handlers.append(new_handler)
214 new_handlers.append(new_handler)
215 # add 404 on the end, which will catch everything that falls through
215 # add 404 on the end, which will catch everything that falls through
216 new_handlers.append((r'(.*)', Template404))
216 new_handlers.append((r'(.*)', Template404))
217 return new_handlers
217 return new_handlers
218
218
219
219
220 class NbserverListApp(BaseIPythonApplication):
220 class NbserverListApp(BaseIPythonApplication):
221
221
222 description="List currently running notebook servers in this profile."
222 description="List currently running notebook servers in this profile."
223
223
224 flags = dict(
224 flags = dict(
225 json=({'NbserverListApp': {'json': True}},
225 json=({'NbserverListApp': {'json': True}},
226 "Produce machine-readable JSON output."),
226 "Produce machine-readable JSON output."),
227 )
227 )
228
228
229 json = Bool(False, config=True,
229 json = Bool(False, config=True,
230 help="If True, each line of output will be a JSON object with the "
230 help="If True, each line of output will be a JSON object with the "
231 "details from the server info file.")
231 "details from the server info file.")
232
232
233 def start(self):
233 def start(self):
234 if not self.json:
234 if not self.json:
235 print("Currently running servers:")
235 print("Currently running servers:")
236 for serverinfo in list_running_servers(self.profile):
236 for serverinfo in list_running_servers(self.profile):
237 if self.json:
237 if self.json:
238 print(json.dumps(serverinfo))
238 print(json.dumps(serverinfo))
239 else:
239 else:
240 print(serverinfo['url'], "::", serverinfo['notebook_dir'])
240 print(serverinfo['url'], "::", serverinfo['notebook_dir'])
241
241
242 #-----------------------------------------------------------------------------
242 #-----------------------------------------------------------------------------
243 # Aliases and Flags
243 # Aliases and Flags
244 #-----------------------------------------------------------------------------
244 #-----------------------------------------------------------------------------
245
245
246 flags = dict(kernel_flags)
246 flags = dict(kernel_flags)
247 flags['no-browser']=(
247 flags['no-browser']=(
248 {'NotebookApp' : {'open_browser' : False}},
248 {'NotebookApp' : {'open_browser' : False}},
249 "Don't open the notebook in a browser after startup."
249 "Don't open the notebook in a browser after startup."
250 )
250 )
251 flags['no-mathjax']=(
251 flags['no-mathjax']=(
252 {'NotebookApp' : {'enable_mathjax' : False}},
252 {'NotebookApp' : {'enable_mathjax' : False}},
253 """Disable MathJax
253 """Disable MathJax
254
254
255 MathJax is the javascript library IPython uses to render math/LaTeX. It is
255 MathJax is the javascript library IPython uses to render math/LaTeX. It is
256 very large, so you may want to disable it if you have a slow internet
256 very large, so you may want to disable it if you have a slow internet
257 connection, or for offline use of the notebook.
257 connection, or for offline use of the notebook.
258
258
259 When disabled, equations etc. will appear as their untransformed TeX source.
259 When disabled, equations etc. will appear as their untransformed TeX source.
260 """
260 """
261 )
261 )
262
262
263 # Add notebook manager flags
263 # Add notebook manager flags
264 flags.update(boolean_flag('script', 'FileNotebookManager.save_script',
264 flags.update(boolean_flag('script', 'FileNotebookManager.save_script',
265 'Auto-save a .py script everytime the .ipynb notebook is saved',
265 'Auto-save a .py script everytime the .ipynb notebook is saved',
266 'Do not auto-save .py scripts for every notebook'))
266 'Do not auto-save .py scripts for every notebook'))
267
267
268 # the flags that are specific to the frontend
268 # the flags that are specific to the frontend
269 # these must be scrubbed before being passed to the kernel,
269 # these must be scrubbed before being passed to the kernel,
270 # or it will raise an error on unrecognized flags
270 # or it will raise an error on unrecognized flags
271 notebook_flags = ['no-browser', 'no-mathjax', 'script', 'no-script']
271 notebook_flags = ['no-browser', 'no-mathjax', 'script', 'no-script']
272
272
273 aliases = dict(kernel_aliases)
273 aliases = dict(kernel_aliases)
274
274
275 aliases.update({
275 aliases.update({
276 'ip': 'NotebookApp.ip',
276 'ip': 'NotebookApp.ip',
277 'port': 'NotebookApp.port',
277 'port': 'NotebookApp.port',
278 'port-retries': 'NotebookApp.port_retries',
278 'port-retries': 'NotebookApp.port_retries',
279 'transport': 'KernelManager.transport',
279 'transport': 'KernelManager.transport',
280 'keyfile': 'NotebookApp.keyfile',
280 'keyfile': 'NotebookApp.keyfile',
281 'certfile': 'NotebookApp.certfile',
281 'certfile': 'NotebookApp.certfile',
282 'notebook-dir': 'NotebookManager.notebook_dir',
282 'notebook-dir': 'NotebookManager.notebook_dir',
283 'browser': 'NotebookApp.browser',
283 'browser': 'NotebookApp.browser',
284 })
284 })
285
285
286 # remove ipkernel flags that are singletons, and don't make sense in
286 # remove ipkernel flags that are singletons, and don't make sense in
287 # multi-kernel evironment:
287 # multi-kernel evironment:
288 aliases.pop('f', None)
288 aliases.pop('f', None)
289
289
290 notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
290 notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
291 u'notebook-dir', u'profile', u'profile-dir']
291 u'notebook-dir', u'profile', u'profile-dir']
292
292
293 #-----------------------------------------------------------------------------
293 #-----------------------------------------------------------------------------
294 # NotebookApp
294 # NotebookApp
295 #-----------------------------------------------------------------------------
295 #-----------------------------------------------------------------------------
296
296
297 class NotebookApp(BaseIPythonApplication):
297 class NotebookApp(BaseIPythonApplication):
298
298
299 name = 'ipython-notebook'
299 name = 'ipython-notebook'
300
300
301 description = """
301 description = """
302 The IPython HTML Notebook.
302 The IPython HTML Notebook.
303
303
304 This launches a Tornado based HTML Notebook Server that serves up an
304 This launches a Tornado based HTML Notebook Server that serves up an
305 HTML5/Javascript Notebook client.
305 HTML5/Javascript Notebook client.
306 """
306 """
307 examples = _examples
307 examples = _examples
308
308
309 classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager,
309 classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager,
310 FileNotebookManager]
310 FileNotebookManager]
311 flags = Dict(flags)
311 flags = Dict(flags)
312 aliases = Dict(aliases)
312 aliases = Dict(aliases)
313
313
314 subcommands = dict(
314 subcommands = dict(
315 list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
315 list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
316 )
316 )
317
317
318 kernel_argv = List(Unicode)
318 kernel_argv = List(Unicode)
319
319
320 def _log_level_default(self):
320 def _log_level_default(self):
321 return logging.INFO
321 return logging.INFO
322
322
323 def _log_format_default(self):
323 def _log_format_default(self):
324 """override default log format to include time"""
324 """override default log format to include time"""
325 return u"%(asctime)s.%(msecs).03d [%(name)s]%(highlevel)s %(message)s"
325 return u"%(asctime)s.%(msecs).03d [%(name)s]%(highlevel)s %(message)s"
326
326
327 # create requested profiles by default, if they don't exist:
327 # create requested profiles by default, if they don't exist:
328 auto_create = Bool(True)
328 auto_create = Bool(True)
329
329
330 # file to be opened in the notebook server
330 # file to be opened in the notebook server
331 file_to_run = Unicode('')
331 file_to_run = Unicode('')
332
332
333 # Network related information.
333 # Network related information.
334
334
335 ip = Unicode(config=True,
335 ip = Unicode(config=True,
336 help="The IP address the notebook server will listen on."
336 help="The IP address the notebook server will listen on."
337 )
337 )
338 def _ip_default(self):
338 def _ip_default(self):
339 return localhost()
339 return localhost()
340
340
341 def _ip_changed(self, name, old, new):
341 def _ip_changed(self, name, old, new):
342 if new == u'*': self.ip = u''
342 if new == u'*': self.ip = u''
343
343
344 port = Integer(8888, config=True,
344 port = Integer(8888, config=True,
345 help="The port the notebook server will listen on."
345 help="The port the notebook server will listen on."
346 )
346 )
347 port_retries = Integer(50, config=True,
347 port_retries = Integer(50, config=True,
348 help="The number of additional ports to try if the specified port is not available."
348 help="The number of additional ports to try if the specified port is not available."
349 )
349 )
350
350
351 certfile = Unicode(u'', config=True,
351 certfile = Unicode(u'', config=True,
352 help="""The full path to an SSL/TLS certificate file."""
352 help="""The full path to an SSL/TLS certificate file."""
353 )
353 )
354
354
355 keyfile = Unicode(u'', config=True,
355 keyfile = Unicode(u'', config=True,
356 help="""The full path to a private key file for usage with SSL/TLS."""
356 help="""The full path to a private key file for usage with SSL/TLS."""
357 )
357 )
358
358
359 cookie_secret = Bytes(b'', config=True,
359 cookie_secret = Bytes(b'', config=True,
360 help="""The random bytes used to secure cookies.
360 help="""The random bytes used to secure cookies.
361 By default this is a new random number every time you start the Notebook.
361 By default this is a new random number every time you start the Notebook.
362 Set it to a value in a config file to enable logins to persist across server sessions.
362 Set it to a value in a config file to enable logins to persist across server sessions.
363
363
364 Note: Cookie secrets should be kept private, do not share config files with
364 Note: Cookie secrets should be kept private, do not share config files with
365 cookie_secret stored in plaintext (you can read the value from a file).
365 cookie_secret stored in plaintext (you can read the value from a file).
366 """
366 """
367 )
367 )
368 def _cookie_secret_default(self):
368 def _cookie_secret_default(self):
369 return os.urandom(1024)
369 return os.urandom(1024)
370
370
371 password = Unicode(u'', config=True,
371 password = Unicode(u'', config=True,
372 help="""Hashed password to use for web authentication.
372 help="""Hashed password to use for web authentication.
373
373
374 To generate, type in a python/IPython shell:
374 To generate, type in a python/IPython shell:
375
375
376 from IPython.lib import passwd; passwd()
376 from IPython.lib import passwd; passwd()
377
377
378 The string should be of the form type:salt:hashed-password.
378 The string should be of the form type:salt:hashed-password.
379 """
379 """
380 )
380 )
381
381
382 open_browser = Bool(True, config=True,
382 open_browser = Bool(True, config=True,
383 help="""Whether to open in a browser after starting.
383 help="""Whether to open in a browser after starting.
384 The specific browser used is platform dependent and
384 The specific browser used is platform dependent and
385 determined by the python standard library `webbrowser`
385 determined by the python standard library `webbrowser`
386 module, unless it is overridden using the --browser
386 module, unless it is overridden using the --browser
387 (NotebookApp.browser) configuration option.
387 (NotebookApp.browser) configuration option.
388 """)
388 """)
389
389
390 browser = Unicode(u'', config=True,
390 browser = Unicode(u'', config=True,
391 help="""Specify what command to use to invoke a web
391 help="""Specify what command to use to invoke a web
392 browser when opening the notebook. If not specified, the
392 browser when opening the notebook. If not specified, the
393 default browser will be determined by the `webbrowser`
393 default browser will be determined by the `webbrowser`
394 standard library module, which allows setting of the
394 standard library module, which allows setting of the
395 BROWSER environment variable to override it.
395 BROWSER environment variable to override it.
396 """)
396 """)
397
397
398 webapp_settings = Dict(config=True,
398 webapp_settings = Dict(config=True,
399 help="Supply overrides for the tornado.web.Application that the "
399 help="Supply overrides for the tornado.web.Application that the "
400 "IPython notebook uses.")
400 "IPython notebook uses.")
401
401
402 enable_mathjax = Bool(True, config=True,
402 enable_mathjax = Bool(True, config=True,
403 help="""Whether to enable MathJax for typesetting math/TeX
403 help="""Whether to enable MathJax for typesetting math/TeX
404
404
405 MathJax is the javascript library IPython uses to render math/LaTeX. It is
405 MathJax is the javascript library IPython uses to render math/LaTeX. It is
406 very large, so you may want to disable it if you have a slow internet
406 very large, so you may want to disable it if you have a slow internet
407 connection, or for offline use of the notebook.
407 connection, or for offline use of the notebook.
408
408
409 When disabled, equations etc. will appear as their untransformed TeX source.
409 When disabled, equations etc. will appear as their untransformed TeX source.
410 """
410 """
411 )
411 )
412 def _enable_mathjax_changed(self, name, old, new):
412 def _enable_mathjax_changed(self, name, old, new):
413 """set mathjax url to empty if mathjax is disabled"""
413 """set mathjax url to empty if mathjax is disabled"""
414 if not new:
414 if not new:
415 self.mathjax_url = u''
415 self.mathjax_url = u''
416
416
417 base_project_url = Unicode('/', config=True,
417 base_url = Unicode('/', config=True,
418 help='''The base URL for the notebook server.
418 help='''The base URL for the notebook server.
419
419
420 Leading and trailing slashes can be omitted,
420 Leading and trailing slashes can be omitted,
421 and will automatically be added.
421 and will automatically be added.
422 ''')
422 ''')
423 def _base_project_url_changed(self, name, old, new):
423 def _base_url_changed(self, name, old, new):
424 if not new.startswith('/'):
424 if not new.startswith('/'):
425 self.base_project_url = '/'+new
425 self.base_url = '/'+new
426 elif not new.endswith('/'):
426 elif not new.endswith('/'):
427 self.base_project_url = new+'/'
427 self.base_url = new+'/'
428
429 base_project_url = Unicode('/', config=True, help="""DEPRECATED use base_url""")
430 def _base_project_url_changed(self, name, old, new):
431 self.log.warn("base_project_url is deprecated, use base_url")
432 self.base_url = new
428
433
429 base_kernel_url = Unicode('/', config=True,
434 base_kernel_url = Unicode('/', config=True,
430 help='''The base URL for the kernel server
435 help='''The base URL for the kernel server
431
436
432 Leading and trailing slashes can be omitted,
437 Leading and trailing slashes can be omitted,
433 and will automatically be added.
438 and will automatically be added.
434 ''')
439 ''')
435 def _base_kernel_url_changed(self, name, old, new):
440 def _base_kernel_url_changed(self, name, old, new):
436 if not new.startswith('/'):
441 if not new.startswith('/'):
437 self.base_kernel_url = '/'+new
442 self.base_kernel_url = '/'+new
438 elif not new.endswith('/'):
443 elif not new.endswith('/'):
439 self.base_kernel_url = new+'/'
444 self.base_kernel_url = new+'/'
440
445
441 websocket_url = Unicode("", config=True,
446 websocket_url = Unicode("", config=True,
442 help="""The base URL for the websocket server,
447 help="""The base URL for the websocket server,
443 if it differs from the HTTP server (hint: it almost certainly doesn't).
448 if it differs from the HTTP server (hint: it almost certainly doesn't).
444
449
445 Should be in the form of an HTTP origin: ws[s]://hostname[:port]
450 Should be in the form of an HTTP origin: ws[s]://hostname[:port]
446 """
451 """
447 )
452 )
448
453
449 extra_static_paths = List(Unicode, config=True,
454 extra_static_paths = List(Unicode, config=True,
450 help="""Extra paths to search for serving static files.
455 help="""Extra paths to search for serving static files.
451
456
452 This allows adding javascript/css to be available from the notebook server machine,
457 This allows adding javascript/css to be available from the notebook server machine,
453 or overriding individual files in the IPython"""
458 or overriding individual files in the IPython"""
454 )
459 )
455 def _extra_static_paths_default(self):
460 def _extra_static_paths_default(self):
456 return [os.path.join(self.profile_dir.location, 'static')]
461 return [os.path.join(self.profile_dir.location, 'static')]
457
462
458 @property
463 @property
459 def static_file_path(self):
464 def static_file_path(self):
460 """return extra paths + the default location"""
465 """return extra paths + the default location"""
461 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
466 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
462
467
463 nbextensions_path = List(Unicode, config=True,
468 nbextensions_path = List(Unicode, config=True,
464 help="""paths for Javascript extensions. By default, this is just IPYTHONDIR/nbextensions"""
469 help="""paths for Javascript extensions. By default, this is just IPYTHONDIR/nbextensions"""
465 )
470 )
466 def _nbextensions_path_default(self):
471 def _nbextensions_path_default(self):
467 return [os.path.join(get_ipython_dir(), 'nbextensions')]
472 return [os.path.join(get_ipython_dir(), 'nbextensions')]
468
473
469 mathjax_url = Unicode("", config=True,
474 mathjax_url = Unicode("", config=True,
470 help="""The url for MathJax.js."""
475 help="""The url for MathJax.js."""
471 )
476 )
472 def _mathjax_url_default(self):
477 def _mathjax_url_default(self):
473 if not self.enable_mathjax:
478 if not self.enable_mathjax:
474 return u''
479 return u''
475 static_url_prefix = self.webapp_settings.get("static_url_prefix",
480 static_url_prefix = self.webapp_settings.get("static_url_prefix",
476 url_path_join(self.base_project_url, "static")
481 url_path_join(self.base_url, "static")
477 )
482 )
478
483
479 # try local mathjax, either in nbextensions/mathjax or static/mathjax
484 # try local mathjax, either in nbextensions/mathjax or static/mathjax
480 for (url_prefix, search_path) in [
485 for (url_prefix, search_path) in [
481 (url_path_join(self.base_project_url, "nbextensions"), self.nbextensions_path),
486 (url_path_join(self.base_url, "nbextensions"), self.nbextensions_path),
482 (static_url_prefix, self.static_file_path),
487 (static_url_prefix, self.static_file_path),
483 ]:
488 ]:
484 self.log.debug("searching for local mathjax in %s", search_path)
489 self.log.debug("searching for local mathjax in %s", search_path)
485 try:
490 try:
486 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), search_path)
491 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), search_path)
487 except IOError:
492 except IOError:
488 continue
493 continue
489 else:
494 else:
490 url = url_path_join(url_prefix, u"mathjax/MathJax.js")
495 url = url_path_join(url_prefix, u"mathjax/MathJax.js")
491 self.log.info("Serving local MathJax from %s at %s", mathjax, url)
496 self.log.info("Serving local MathJax from %s at %s", mathjax, url)
492 return url
497 return url
493
498
494 # no local mathjax, serve from CDN
499 # no local mathjax, serve from CDN
495 if self.certfile:
500 if self.certfile:
496 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
501 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
497 host = u"https://c328740.ssl.cf1.rackcdn.com"
502 host = u"https://c328740.ssl.cf1.rackcdn.com"
498 else:
503 else:
499 host = u"http://cdn.mathjax.org"
504 host = u"http://cdn.mathjax.org"
500
505
501 url = host + u"/mathjax/latest/MathJax.js"
506 url = host + u"/mathjax/latest/MathJax.js"
502 self.log.info("Using MathJax from CDN: %s", url)
507 self.log.info("Using MathJax from CDN: %s", url)
503 return url
508 return url
504
509
505 def _mathjax_url_changed(self, name, old, new):
510 def _mathjax_url_changed(self, name, old, new):
506 if new and not self.enable_mathjax:
511 if new and not self.enable_mathjax:
507 # enable_mathjax=False overrides mathjax_url
512 # enable_mathjax=False overrides mathjax_url
508 self.mathjax_url = u''
513 self.mathjax_url = u''
509 else:
514 else:
510 self.log.info("Using MathJax: %s", new)
515 self.log.info("Using MathJax: %s", new)
511
516
512 notebook_manager_class = DottedObjectName('IPython.html.services.notebooks.filenbmanager.FileNotebookManager',
517 notebook_manager_class = DottedObjectName('IPython.html.services.notebooks.filenbmanager.FileNotebookManager',
513 config=True,
518 config=True,
514 help='The notebook manager class to use.')
519 help='The notebook manager class to use.')
515
520
516 trust_xheaders = Bool(False, config=True,
521 trust_xheaders = Bool(False, config=True,
517 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
522 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
518 "sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
523 "sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
519 )
524 )
520
525
521 info_file = Unicode()
526 info_file = Unicode()
522
527
523 def _info_file_default(self):
528 def _info_file_default(self):
524 info_file = "nbserver-%s.json"%os.getpid()
529 info_file = "nbserver-%s.json"%os.getpid()
525 return os.path.join(self.profile_dir.security_dir, info_file)
530 return os.path.join(self.profile_dir.security_dir, info_file)
526
531
527 def parse_command_line(self, argv=None):
532 def parse_command_line(self, argv=None):
528 super(NotebookApp, self).parse_command_line(argv)
533 super(NotebookApp, self).parse_command_line(argv)
529
534
530 if self.extra_args:
535 if self.extra_args:
531 arg0 = self.extra_args[0]
536 arg0 = self.extra_args[0]
532 f = os.path.abspath(arg0)
537 f = os.path.abspath(arg0)
533 self.argv.remove(arg0)
538 self.argv.remove(arg0)
534 if not os.path.exists(f):
539 if not os.path.exists(f):
535 self.log.critical("No such file or directory: %s", f)
540 self.log.critical("No such file or directory: %s", f)
536 self.exit(1)
541 self.exit(1)
537 if os.path.isdir(f):
542 if os.path.isdir(f):
538 self.config.FileNotebookManager.notebook_dir = f
543 self.config.FileNotebookManager.notebook_dir = f
539 elif os.path.isfile(f):
544 elif os.path.isfile(f):
540 self.file_to_run = f
545 self.file_to_run = f
541
546
542 def init_kernel_argv(self):
547 def init_kernel_argv(self):
543 """construct the kernel arguments"""
548 """construct the kernel arguments"""
544 # Scrub frontend-specific flags
549 # Scrub frontend-specific flags
545 self.kernel_argv = swallow_argv(self.argv, notebook_aliases, notebook_flags)
550 self.kernel_argv = swallow_argv(self.argv, notebook_aliases, notebook_flags)
546 if any(arg.startswith(u'--pylab') for arg in self.kernel_argv):
551 if any(arg.startswith(u'--pylab') for arg in self.kernel_argv):
547 self.log.warn('\n '.join([
552 self.log.warn('\n '.join([
548 "Starting all kernels in pylab mode is not recommended,",
553 "Starting all kernels in pylab mode is not recommended,",
549 "and will be disabled in a future release.",
554 "and will be disabled in a future release.",
550 "Please use the %matplotlib magic to enable matplotlib instead.",
555 "Please use the %matplotlib magic to enable matplotlib instead.",
551 "pylab implies many imports, which can have confusing side effects",
556 "pylab implies many imports, which can have confusing side effects",
552 "and harm the reproducibility of your notebooks.",
557 "and harm the reproducibility of your notebooks.",
553 ]))
558 ]))
554 # Kernel should inherit default config file from frontend
559 # Kernel should inherit default config file from frontend
555 self.kernel_argv.append("--IPKernelApp.parent_appname='%s'" % self.name)
560 self.kernel_argv.append("--IPKernelApp.parent_appname='%s'" % self.name)
556 # Kernel should get *absolute* path to profile directory
561 # Kernel should get *absolute* path to profile directory
557 self.kernel_argv.extend(["--profile-dir", self.profile_dir.location])
562 self.kernel_argv.extend(["--profile-dir", self.profile_dir.location])
558
563
559 def init_configurables(self):
564 def init_configurables(self):
560 # force Session default to be secure
565 # force Session default to be secure
561 default_secure(self.config)
566 default_secure(self.config)
562 self.kernel_manager = MappingKernelManager(
567 self.kernel_manager = MappingKernelManager(
563 parent=self, log=self.log, kernel_argv=self.kernel_argv,
568 parent=self, log=self.log, kernel_argv=self.kernel_argv,
564 connection_dir = self.profile_dir.security_dir,
569 connection_dir = self.profile_dir.security_dir,
565 )
570 )
566 kls = import_item(self.notebook_manager_class)
571 kls = import_item(self.notebook_manager_class)
567 self.notebook_manager = kls(parent=self, log=self.log)
572 self.notebook_manager = kls(parent=self, log=self.log)
568 self.session_manager = SessionManager(parent=self, log=self.log)
573 self.session_manager = SessionManager(parent=self, log=self.log)
569 self.cluster_manager = ClusterManager(parent=self, log=self.log)
574 self.cluster_manager = ClusterManager(parent=self, log=self.log)
570 self.cluster_manager.update_profiles()
575 self.cluster_manager.update_profiles()
571
576
572 def init_logging(self):
577 def init_logging(self):
573 # This prevents double log messages because tornado use a root logger that
578 # This prevents double log messages because tornado use a root logger that
574 # self.log is a child of. The logging module dipatches log messages to a log
579 # self.log is a child of. The logging module dipatches log messages to a log
575 # and all of its ancenstors until propagate is set to False.
580 # and all of its ancenstors until propagate is set to False.
576 self.log.propagate = False
581 self.log.propagate = False
577
582
578 # hook up tornado 3's loggers to our app handlers
583 # hook up tornado 3's loggers to our app handlers
579 for name in ('access', 'application', 'general'):
584 for name in ('access', 'application', 'general'):
580 logger = logging.getLogger('tornado.%s' % name)
585 logger = logging.getLogger('tornado.%s' % name)
581 logger.parent = self.log
586 logger.parent = self.log
582 logger.setLevel(self.log.level)
587 logger.setLevel(self.log.level)
583
588
584 def init_webapp(self):
589 def init_webapp(self):
585 """initialize tornado webapp and httpserver"""
590 """initialize tornado webapp and httpserver"""
586 self.web_app = NotebookWebApplication(
591 self.web_app = NotebookWebApplication(
587 self, self.kernel_manager, self.notebook_manager,
592 self, self.kernel_manager, self.notebook_manager,
588 self.cluster_manager, self.session_manager,
593 self.cluster_manager, self.session_manager,
589 self.log, self.base_project_url, self.webapp_settings
594 self.log, self.base_url, self.webapp_settings
590 )
595 )
591 if self.certfile:
596 if self.certfile:
592 ssl_options = dict(certfile=self.certfile)
597 ssl_options = dict(certfile=self.certfile)
593 if self.keyfile:
598 if self.keyfile:
594 ssl_options['keyfile'] = self.keyfile
599 ssl_options['keyfile'] = self.keyfile
595 else:
600 else:
596 ssl_options = None
601 ssl_options = None
597 self.web_app.password = self.password
602 self.web_app.password = self.password
598 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
603 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
599 xheaders=self.trust_xheaders)
604 xheaders=self.trust_xheaders)
600 if not self.ip:
605 if not self.ip:
601 warning = "WARNING: The notebook server is listening on all IP addresses"
606 warning = "WARNING: The notebook server is listening on all IP addresses"
602 if ssl_options is None:
607 if ssl_options is None:
603 self.log.critical(warning + " and not using encryption. This "
608 self.log.critical(warning + " and not using encryption. This "
604 "is not recommended.")
609 "is not recommended.")
605 if not self.password:
610 if not self.password:
606 self.log.critical(warning + " and not using authentication. "
611 self.log.critical(warning + " and not using authentication. "
607 "This is highly insecure and not recommended.")
612 "This is highly insecure and not recommended.")
608 success = None
613 success = None
609 for port in random_ports(self.port, self.port_retries+1):
614 for port in random_ports(self.port, self.port_retries+1):
610 try:
615 try:
611 self.http_server.listen(port, self.ip)
616 self.http_server.listen(port, self.ip)
612 except socket.error as e:
617 except socket.error as e:
613 if e.errno == errno.EADDRINUSE:
618 if e.errno == errno.EADDRINUSE:
614 self.log.info('The port %i is already in use, trying another random port.' % port)
619 self.log.info('The port %i is already in use, trying another random port.' % port)
615 continue
620 continue
616 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
621 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
617 self.log.warn("Permission to listen on port %i denied" % port)
622 self.log.warn("Permission to listen on port %i denied" % port)
618 continue
623 continue
619 else:
624 else:
620 raise
625 raise
621 else:
626 else:
622 self.port = port
627 self.port = port
623 success = True
628 success = True
624 break
629 break
625 if not success:
630 if not success:
626 self.log.critical('ERROR: the notebook server could not be started because '
631 self.log.critical('ERROR: the notebook server could not be started because '
627 'no available port could be found.')
632 'no available port could be found.')
628 self.exit(1)
633 self.exit(1)
629
634
630 @property
635 @property
631 def display_url(self):
636 def display_url(self):
632 ip = self.ip if self.ip else '[all ip addresses on your system]'
637 ip = self.ip if self.ip else '[all ip addresses on your system]'
633 return self._url(ip)
638 return self._url(ip)
634
639
635 @property
640 @property
636 def connection_url(self):
641 def connection_url(self):
637 ip = self.ip if self.ip else localhost()
642 ip = self.ip if self.ip else localhost()
638 return self._url(ip)
643 return self._url(ip)
639
644
640 def _url(self, ip):
645 def _url(self, ip):
641 proto = 'https' if self.certfile else 'http'
646 proto = 'https' if self.certfile else 'http'
642 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_project_url)
647 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_url)
643
648
644 def init_signal(self):
649 def init_signal(self):
645 if not sys.platform.startswith('win'):
650 if not sys.platform.startswith('win'):
646 signal.signal(signal.SIGINT, self._handle_sigint)
651 signal.signal(signal.SIGINT, self._handle_sigint)
647 signal.signal(signal.SIGTERM, self._signal_stop)
652 signal.signal(signal.SIGTERM, self._signal_stop)
648 if hasattr(signal, 'SIGUSR1'):
653 if hasattr(signal, 'SIGUSR1'):
649 # Windows doesn't support SIGUSR1
654 # Windows doesn't support SIGUSR1
650 signal.signal(signal.SIGUSR1, self._signal_info)
655 signal.signal(signal.SIGUSR1, self._signal_info)
651 if hasattr(signal, 'SIGINFO'):
656 if hasattr(signal, 'SIGINFO'):
652 # only on BSD-based systems
657 # only on BSD-based systems
653 signal.signal(signal.SIGINFO, self._signal_info)
658 signal.signal(signal.SIGINFO, self._signal_info)
654
659
655 def _handle_sigint(self, sig, frame):
660 def _handle_sigint(self, sig, frame):
656 """SIGINT handler spawns confirmation dialog"""
661 """SIGINT handler spawns confirmation dialog"""
657 # register more forceful signal handler for ^C^C case
662 # register more forceful signal handler for ^C^C case
658 signal.signal(signal.SIGINT, self._signal_stop)
663 signal.signal(signal.SIGINT, self._signal_stop)
659 # request confirmation dialog in bg thread, to avoid
664 # request confirmation dialog in bg thread, to avoid
660 # blocking the App
665 # blocking the App
661 thread = threading.Thread(target=self._confirm_exit)
666 thread = threading.Thread(target=self._confirm_exit)
662 thread.daemon = True
667 thread.daemon = True
663 thread.start()
668 thread.start()
664
669
665 def _restore_sigint_handler(self):
670 def _restore_sigint_handler(self):
666 """callback for restoring original SIGINT handler"""
671 """callback for restoring original SIGINT handler"""
667 signal.signal(signal.SIGINT, self._handle_sigint)
672 signal.signal(signal.SIGINT, self._handle_sigint)
668
673
669 def _confirm_exit(self):
674 def _confirm_exit(self):
670 """confirm shutdown on ^C
675 """confirm shutdown on ^C
671
676
672 A second ^C, or answering 'y' within 5s will cause shutdown,
677 A second ^C, or answering 'y' within 5s will cause shutdown,
673 otherwise original SIGINT handler will be restored.
678 otherwise original SIGINT handler will be restored.
674
679
675 This doesn't work on Windows.
680 This doesn't work on Windows.
676 """
681 """
677 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
682 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
678 time.sleep(0.1)
683 time.sleep(0.1)
679 info = self.log.info
684 info = self.log.info
680 info('interrupted')
685 info('interrupted')
681 print(self.notebook_info())
686 print(self.notebook_info())
682 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
687 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
683 sys.stdout.flush()
688 sys.stdout.flush()
684 r,w,x = select.select([sys.stdin], [], [], 5)
689 r,w,x = select.select([sys.stdin], [], [], 5)
685 if r:
690 if r:
686 line = sys.stdin.readline()
691 line = sys.stdin.readline()
687 if line.lower().startswith('y'):
692 if line.lower().startswith('y'):
688 self.log.critical("Shutdown confirmed")
693 self.log.critical("Shutdown confirmed")
689 ioloop.IOLoop.instance().stop()
694 ioloop.IOLoop.instance().stop()
690 return
695 return
691 else:
696 else:
692 print("No answer for 5s:", end=' ')
697 print("No answer for 5s:", end=' ')
693 print("resuming operation...")
698 print("resuming operation...")
694 # no answer, or answer is no:
699 # no answer, or answer is no:
695 # set it back to original SIGINT handler
700 # set it back to original SIGINT handler
696 # use IOLoop.add_callback because signal.signal must be called
701 # use IOLoop.add_callback because signal.signal must be called
697 # from main thread
702 # from main thread
698 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
703 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
699
704
700 def _signal_stop(self, sig, frame):
705 def _signal_stop(self, sig, frame):
701 self.log.critical("received signal %s, stopping", sig)
706 self.log.critical("received signal %s, stopping", sig)
702 ioloop.IOLoop.instance().stop()
707 ioloop.IOLoop.instance().stop()
703
708
704 def _signal_info(self, sig, frame):
709 def _signal_info(self, sig, frame):
705 print(self.notebook_info())
710 print(self.notebook_info())
706
711
707 def init_components(self):
712 def init_components(self):
708 """Check the components submodule, and warn if it's unclean"""
713 """Check the components submodule, and warn if it's unclean"""
709 status = submodule.check_submodule_status()
714 status = submodule.check_submodule_status()
710 if status == 'missing':
715 if status == 'missing':
711 self.log.warn("components submodule missing, running `git submodule update`")
716 self.log.warn("components submodule missing, running `git submodule update`")
712 submodule.update_submodules(submodule.ipython_parent())
717 submodule.update_submodules(submodule.ipython_parent())
713 elif status == 'unclean':
718 elif status == 'unclean':
714 self.log.warn("components submodule unclean, you may see 404s on static/components")
719 self.log.warn("components submodule unclean, you may see 404s on static/components")
715 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
720 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
716
721
717 @catch_config_error
722 @catch_config_error
718 def initialize(self, argv=None):
723 def initialize(self, argv=None):
719 super(NotebookApp, self).initialize(argv)
724 super(NotebookApp, self).initialize(argv)
720 self.init_logging()
725 self.init_logging()
721 self.init_kernel_argv()
726 self.init_kernel_argv()
722 self.init_configurables()
727 self.init_configurables()
723 self.init_components()
728 self.init_components()
724 self.init_webapp()
729 self.init_webapp()
725 self.init_signal()
730 self.init_signal()
726
731
727 def cleanup_kernels(self):
732 def cleanup_kernels(self):
728 """Shutdown all kernels.
733 """Shutdown all kernels.
729
734
730 The kernels will shutdown themselves when this process no longer exists,
735 The kernels will shutdown themselves when this process no longer exists,
731 but explicit shutdown allows the KernelManagers to cleanup the connection files.
736 but explicit shutdown allows the KernelManagers to cleanup the connection files.
732 """
737 """
733 self.log.info('Shutting down kernels')
738 self.log.info('Shutting down kernels')
734 self.kernel_manager.shutdown_all()
739 self.kernel_manager.shutdown_all()
735
740
736 def notebook_info(self):
741 def notebook_info(self):
737 "Return the current working directory and the server url information"
742 "Return the current working directory and the server url information"
738 info = self.notebook_manager.info_string() + "\n"
743 info = self.notebook_manager.info_string() + "\n"
739 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
744 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
740 return info + "The IPython Notebook is running at: %s" % self.display_url
745 return info + "The IPython Notebook is running at: %s" % self.display_url
741
746
742 def server_info(self):
747 def server_info(self):
743 """Return a JSONable dict of information about this server."""
748 """Return a JSONable dict of information about this server."""
744 return {'url': self.connection_url,
749 return {'url': self.connection_url,
745 'hostname': self.ip if self.ip else 'localhost',
750 'hostname': self.ip if self.ip else 'localhost',
746 'port': self.port,
751 'port': self.port,
747 'secure': bool(self.certfile),
752 'secure': bool(self.certfile),
748 'base_project_url': self.base_project_url,
753 'base_url': self.base_url,
749 'notebook_dir': os.path.abspath(self.notebook_manager.notebook_dir),
754 'notebook_dir': os.path.abspath(self.notebook_manager.notebook_dir),
750 }
755 }
751
756
752 def write_server_info_file(self):
757 def write_server_info_file(self):
753 """Write the result of server_info() to the JSON file info_file."""
758 """Write the result of server_info() to the JSON file info_file."""
754 with open(self.info_file, 'w') as f:
759 with open(self.info_file, 'w') as f:
755 json.dump(self.server_info(), f, indent=2)
760 json.dump(self.server_info(), f, indent=2)
756
761
757 def remove_server_info_file(self):
762 def remove_server_info_file(self):
758 """Remove the nbserver-<pid>.json file created for this server.
763 """Remove the nbserver-<pid>.json file created for this server.
759
764
760 Ignores the error raised when the file has already been removed.
765 Ignores the error raised when the file has already been removed.
761 """
766 """
762 try:
767 try:
763 os.unlink(self.info_file)
768 os.unlink(self.info_file)
764 except OSError as e:
769 except OSError as e:
765 if e.errno != errno.ENOENT:
770 if e.errno != errno.ENOENT:
766 raise
771 raise
767
772
768 def start(self):
773 def start(self):
769 """ Start the IPython Notebook server app, after initialization
774 """ Start the IPython Notebook server app, after initialization
770
775
771 This method takes no arguments so all configuration and initialization
776 This method takes no arguments so all configuration and initialization
772 must be done prior to calling this method."""
777 must be done prior to calling this method."""
773 if self.subapp is not None:
778 if self.subapp is not None:
774 return self.subapp.start()
779 return self.subapp.start()
775
780
776 info = self.log.info
781 info = self.log.info
777 for line in self.notebook_info().split("\n"):
782 for line in self.notebook_info().split("\n"):
778 info(line)
783 info(line)
779 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
784 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
780
785
781 self.write_server_info_file()
786 self.write_server_info_file()
782
787
783 if self.open_browser or self.file_to_run:
788 if self.open_browser or self.file_to_run:
784 try:
789 try:
785 browser = webbrowser.get(self.browser or None)
790 browser = webbrowser.get(self.browser or None)
786 except webbrowser.Error as e:
791 except webbrowser.Error as e:
787 self.log.warn('No web browser found: %s.' % e)
792 self.log.warn('No web browser found: %s.' % e)
788 browser = None
793 browser = None
789
794
790 f = self.file_to_run
795 f = self.file_to_run
791 if f:
796 if f:
792 nbdir = os.path.abspath(self.notebook_manager.notebook_dir)
797 nbdir = os.path.abspath(self.notebook_manager.notebook_dir)
793 if f.startswith(nbdir):
798 if f.startswith(nbdir):
794 f = f[len(nbdir):]
799 f = f[len(nbdir):]
795 else:
800 else:
796 self.log.warn(
801 self.log.warn(
797 "Probably won't be able to open notebook %s "
802 "Probably won't be able to open notebook %s "
798 "because it is not in notebook_dir %s",
803 "because it is not in notebook_dir %s",
799 f, nbdir,
804 f, nbdir,
800 )
805 )
801
806
802 if os.path.isfile(self.file_to_run):
807 if os.path.isfile(self.file_to_run):
803 url = url_path_join('notebooks', f)
808 url = url_path_join('notebooks', f)
804 else:
809 else:
805 url = url_path_join('tree', f)
810 url = url_path_join('tree', f)
806 if browser:
811 if browser:
807 b = lambda : browser.open("%s%s" % (self.connection_url, url),
812 b = lambda : browser.open("%s%s" % (self.connection_url, url),
808 new=2)
813 new=2)
809 threading.Thread(target=b).start()
814 threading.Thread(target=b).start()
810 try:
815 try:
811 ioloop.IOLoop.instance().start()
816 ioloop.IOLoop.instance().start()
812 except KeyboardInterrupt:
817 except KeyboardInterrupt:
813 info("Interrupted...")
818 info("Interrupted...")
814 finally:
819 finally:
815 self.cleanup_kernels()
820 self.cleanup_kernels()
816 self.remove_server_info_file()
821 self.remove_server_info_file()
817
822
818
823
819 def list_running_servers(profile='default'):
824 def list_running_servers(profile='default'):
820 """Iterate over the server info files of running notebook servers.
825 """Iterate over the server info files of running notebook servers.
821
826
822 Given a profile name, find nbserver-* files in the security directory of
827 Given a profile name, find nbserver-* files in the security directory of
823 that profile, and yield dicts of their information, each one pertaining to
828 that profile, and yield dicts of their information, each one pertaining to
824 a currently running notebook server instance.
829 a currently running notebook server instance.
825 """
830 """
826 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile)
831 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile)
827 for file in os.listdir(pd.security_dir):
832 for file in os.listdir(pd.security_dir):
828 if file.startswith('nbserver-'):
833 if file.startswith('nbserver-'):
829 with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f:
834 with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f:
830 yield json.load(f)
835 yield json.load(f)
831
836
832 #-----------------------------------------------------------------------------
837 #-----------------------------------------------------------------------------
833 # Main entry point
838 # Main entry point
834 #-----------------------------------------------------------------------------
839 #-----------------------------------------------------------------------------
835
840
836 launch_new_instance = NotebookApp.launch_instance
841 launch_new_instance = NotebookApp.launch_instance
837
842
@@ -1,290 +1,290 b''
1 """Tornado handlers for the notebooks web service.
1 """Tornado handlers for the notebooks web service.
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2011 The IPython Development Team
9 # Copyright (C) 2011 The IPython Development Team
10 #
10 #
11 # Distributed under the terms of the BSD License. The full license is in
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19 import json
19 import json
20
20
21 from tornado import web
21 from tornado import web
22
22
23 from IPython.html.utils import url_path_join, url_escape
23 from IPython.html.utils import url_path_join, url_escape
24 from IPython.utils.jsonutil import date_default
24 from IPython.utils.jsonutil import date_default
25
25
26 from IPython.html.base.handlers import (IPythonHandler, json_errors,
26 from IPython.html.base.handlers import (IPythonHandler, json_errors,
27 notebook_path_regex, path_regex,
27 notebook_path_regex, path_regex,
28 notebook_name_regex)
28 notebook_name_regex)
29
29
30 #-----------------------------------------------------------------------------
30 #-----------------------------------------------------------------------------
31 # Notebook web service handlers
31 # Notebook web service handlers
32 #-----------------------------------------------------------------------------
32 #-----------------------------------------------------------------------------
33
33
34
34
35 class NotebookHandler(IPythonHandler):
35 class NotebookHandler(IPythonHandler):
36
36
37 SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE')
37 SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE')
38
38
39 def notebook_location(self, name, path=''):
39 def notebook_location(self, name, path=''):
40 """Return the full URL location of a notebook based.
40 """Return the full URL location of a notebook based.
41
41
42 Parameters
42 Parameters
43 ----------
43 ----------
44 name : unicode
44 name : unicode
45 The base name of the notebook, such as "foo.ipynb".
45 The base name of the notebook, such as "foo.ipynb".
46 path : unicode
46 path : unicode
47 The URL path of the notebook.
47 The URL path of the notebook.
48 """
48 """
49 return url_escape(url_path_join(
49 return url_escape(url_path_join(
50 self.base_project_url, 'api', 'notebooks', path, name
50 self.base_url, 'api', 'notebooks', path, name
51 ))
51 ))
52
52
53 def _finish_model(self, model, location=True):
53 def _finish_model(self, model, location=True):
54 """Finish a JSON request with a model, setting relevant headers, etc."""
54 """Finish a JSON request with a model, setting relevant headers, etc."""
55 if location:
55 if location:
56 location = self.notebook_location(model['name'], model['path'])
56 location = self.notebook_location(model['name'], model['path'])
57 self.set_header('Location', location)
57 self.set_header('Location', location)
58 self.set_header('Last-Modified', model['last_modified'])
58 self.set_header('Last-Modified', model['last_modified'])
59 self.finish(json.dumps(model, default=date_default))
59 self.finish(json.dumps(model, default=date_default))
60
60
61 @web.authenticated
61 @web.authenticated
62 @json_errors
62 @json_errors
63 def get(self, path='', name=None):
63 def get(self, path='', name=None):
64 """Return a Notebook or list of notebooks.
64 """Return a Notebook or list of notebooks.
65
65
66 * GET with path and no notebook name lists notebooks in a directory
66 * GET with path and no notebook name lists notebooks in a directory
67 * GET with path and notebook name returns notebook JSON
67 * GET with path and notebook name returns notebook JSON
68 """
68 """
69 nbm = self.notebook_manager
69 nbm = self.notebook_manager
70 # Check to see if a notebook name was given
70 # Check to see if a notebook name was given
71 if name is None:
71 if name is None:
72 # TODO: Remove this after we create the contents web service and directories are
72 # TODO: Remove this after we create the contents web service and directories are
73 # no longer listed by the notebook web service. This should only handle notebooks
73 # no longer listed by the notebook web service. This should only handle notebooks
74 # and not directories.
74 # and not directories.
75 dirs = nbm.list_dirs(path)
75 dirs = nbm.list_dirs(path)
76 notebooks = []
76 notebooks = []
77 index = []
77 index = []
78 for nb in nbm.list_notebooks(path):
78 for nb in nbm.list_notebooks(path):
79 if nb['name'].lower() == 'index.ipynb':
79 if nb['name'].lower() == 'index.ipynb':
80 index.append(nb)
80 index.append(nb)
81 else:
81 else:
82 notebooks.append(nb)
82 notebooks.append(nb)
83 notebooks = index + dirs + notebooks
83 notebooks = index + dirs + notebooks
84 self.finish(json.dumps(notebooks, default=date_default))
84 self.finish(json.dumps(notebooks, default=date_default))
85 return
85 return
86 # get and return notebook representation
86 # get and return notebook representation
87 model = nbm.get_notebook_model(name, path)
87 model = nbm.get_notebook_model(name, path)
88 self._finish_model(model, location=False)
88 self._finish_model(model, location=False)
89
89
90 @web.authenticated
90 @web.authenticated
91 @json_errors
91 @json_errors
92 def patch(self, path='', name=None):
92 def patch(self, path='', name=None):
93 """PATCH renames a notebook without re-uploading content."""
93 """PATCH renames a notebook without re-uploading content."""
94 nbm = self.notebook_manager
94 nbm = self.notebook_manager
95 if name is None:
95 if name is None:
96 raise web.HTTPError(400, u'Notebook name missing')
96 raise web.HTTPError(400, u'Notebook name missing')
97 model = self.get_json_body()
97 model = self.get_json_body()
98 if model is None:
98 if model is None:
99 raise web.HTTPError(400, u'JSON body missing')
99 raise web.HTTPError(400, u'JSON body missing')
100 model = nbm.update_notebook_model(model, name, path)
100 model = nbm.update_notebook_model(model, name, path)
101 self._finish_model(model)
101 self._finish_model(model)
102
102
103 def _copy_notebook(self, copy_from, path, copy_to=None):
103 def _copy_notebook(self, copy_from, path, copy_to=None):
104 """Copy a notebook in path, optionally specifying the new name.
104 """Copy a notebook in path, optionally specifying the new name.
105
105
106 Only support copying within the same directory.
106 Only support copying within the same directory.
107 """
107 """
108 self.log.info(u"Copying notebook from %s/%s to %s/%s",
108 self.log.info(u"Copying notebook from %s/%s to %s/%s",
109 path, copy_from,
109 path, copy_from,
110 path, copy_to or '',
110 path, copy_to or '',
111 )
111 )
112 model = self.notebook_manager.copy_notebook(copy_from, copy_to, path)
112 model = self.notebook_manager.copy_notebook(copy_from, copy_to, path)
113 self.set_status(201)
113 self.set_status(201)
114 self._finish_model(model)
114 self._finish_model(model)
115
115
116 def _upload_notebook(self, model, path, name=None):
116 def _upload_notebook(self, model, path, name=None):
117 """Upload a notebook
117 """Upload a notebook
118
118
119 If name specified, create it in path/name.
119 If name specified, create it in path/name.
120 """
120 """
121 self.log.info(u"Uploading notebook to %s/%s", path, name or '')
121 self.log.info(u"Uploading notebook to %s/%s", path, name or '')
122 if name:
122 if name:
123 model['name'] = name
123 model['name'] = name
124
124
125 model = self.notebook_manager.create_notebook_model(model, path)
125 model = self.notebook_manager.create_notebook_model(model, path)
126 self.set_status(201)
126 self.set_status(201)
127 self._finish_model(model)
127 self._finish_model(model)
128
128
129 def _create_empty_notebook(self, path, name=None):
129 def _create_empty_notebook(self, path, name=None):
130 """Create an empty notebook in path
130 """Create an empty notebook in path
131
131
132 If name specified, create it in path/name.
132 If name specified, create it in path/name.
133 """
133 """
134 self.log.info(u"Creating new notebook in %s/%s", path, name or '')
134 self.log.info(u"Creating new notebook in %s/%s", path, name or '')
135 model = {}
135 model = {}
136 if name:
136 if name:
137 model['name'] = name
137 model['name'] = name
138 model = self.notebook_manager.create_notebook_model(model, path=path)
138 model = self.notebook_manager.create_notebook_model(model, path=path)
139 self.set_status(201)
139 self.set_status(201)
140 self._finish_model(model)
140 self._finish_model(model)
141
141
142 def _save_notebook(self, model, path, name):
142 def _save_notebook(self, model, path, name):
143 """Save an existing notebook."""
143 """Save an existing notebook."""
144 self.log.info(u"Saving notebook at %s/%s", path, name)
144 self.log.info(u"Saving notebook at %s/%s", path, name)
145 model = self.notebook_manager.save_notebook_model(model, name, path)
145 model = self.notebook_manager.save_notebook_model(model, name, path)
146 if model['path'] != path.strip('/') or model['name'] != name:
146 if model['path'] != path.strip('/') or model['name'] != name:
147 # a rename happened, set Location header
147 # a rename happened, set Location header
148 location = True
148 location = True
149 else:
149 else:
150 location = False
150 location = False
151 self._finish_model(model, location)
151 self._finish_model(model, location)
152
152
153 @web.authenticated
153 @web.authenticated
154 @json_errors
154 @json_errors
155 def post(self, path='', name=None):
155 def post(self, path='', name=None):
156 """Create a new notebook in the specified path.
156 """Create a new notebook in the specified path.
157
157
158 POST creates new notebooks. The server always decides on the notebook name.
158 POST creates new notebooks. The server always decides on the notebook name.
159
159
160 POST /api/notebooks/path
160 POST /api/notebooks/path
161 New untitled notebook in path. If content specified, upload a
161 New untitled notebook in path. If content specified, upload a
162 notebook, otherwise start empty.
162 notebook, otherwise start empty.
163 POST /api/notebooks/path?copy=OtherNotebook.ipynb
163 POST /api/notebooks/path?copy=OtherNotebook.ipynb
164 New copy of OtherNotebook in path
164 New copy of OtherNotebook in path
165 """
165 """
166
166
167 if name is not None:
167 if name is not None:
168 raise web.HTTPError(400, "Only POST to directories. Use PUT for full names.")
168 raise web.HTTPError(400, "Only POST to directories. Use PUT for full names.")
169
169
170 model = self.get_json_body()
170 model = self.get_json_body()
171
171
172 if model is not None:
172 if model is not None:
173 copy_from = model.get('copy_from')
173 copy_from = model.get('copy_from')
174 if copy_from:
174 if copy_from:
175 if model.get('content'):
175 if model.get('content'):
176 raise web.HTTPError(400, "Can't upload and copy at the same time.")
176 raise web.HTTPError(400, "Can't upload and copy at the same time.")
177 self._copy_notebook(copy_from, path)
177 self._copy_notebook(copy_from, path)
178 else:
178 else:
179 self._upload_notebook(model, path)
179 self._upload_notebook(model, path)
180 else:
180 else:
181 self._create_empty_notebook(path)
181 self._create_empty_notebook(path)
182
182
183 @web.authenticated
183 @web.authenticated
184 @json_errors
184 @json_errors
185 def put(self, path='', name=None):
185 def put(self, path='', name=None):
186 """Saves the notebook in the location specified by name and path.
186 """Saves the notebook in the location specified by name and path.
187
187
188 PUT is very similar to POST, but the requester specifies the name,
188 PUT is very similar to POST, but the requester specifies the name,
189 whereas with POST, the server picks the name.
189 whereas with POST, the server picks the name.
190
190
191 PUT /api/notebooks/path/Name.ipynb
191 PUT /api/notebooks/path/Name.ipynb
192 Save notebook at ``path/Name.ipynb``. Notebook structure is specified
192 Save notebook at ``path/Name.ipynb``. Notebook structure is specified
193 in `content` key of JSON request body. If content is not specified,
193 in `content` key of JSON request body. If content is not specified,
194 create a new empty notebook.
194 create a new empty notebook.
195 PUT /api/notebooks/path/Name.ipynb?copy=OtherNotebook.ipynb
195 PUT /api/notebooks/path/Name.ipynb?copy=OtherNotebook.ipynb
196 Copy OtherNotebook to Name
196 Copy OtherNotebook to Name
197 """
197 """
198 if name is None:
198 if name is None:
199 raise web.HTTPError(400, "Only PUT to full names. Use POST for directories.")
199 raise web.HTTPError(400, "Only PUT to full names. Use POST for directories.")
200
200
201 model = self.get_json_body()
201 model = self.get_json_body()
202 if model:
202 if model:
203 copy_from = model.get('copy_from')
203 copy_from = model.get('copy_from')
204 if copy_from:
204 if copy_from:
205 if model.get('content'):
205 if model.get('content'):
206 raise web.HTTPError(400, "Can't upload and copy at the same time.")
206 raise web.HTTPError(400, "Can't upload and copy at the same time.")
207 self._copy_notebook(copy_from, path, name)
207 self._copy_notebook(copy_from, path, name)
208 elif self.notebook_manager.notebook_exists(name, path):
208 elif self.notebook_manager.notebook_exists(name, path):
209 self._save_notebook(model, path, name)
209 self._save_notebook(model, path, name)
210 else:
210 else:
211 self._upload_notebook(model, path, name)
211 self._upload_notebook(model, path, name)
212 else:
212 else:
213 self._create_empty_notebook(path, name)
213 self._create_empty_notebook(path, name)
214
214
215 @web.authenticated
215 @web.authenticated
216 @json_errors
216 @json_errors
217 def delete(self, path='', name=None):
217 def delete(self, path='', name=None):
218 """delete the notebook in the given notebook path"""
218 """delete the notebook in the given notebook path"""
219 nbm = self.notebook_manager
219 nbm = self.notebook_manager
220 nbm.delete_notebook_model(name, path)
220 nbm.delete_notebook_model(name, path)
221 self.set_status(204)
221 self.set_status(204)
222 self.finish()
222 self.finish()
223
223
224
224
225 class NotebookCheckpointsHandler(IPythonHandler):
225 class NotebookCheckpointsHandler(IPythonHandler):
226
226
227 SUPPORTED_METHODS = ('GET', 'POST')
227 SUPPORTED_METHODS = ('GET', 'POST')
228
228
229 @web.authenticated
229 @web.authenticated
230 @json_errors
230 @json_errors
231 def get(self, path='', name=None):
231 def get(self, path='', name=None):
232 """get lists checkpoints for a notebook"""
232 """get lists checkpoints for a notebook"""
233 nbm = self.notebook_manager
233 nbm = self.notebook_manager
234 checkpoints = nbm.list_checkpoints(name, path)
234 checkpoints = nbm.list_checkpoints(name, path)
235 data = json.dumps(checkpoints, default=date_default)
235 data = json.dumps(checkpoints, default=date_default)
236 self.finish(data)
236 self.finish(data)
237
237
238 @web.authenticated
238 @web.authenticated
239 @json_errors
239 @json_errors
240 def post(self, path='', name=None):
240 def post(self, path='', name=None):
241 """post creates a new checkpoint"""
241 """post creates a new checkpoint"""
242 nbm = self.notebook_manager
242 nbm = self.notebook_manager
243 checkpoint = nbm.create_checkpoint(name, path)
243 checkpoint = nbm.create_checkpoint(name, path)
244 data = json.dumps(checkpoint, default=date_default)
244 data = json.dumps(checkpoint, default=date_default)
245 location = url_path_join(self.base_project_url, 'api/notebooks',
245 location = url_path_join(self.base_url, 'api/notebooks',
246 path, name, 'checkpoints', checkpoint['id'])
246 path, name, 'checkpoints', checkpoint['id'])
247 self.set_header('Location', url_escape(location))
247 self.set_header('Location', url_escape(location))
248 self.set_status(201)
248 self.set_status(201)
249 self.finish(data)
249 self.finish(data)
250
250
251
251
252 class ModifyNotebookCheckpointsHandler(IPythonHandler):
252 class ModifyNotebookCheckpointsHandler(IPythonHandler):
253
253
254 SUPPORTED_METHODS = ('POST', 'DELETE')
254 SUPPORTED_METHODS = ('POST', 'DELETE')
255
255
256 @web.authenticated
256 @web.authenticated
257 @json_errors
257 @json_errors
258 def post(self, path, name, checkpoint_id):
258 def post(self, path, name, checkpoint_id):
259 """post restores a notebook from a checkpoint"""
259 """post restores a notebook from a checkpoint"""
260 nbm = self.notebook_manager
260 nbm = self.notebook_manager
261 nbm.restore_checkpoint(checkpoint_id, name, path)
261 nbm.restore_checkpoint(checkpoint_id, name, path)
262 self.set_status(204)
262 self.set_status(204)
263 self.finish()
263 self.finish()
264
264
265 @web.authenticated
265 @web.authenticated
266 @json_errors
266 @json_errors
267 def delete(self, path, name, checkpoint_id):
267 def delete(self, path, name, checkpoint_id):
268 """delete clears a checkpoint for a given notebook"""
268 """delete clears a checkpoint for a given notebook"""
269 nbm = self.notebook_manager
269 nbm = self.notebook_manager
270 nbm.delete_checkpoint(checkpoint_id, name, path)
270 nbm.delete_checkpoint(checkpoint_id, name, path)
271 self.set_status(204)
271 self.set_status(204)
272 self.finish()
272 self.finish()
273
273
274 #-----------------------------------------------------------------------------
274 #-----------------------------------------------------------------------------
275 # URL to handler mappings
275 # URL to handler mappings
276 #-----------------------------------------------------------------------------
276 #-----------------------------------------------------------------------------
277
277
278
278
279 _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
279 _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
280
280
281 default_handlers = [
281 default_handlers = [
282 (r"/api/notebooks%s/checkpoints" % notebook_path_regex, NotebookCheckpointsHandler),
282 (r"/api/notebooks%s/checkpoints" % notebook_path_regex, NotebookCheckpointsHandler),
283 (r"/api/notebooks%s/checkpoints/%s" % (notebook_path_regex, _checkpoint_id_regex),
283 (r"/api/notebooks%s/checkpoints/%s" % (notebook_path_regex, _checkpoint_id_regex),
284 ModifyNotebookCheckpointsHandler),
284 ModifyNotebookCheckpointsHandler),
285 (r"/api/notebooks%s" % notebook_path_regex, NotebookHandler),
285 (r"/api/notebooks%s" % notebook_path_regex, NotebookHandler),
286 (r"/api/notebooks%s" % path_regex, NotebookHandler),
286 (r"/api/notebooks%s" % path_regex, NotebookHandler),
287 ]
287 ]
288
288
289
289
290
290
@@ -1,45 +1,52 b''
1 //----------------------------------------------------------------------------
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2008-2011 The IPython Development Team
2 // Copyright (C) 2008-2011 The IPython Development Team
3 //
3 //
4 // Distributed under the terms of the BSD License. The full license is in
4 // Distributed under the terms of the BSD License. The full license is in
5 // the file COPYING, distributed as part of this software.
5 // the file COPYING, distributed as part of this software.
6 //----------------------------------------------------------------------------
6 //----------------------------------------------------------------------------
7
7
8 //============================================================================
8 //============================================================================
9 // Login button
9 // Login button
10 //============================================================================
10 //============================================================================
11
11
12 var IPython = (function (IPython) {
12 var IPython = (function (IPython) {
13 "use strict";
13
14
14 var LoginWidget = function (selector, options) {
15 var LoginWidget = function (selector, options) {
15 var options = options || {};
16 options = options || {};
16 this.base_url = options.baseProjectUrl ||Β $('body').data('baseProjectUrl') ;
17 this.base_url = options.base_url || IPython.utils.get_body_data("baseUrl");
17 this.selector = selector;
18 this.selector = selector;
18 if (this.selector !== undefined) {
19 if (this.selector !== undefined) {
19 this.element = $(selector);
20 this.element = $(selector);
20 this.style();
21 this.style();
21 this.bind_events();
22 this.bind_events();
22 }
23 }
23 };
24 };
24
25
25 LoginWidget.prototype.style = function () {
26 LoginWidget.prototype.style = function () {
26 this.element.find("button").addClass("btn btn-small");
27 this.element.find("button").addClass("btn btn-small");
27 };
28 };
28
29
29
30
30 LoginWidget.prototype.bind_events = function () {
31 LoginWidget.prototype.bind_events = function () {
31 var that = this;
32 var that = this;
32 this.element.find("button#logout").click(function () {
33 this.element.find("button#logout").click(function () {
33 window.location = that.base_url+"logout";
34 window.location = IPythin.utils.url_join_encode(
35 that.base_url,
36 "logout"
37 );
34 });
38 });
35 this.element.find("button#login").click(function () {
39 this.element.find("button#login").click(function () {
36 window.location = that.base_url+"login";
40 window.location = IPythin.utils.url_join_encode(
41 that.base_url,
42 "login"
43 );
37 });
44 });
38 };
45 };
39
46
40 // Set module variables
47 // Set module variables
41 IPython.LoginWidget = LoginWidget;
48 IPython.LoginWidget = LoginWidget;
42
49
43 return IPython;
50 return IPython;
44
51
45 }(IPython));
52 }(IPython));
@@ -1,523 +1,547 b''
1 //----------------------------------------------------------------------------
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2008-2012 The IPython Development Team
2 // Copyright (C) 2008-2012 The IPython Development Team
3 //
3 //
4 // Distributed under the terms of the BSD License. The full license is in
4 // Distributed under the terms of the BSD License. The full license is in
5 // the file COPYING, distributed as part of this software.
5 // the file COPYING, distributed as part of this software.
6 //----------------------------------------------------------------------------
6 //----------------------------------------------------------------------------
7
7
8 //============================================================================
8 //============================================================================
9 // Utilities
9 // Utilities
10 //============================================================================
10 //============================================================================
11 IPython.namespace('IPython.utils');
11 IPython.namespace('IPython.utils');
12
12
13 IPython.utils = (function (IPython) {
13 IPython.utils = (function (IPython) {
14 "use strict";
14 "use strict";
15
15
16 //============================================================================
16 //============================================================================
17 // Cross-browser RegEx Split
17 // Cross-browser RegEx Split
18 //============================================================================
18 //============================================================================
19
19
20 // This code has been MODIFIED from the code licensed below to not replace the
20 // This code has been MODIFIED from the code licensed below to not replace the
21 // default browser split. The license is reproduced here.
21 // default browser split. The license is reproduced here.
22
22
23 // see http://blog.stevenlevithan.com/archives/cross-browser-split for more info:
23 // see http://blog.stevenlevithan.com/archives/cross-browser-split for more info:
24 /*!
24 /*!
25 * Cross-Browser Split 1.1.1
25 * Cross-Browser Split 1.1.1
26 * Copyright 2007-2012 Steven Levithan <stevenlevithan.com>
26 * Copyright 2007-2012 Steven Levithan <stevenlevithan.com>
27 * Available under the MIT License
27 * Available under the MIT License
28 * ECMAScript compliant, uniform cross-browser split method
28 * ECMAScript compliant, uniform cross-browser split method
29 */
29 */
30
30
31 /**
31 /**
32 * Splits a string into an array of strings using a regex or string
32 * Splits a string into an array of strings using a regex or string
33 * separator. Matches of the separator are not included in the result array.
33 * separator. Matches of the separator are not included in the result array.
34 * However, if `separator` is a regex that contains capturing groups,
34 * However, if `separator` is a regex that contains capturing groups,
35 * backreferences are spliced into the result each time `separator` is
35 * backreferences are spliced into the result each time `separator` is
36 * matched. Fixes browser bugs compared to the native
36 * matched. Fixes browser bugs compared to the native
37 * `String.prototype.split` and can be used reliably cross-browser.
37 * `String.prototype.split` and can be used reliably cross-browser.
38 * @param {String} str String to split.
38 * @param {String} str String to split.
39 * @param {RegExp|String} separator Regex or string to use for separating
39 * @param {RegExp|String} separator Regex or string to use for separating
40 * the string.
40 * the string.
41 * @param {Number} [limit] Maximum number of items to include in the result
41 * @param {Number} [limit] Maximum number of items to include in the result
42 * array.
42 * array.
43 * @returns {Array} Array of substrings.
43 * @returns {Array} Array of substrings.
44 * @example
44 * @example
45 *
45 *
46 * // Basic use
46 * // Basic use
47 * regex_split('a b c d', ' ');
47 * regex_split('a b c d', ' ');
48 * // -> ['a', 'b', 'c', 'd']
48 * // -> ['a', 'b', 'c', 'd']
49 *
49 *
50 * // With limit
50 * // With limit
51 * regex_split('a b c d', ' ', 2);
51 * regex_split('a b c d', ' ', 2);
52 * // -> ['a', 'b']
52 * // -> ['a', 'b']
53 *
53 *
54 * // Backreferences in result array
54 * // Backreferences in result array
55 * regex_split('..word1 word2..', /([a-z]+)(\d+)/i);
55 * regex_split('..word1 word2..', /([a-z]+)(\d+)/i);
56 * // -> ['..', 'word', '1', ' ', 'word', '2', '..']
56 * // -> ['..', 'word', '1', ' ', 'word', '2', '..']
57 */
57 */
58 var regex_split = function (str, separator, limit) {
58 var regex_split = function (str, separator, limit) {
59 // If `separator` is not a regex, use `split`
59 // If `separator` is not a regex, use `split`
60 if (Object.prototype.toString.call(separator) !== "[object RegExp]") {
60 if (Object.prototype.toString.call(separator) !== "[object RegExp]") {
61 return split.call(str, separator, limit);
61 return split.call(str, separator, limit);
62 }
62 }
63 var output = [],
63 var output = [],
64 flags = (separator.ignoreCase ? "i" : "") +
64 flags = (separator.ignoreCase ? "i" : "") +
65 (separator.multiline ? "m" : "") +
65 (separator.multiline ? "m" : "") +
66 (separator.extended ? "x" : "") + // Proposed for ES6
66 (separator.extended ? "x" : "") + // Proposed for ES6
67 (separator.sticky ? "y" : ""), // Firefox 3+
67 (separator.sticky ? "y" : ""), // Firefox 3+
68 lastLastIndex = 0,
68 lastLastIndex = 0,
69 // Make `global` and avoid `lastIndex` issues by working with a copy
69 // Make `global` and avoid `lastIndex` issues by working with a copy
70 separator = new RegExp(separator.source, flags + "g"),
70 separator = new RegExp(separator.source, flags + "g"),
71 separator2, match, lastIndex, lastLength;
71 separator2, match, lastIndex, lastLength;
72 str += ""; // Type-convert
72 str += ""; // Type-convert
73
73
74 var compliantExecNpcg = typeof(/()??/.exec("")[1]) === "undefined";
74 var compliantExecNpcg = typeof(/()??/.exec("")[1]) === "undefined";
75 if (!compliantExecNpcg) {
75 if (!compliantExecNpcg) {
76 // Doesn't need flags gy, but they don't hurt
76 // Doesn't need flags gy, but they don't hurt
77 separator2 = new RegExp("^" + separator.source + "$(?!\\s)", flags);
77 separator2 = new RegExp("^" + separator.source + "$(?!\\s)", flags);
78 }
78 }
79 /* Values for `limit`, per the spec:
79 /* Values for `limit`, per the spec:
80 * If undefined: 4294967295 // Math.pow(2, 32) - 1
80 * If undefined: 4294967295 // Math.pow(2, 32) - 1
81 * If 0, Infinity, or NaN: 0
81 * If 0, Infinity, or NaN: 0
82 * If positive number: limit = Math.floor(limit); if (limit > 4294967295) limit -= 4294967296;
82 * If positive number: limit = Math.floor(limit); if (limit > 4294967295) limit -= 4294967296;
83 * If negative number: 4294967296 - Math.floor(Math.abs(limit))
83 * If negative number: 4294967296 - Math.floor(Math.abs(limit))
84 * If other: Type-convert, then use the above rules
84 * If other: Type-convert, then use the above rules
85 */
85 */
86 limit = typeof(limit) === "undefined" ?
86 limit = typeof(limit) === "undefined" ?
87 -1 >>> 0 : // Math.pow(2, 32) - 1
87 -1 >>> 0 : // Math.pow(2, 32) - 1
88 limit >>> 0; // ToUint32(limit)
88 limit >>> 0; // ToUint32(limit)
89 while (match = separator.exec(str)) {
89 while (match = separator.exec(str)) {
90 // `separator.lastIndex` is not reliable cross-browser
90 // `separator.lastIndex` is not reliable cross-browser
91 lastIndex = match.index + match[0].length;
91 lastIndex = match.index + match[0].length;
92 if (lastIndex > lastLastIndex) {
92 if (lastIndex > lastLastIndex) {
93 output.push(str.slice(lastLastIndex, match.index));
93 output.push(str.slice(lastLastIndex, match.index));
94 // Fix browsers whose `exec` methods don't consistently return `undefined` for
94 // Fix browsers whose `exec` methods don't consistently return `undefined` for
95 // nonparticipating capturing groups
95 // nonparticipating capturing groups
96 if (!compliantExecNpcg && match.length > 1) {
96 if (!compliantExecNpcg && match.length > 1) {
97 match[0].replace(separator2, function () {
97 match[0].replace(separator2, function () {
98 for (var i = 1; i < arguments.length - 2; i++) {
98 for (var i = 1; i < arguments.length - 2; i++) {
99 if (typeof(arguments[i]) === "undefined") {
99 if (typeof(arguments[i]) === "undefined") {
100 match[i] = undefined;
100 match[i] = undefined;
101 }
101 }
102 }
102 }
103 });
103 });
104 }
104 }
105 if (match.length > 1 && match.index < str.length) {
105 if (match.length > 1 && match.index < str.length) {
106 Array.prototype.push.apply(output, match.slice(1));
106 Array.prototype.push.apply(output, match.slice(1));
107 }
107 }
108 lastLength = match[0].length;
108 lastLength = match[0].length;
109 lastLastIndex = lastIndex;
109 lastLastIndex = lastIndex;
110 if (output.length >= limit) {
110 if (output.length >= limit) {
111 break;
111 break;
112 }
112 }
113 }
113 }
114 if (separator.lastIndex === match.index) {
114 if (separator.lastIndex === match.index) {
115 separator.lastIndex++; // Avoid an infinite loop
115 separator.lastIndex++; // Avoid an infinite loop
116 }
116 }
117 }
117 }
118 if (lastLastIndex === str.length) {
118 if (lastLastIndex === str.length) {
119 if (lastLength || !separator.test("")) {
119 if (lastLength || !separator.test("")) {
120 output.push("");
120 output.push("");
121 }
121 }
122 } else {
122 } else {
123 output.push(str.slice(lastLastIndex));
123 output.push(str.slice(lastLastIndex));
124 }
124 }
125 return output.length > limit ? output.slice(0, limit) : output;
125 return output.length > limit ? output.slice(0, limit) : output;
126 };
126 };
127
127
128 //============================================================================
128 //============================================================================
129 // End contributed Cross-browser RegEx Split
129 // End contributed Cross-browser RegEx Split
130 //============================================================================
130 //============================================================================
131
131
132
132
133 var uuid = function () {
133 var uuid = function () {
134 // http://www.ietf.org/rfc/rfc4122.txt
134 // http://www.ietf.org/rfc/rfc4122.txt
135 var s = [];
135 var s = [];
136 var hexDigits = "0123456789ABCDEF";
136 var hexDigits = "0123456789ABCDEF";
137 for (var i = 0; i < 32; i++) {
137 for (var i = 0; i < 32; i++) {
138 s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
138 s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
139 }
139 }
140 s[12] = "4"; // bits 12-15 of the time_hi_and_version field to 0010
140 s[12] = "4"; // bits 12-15 of the time_hi_and_version field to 0010
141 s[16] = hexDigits.substr((s[16] & 0x3) | 0x8, 1); // bits 6-7 of the clock_seq_hi_and_reserved to 01
141 s[16] = hexDigits.substr((s[16] & 0x3) | 0x8, 1); // bits 6-7 of the clock_seq_hi_and_reserved to 01
142
142
143 var uuid = s.join("");
143 var uuid = s.join("");
144 return uuid;
144 return uuid;
145 };
145 };
146
146
147
147
148 //Fix raw text to parse correctly in crazy XML
148 //Fix raw text to parse correctly in crazy XML
149 function xmlencode(string) {
149 function xmlencode(string) {
150 return string.replace(/\&/g,'&'+'amp;')
150 return string.replace(/\&/g,'&'+'amp;')
151 .replace(/</g,'&'+'lt;')
151 .replace(/</g,'&'+'lt;')
152 .replace(/>/g,'&'+'gt;')
152 .replace(/>/g,'&'+'gt;')
153 .replace(/\'/g,'&'+'apos;')
153 .replace(/\'/g,'&'+'apos;')
154 .replace(/\"/g,'&'+'quot;')
154 .replace(/\"/g,'&'+'quot;')
155 .replace(/`/g,'&'+'#96;');
155 .replace(/`/g,'&'+'#96;');
156 }
156 }
157
157
158
158
159 //Map from terminal commands to CSS classes
159 //Map from terminal commands to CSS classes
160 var ansi_colormap = {
160 var ansi_colormap = {
161 "01":"ansibold",
161 "01":"ansibold",
162
162
163 "30":"ansiblack",
163 "30":"ansiblack",
164 "31":"ansired",
164 "31":"ansired",
165 "32":"ansigreen",
165 "32":"ansigreen",
166 "33":"ansiyellow",
166 "33":"ansiyellow",
167 "34":"ansiblue",
167 "34":"ansiblue",
168 "35":"ansipurple",
168 "35":"ansipurple",
169 "36":"ansicyan",
169 "36":"ansicyan",
170 "37":"ansigray",
170 "37":"ansigray",
171
171
172 "40":"ansibgblack",
172 "40":"ansibgblack",
173 "41":"ansibgred",
173 "41":"ansibgred",
174 "42":"ansibggreen",
174 "42":"ansibggreen",
175 "43":"ansibgyellow",
175 "43":"ansibgyellow",
176 "44":"ansibgblue",
176 "44":"ansibgblue",
177 "45":"ansibgpurple",
177 "45":"ansibgpurple",
178 "46":"ansibgcyan",
178 "46":"ansibgcyan",
179 "47":"ansibggray"
179 "47":"ansibggray"
180 };
180 };
181
181
182 function _process_numbers(attrs, numbers) {
182 function _process_numbers(attrs, numbers) {
183 // process ansi escapes
183 // process ansi escapes
184 var n = numbers.shift();
184 var n = numbers.shift();
185 if (ansi_colormap[n]) {
185 if (ansi_colormap[n]) {
186 if ( ! attrs["class"] ) {
186 if ( ! attrs["class"] ) {
187 attrs["class"] = ansi_colormap[n];
187 attrs["class"] = ansi_colormap[n];
188 } else {
188 } else {
189 attrs["class"] += " " + ansi_colormap[n];
189 attrs["class"] += " " + ansi_colormap[n];
190 }
190 }
191 } else if (n == "38" || n == "48") {
191 } else if (n == "38" || n == "48") {
192 // VT100 256 color or 24 bit RGB
192 // VT100 256 color or 24 bit RGB
193 if (numbers.length < 2) {
193 if (numbers.length < 2) {
194 console.log("Not enough fields for VT100 color", numbers);
194 console.log("Not enough fields for VT100 color", numbers);
195 return;
195 return;
196 }
196 }
197
197
198 var index_or_rgb = numbers.shift();
198 var index_or_rgb = numbers.shift();
199 var r,g,b;
199 var r,g,b;
200 if (index_or_rgb == "5") {
200 if (index_or_rgb == "5") {
201 // 256 color
201 // 256 color
202 var idx = parseInt(numbers.shift());
202 var idx = parseInt(numbers.shift());
203 if (idx < 16) {
203 if (idx < 16) {
204 // indexed ANSI
204 // indexed ANSI
205 // ignore bright / non-bright distinction
205 // ignore bright / non-bright distinction
206 idx = idx % 8;
206 idx = idx % 8;
207 var ansiclass = ansi_colormap[n[0] + (idx % 8).toString()];
207 var ansiclass = ansi_colormap[n[0] + (idx % 8).toString()];
208 if ( ! attrs["class"] ) {
208 if ( ! attrs["class"] ) {
209 attrs["class"] = ansiclass;
209 attrs["class"] = ansiclass;
210 } else {
210 } else {
211 attrs["class"] += " " + ansiclass;
211 attrs["class"] += " " + ansiclass;
212 }
212 }
213 return;
213 return;
214 } else if (idx < 232) {
214 } else if (idx < 232) {
215 // 216 color 6x6x6 RGB
215 // 216 color 6x6x6 RGB
216 idx = idx - 16;
216 idx = idx - 16;
217 b = idx % 6;
217 b = idx % 6;
218 g = Math.floor(idx / 6) % 6;
218 g = Math.floor(idx / 6) % 6;
219 r = Math.floor(idx / 36) % 6;
219 r = Math.floor(idx / 36) % 6;
220 // convert to rgb
220 // convert to rgb
221 r = (r * 51);
221 r = (r * 51);
222 g = (g * 51);
222 g = (g * 51);
223 b = (b * 51);
223 b = (b * 51);
224 } else {
224 } else {
225 // grayscale
225 // grayscale
226 idx = idx - 231;
226 idx = idx - 231;
227 // it's 1-24 and should *not* include black or white,
227 // it's 1-24 and should *not* include black or white,
228 // so a 26 point scale
228 // so a 26 point scale
229 r = g = b = Math.floor(idx * 256 / 26);
229 r = g = b = Math.floor(idx * 256 / 26);
230 }
230 }
231 } else if (index_or_rgb == "2") {
231 } else if (index_or_rgb == "2") {
232 // Simple 24 bit RGB
232 // Simple 24 bit RGB
233 if (numbers.length > 3) {
233 if (numbers.length > 3) {
234 console.log("Not enough fields for RGB", numbers);
234 console.log("Not enough fields for RGB", numbers);
235 return;
235 return;
236 }
236 }
237 r = numbers.shift();
237 r = numbers.shift();
238 g = numbers.shift();
238 g = numbers.shift();
239 b = numbers.shift();
239 b = numbers.shift();
240 } else {
240 } else {
241 console.log("unrecognized control", numbers);
241 console.log("unrecognized control", numbers);
242 return;
242 return;
243 }
243 }
244 if (r !== undefined) {
244 if (r !== undefined) {
245 // apply the rgb color
245 // apply the rgb color
246 var line;
246 var line;
247 if (n == "38") {
247 if (n == "38") {
248 line = "color: ";
248 line = "color: ";
249 } else {
249 } else {
250 line = "background-color: ";
250 line = "background-color: ";
251 }
251 }
252 line = line + "rgb(" + r + "," + g + "," + b + ");"
252 line = line + "rgb(" + r + "," + g + "," + b + ");"
253 if ( !attrs["style"] ) {
253 if ( !attrs["style"] ) {
254 attrs["style"] = line;
254 attrs["style"] = line;
255 } else {
255 } else {
256 attrs["style"] += " " + line;
256 attrs["style"] += " " + line;
257 }
257 }
258 }
258 }
259 }
259 }
260 }
260 }
261
261
262 function ansispan(str) {
262 function ansispan(str) {
263 // ansispan function adapted from github.com/mmalecki/ansispan (MIT License)
263 // ansispan function adapted from github.com/mmalecki/ansispan (MIT License)
264 // regular ansi escapes (using the table above)
264 // regular ansi escapes (using the table above)
265 return str.replace(/\033\[(0?[01]|22|39)?([;\d]+)?m/g, function(match, prefix, pattern) {
265 return str.replace(/\033\[(0?[01]|22|39)?([;\d]+)?m/g, function(match, prefix, pattern) {
266 if (!pattern) {
266 if (!pattern) {
267 // [(01|22|39|)m close spans
267 // [(01|22|39|)m close spans
268 return "</span>";
268 return "</span>";
269 }
269 }
270 // consume sequence of color escapes
270 // consume sequence of color escapes
271 var numbers = pattern.match(/\d+/g);
271 var numbers = pattern.match(/\d+/g);
272 var attrs = {};
272 var attrs = {};
273 while (numbers.length > 0) {
273 while (numbers.length > 0) {
274 _process_numbers(attrs, numbers);
274 _process_numbers(attrs, numbers);
275 }
275 }
276
276
277 var span = "<span ";
277 var span = "<span ";
278 for (var attr in attrs) {
278 for (var attr in attrs) {
279 var value = attrs[attr];
279 var value = attrs[attr];
280 span = span + " " + attr + '="' + attrs[attr] + '"';
280 span = span + " " + attr + '="' + attrs[attr] + '"';
281 }
281 }
282 return span + ">";
282 return span + ">";
283 });
283 });
284 };
284 };
285
285
286 // Transform ANSI color escape codes into HTML <span> tags with css
286 // Transform ANSI color escape codes into HTML <span> tags with css
287 // classes listed in the above ansi_colormap object. The actual color used
287 // classes listed in the above ansi_colormap object. The actual color used
288 // are set in the css file.
288 // are set in the css file.
289 function fixConsole(txt) {
289 function fixConsole(txt) {
290 txt = xmlencode(txt);
290 txt = xmlencode(txt);
291 var re = /\033\[([\dA-Fa-f;]*?)m/;
291 var re = /\033\[([\dA-Fa-f;]*?)m/;
292 var opened = false;
292 var opened = false;
293 var cmds = [];
293 var cmds = [];
294 var opener = "";
294 var opener = "";
295 var closer = "";
295 var closer = "";
296
296
297 // Strip all ANSI codes that are not color related. Matches
297 // Strip all ANSI codes that are not color related. Matches
298 // all ANSI codes that do not end with "m".
298 // all ANSI codes that do not end with "m".
299 var ignored_re = /(?=(\033\[[\d;=]*[a-ln-zA-Z]{1}))\1(?!m)/g;
299 var ignored_re = /(?=(\033\[[\d;=]*[a-ln-zA-Z]{1}))\1(?!m)/g;
300 txt = txt.replace(ignored_re, "");
300 txt = txt.replace(ignored_re, "");
301
301
302 // color ansi codes
302 // color ansi codes
303 txt = ansispan(txt);
303 txt = ansispan(txt);
304 return txt;
304 return txt;
305 }
305 }
306
306
307 // Remove chunks that should be overridden by the effect of
307 // Remove chunks that should be overridden by the effect of
308 // carriage return characters
308 // carriage return characters
309 function fixCarriageReturn(txt) {
309 function fixCarriageReturn(txt) {
310 var tmp = txt;
310 var tmp = txt;
311 do {
311 do {
312 txt = tmp;
312 txt = tmp;
313 tmp = txt.replace(/\r+\n/gm, '\n'); // \r followed by \n --> newline
313 tmp = txt.replace(/\r+\n/gm, '\n'); // \r followed by \n --> newline
314 tmp = tmp.replace(/^.*\r+/gm, ''); // Other \r --> clear line
314 tmp = tmp.replace(/^.*\r+/gm, ''); // Other \r --> clear line
315 } while (tmp.length < txt.length);
315 } while (tmp.length < txt.length);
316 return txt;
316 return txt;
317 }
317 }
318
318
319 // Locate any URLs and convert them to a anchor tag
319 // Locate any URLs and convert them to a anchor tag
320 function autoLinkUrls(txt) {
320 function autoLinkUrls(txt) {
321 return txt.replace(/(^|\s)(https?|ftp)(:[^'">\s]+)/gi,
321 return txt.replace(/(^|\s)(https?|ftp)(:[^'">\s]+)/gi,
322 "$1<a target=\"_blank\" href=\"$2$3\">$2$3</a>");
322 "$1<a target=\"_blank\" href=\"$2$3\">$2$3</a>");
323 }
323 }
324
324
325 // some keycodes that seem to be platform/browser independent
325 // some keycodes that seem to be platform/browser independent
326 var keycodes = {
326 var keycodes = {
327 BACKSPACE: 8,
327 BACKSPACE: 8,
328 TAB : 9,
328 TAB : 9,
329 ENTER : 13,
329 ENTER : 13,
330 SHIFT : 16,
330 SHIFT : 16,
331 CTRL : 17,
331 CTRL : 17,
332 CONTROL : 17,
332 CONTROL : 17,
333 ALT : 18,
333 ALT : 18,
334 CAPS_LOCK: 20,
334 CAPS_LOCK: 20,
335 ESC : 27,
335 ESC : 27,
336 SPACE : 32,
336 SPACE : 32,
337 PGUP : 33,
337 PGUP : 33,
338 PGDOWN : 34,
338 PGDOWN : 34,
339 END : 35,
339 END : 35,
340 HOME : 36,
340 HOME : 36,
341 LEFT_ARROW: 37,
341 LEFT_ARROW: 37,
342 LEFTARROW: 37,
342 LEFTARROW: 37,
343 LEFT : 37,
343 LEFT : 37,
344 UP_ARROW : 38,
344 UP_ARROW : 38,
345 UPARROW : 38,
345 UPARROW : 38,
346 UP : 38,
346 UP : 38,
347 RIGHT_ARROW:39,
347 RIGHT_ARROW:39,
348 RIGHTARROW:39,
348 RIGHTARROW:39,
349 RIGHT : 39,
349 RIGHT : 39,
350 DOWN_ARROW: 40,
350 DOWN_ARROW: 40,
351 DOWNARROW: 40,
351 DOWNARROW: 40,
352 DOWN : 40,
352 DOWN : 40,
353 I : 73,
353 I : 73,
354 M : 77,
354 M : 77,
355 // all three of these keys may be COMMAND on OS X:
355 // all three of these keys may be COMMAND on OS X:
356 LEFT_SUPER : 91,
356 LEFT_SUPER : 91,
357 RIGHT_SUPER : 92,
357 RIGHT_SUPER : 92,
358 COMMAND : 93,
358 COMMAND : 93,
359 };
359 };
360
360
361 // trigger a key press event
361 // trigger a key press event
362 var press = function (key) {
362 var press = function (key) {
363 var key_press = $.Event('keydown', {which: key});
363 var key_press = $.Event('keydown', {which: key});
364 $(document).trigger(key_press);
364 $(document).trigger(key_press);
365 }
365 }
366
366
367 var press_up = function() { press(keycodes.UP); };
367 var press_up = function() { press(keycodes.UP); };
368 var press_down = function() { press(keycodes.DOWN); };
368 var press_down = function() { press(keycodes.DOWN); };
369
369
370 var press_ctrl_enter = function() {
370 var press_ctrl_enter = function() {
371 $(document).trigger($.Event('keydown', {which: keycodes.ENTER, ctrlKey: true}));
371 $(document).trigger($.Event('keydown', {which: keycodes.ENTER, ctrlKey: true}));
372 };
372 };
373
373
374 var press_shift_enter = function() {
374 var press_shift_enter = function() {
375 $(document).trigger($.Event('keydown', {which: keycodes.ENTER, shiftKey: true}));
375 $(document).trigger($.Event('keydown', {which: keycodes.ENTER, shiftKey: true}));
376 };
376 };
377
377
378 // trigger the ctrl-m shortcut followed by one of our keys
378 // trigger the ctrl-m shortcut followed by one of our keys
379 var press_ghetto = function(key) {
379 var press_ghetto = function(key) {
380 $(document).trigger($.Event('keydown', {which: keycodes.M, ctrlKey: true}));
380 $(document).trigger($.Event('keydown', {which: keycodes.M, ctrlKey: true}));
381 press(key);
381 press(key);
382 };
382 };
383
383
384
384
385 var points_to_pixels = function (points) {
385 var points_to_pixels = function (points) {
386 // A reasonably good way of converting between points and pixels.
386 // A reasonably good way of converting between points and pixels.
387 var test = $('<div style="display: none; width: 10000pt; padding:0; border:0;"></div>');
387 var test = $('<div style="display: none; width: 10000pt; padding:0; border:0;"></div>');
388 $(body).append(test);
388 $(body).append(test);
389 var pixel_per_point = test.width()/10000;
389 var pixel_per_point = test.width()/10000;
390 test.remove();
390 test.remove();
391 return Math.floor(points*pixel_per_point);
391 return Math.floor(points*pixel_per_point);
392 };
392 };
393
393
394 var always_new = function (constructor) {
394 var always_new = function (constructor) {
395 // wrapper around contructor to avoid requiring `var a = new constructor()`
395 // wrapper around contructor to avoid requiring `var a = new constructor()`
396 // useful for passing constructors as callbacks,
396 // useful for passing constructors as callbacks,
397 // not for programmer laziness.
397 // not for programmer laziness.
398 // from http://programmers.stackexchange.com/questions/118798
398 // from http://programmers.stackexchange.com/questions/118798
399 return function () {
399 return function () {
400 var obj = Object.create(constructor.prototype);
400 var obj = Object.create(constructor.prototype);
401 constructor.apply(obj, arguments);
401 constructor.apply(obj, arguments);
402 return obj;
402 return obj;
403 };
403 };
404 };
404 };
405
405
406
406
407 var url_path_join = function () {
407 var url_path_join = function () {
408 // join a sequence of url components with '/'
408 // join a sequence of url components with '/'
409 var url = '';
409 var url = '';
410 for (var i = 0; i < arguments.length; i++) {
410 for (var i = 0; i < arguments.length; i++) {
411 if (arguments[i] === '') {
411 if (arguments[i] === '') {
412 continue;
412 continue;
413 }
413 }
414 if (url.length > 0 && url[url.length-1] != '/') {
414 if (url.length > 0 && url[url.length-1] != '/') {
415 url = url + '/' + arguments[i];
415 url = url + '/' + arguments[i];
416 } else {
416 } else {
417 url = url + arguments[i];
417 url = url + arguments[i];
418 }
418 }
419 }
419 }
420 url = url.replace(/\/\/+/, '/');
420 return url;
421 return url;
421 };
422 };
422
423
424 var parse_url = function (url) {
425 // an `a` element with an href allows attr-access to the parsed segments of a URL
426 // a = parse_url("http://localhost:8888/path/name#hash")
427 // a.protocol = "http:"
428 // a.host = "localhost:8888"
429 // a.hostname = "localhost"
430 // a.port = 8888
431 // a.pathname = "/path/name"
432 // a.hash = "#hash"
433 var a = document.createElement("a");
434 a.href = url;
435 return a;
436 };
423
437
424 var encode_uri_components = function (uri) {
438 var encode_uri_components = function (uri) {
425 // encode just the components of a multi-segment uri,
439 // encode just the components of a multi-segment uri,
426 // leaving '/' separators
440 // leaving '/' separators
427 return uri.split('/').map(encodeURIComponent).join('/');
441 return uri.split('/').map(encodeURIComponent).join('/');
428 }
442 };
429
443
430 var url_join_encode = function () {
444 var url_join_encode = function () {
431 // join a sequence of url components with '/',
445 // join a sequence of url components with '/',
432 // encoding each component with encodeURIComponent
446 // encoding each component with encodeURIComponent
433 return encode_uri_components(url_path_join.apply(null, arguments));
447 return encode_uri_components(url_path_join.apply(null, arguments));
434 };
448 };
435
449
436
450
437 var splitext = function (filename) {
451 var splitext = function (filename) {
438 // mimic Python os.path.splitext
452 // mimic Python os.path.splitext
439 // Returns ['base', '.ext']
453 // Returns ['base', '.ext']
440 var idx = filename.lastIndexOf('.');
454 var idx = filename.lastIndexOf('.');
441 if (idx > 0) {
455 if (idx > 0) {
442 return [filename.slice(0, idx), filename.slice(idx)];
456 return [filename.slice(0, idx), filename.slice(idx)];
443 } else {
457 } else {
444 return [filename, ''];
458 return [filename, ''];
445 }
459 }
446 }
460 };
461
462
463 var get_body_data = function(key) {
464 // get a url-encoded item from body.data and decode it
465 // we should never have any encoded URLs anywhere else in code
466 // until we are building an actual request
467 return decodeURIComponent($('body').data(key));
468 };
447
469
448
470
449 // http://stackoverflow.com/questions/2400935/browser-detection-in-javascript
471 // http://stackoverflow.com/questions/2400935/browser-detection-in-javascript
450 var browser = (function() {
472 var browser = (function() {
451 if (typeof navigator === 'undefined') {
473 if (typeof navigator === 'undefined') {
452 // navigator undefined in node
474 // navigator undefined in node
453 return 'None';
475 return 'None';
454 }
476 }
455 var N= navigator.appName, ua= navigator.userAgent, tem;
477 var N= navigator.appName, ua= navigator.userAgent, tem;
456 var M= ua.match(/(opera|chrome|safari|firefox|msie)\/?\s*(\.?\d+(\.\d+)*)/i);
478 var M= ua.match(/(opera|chrome|safari|firefox|msie)\/?\s*(\.?\d+(\.\d+)*)/i);
457 if (M && (tem= ua.match(/version\/([\.\d]+)/i))!= null) M[2]= tem[1];
479 if (M && (tem= ua.match(/version\/([\.\d]+)/i))!= null) M[2]= tem[1];
458 M= M? [M[1], M[2]]: [N, navigator.appVersion,'-?'];
480 M= M? [M[1], M[2]]: [N, navigator.appVersion,'-?'];
459 return M;
481 return M;
460 })();
482 })();
461
483
462 // http://stackoverflow.com/questions/11219582/how-to-detect-my-browser-version-and-operating-system-using-javascript
484 // http://stackoverflow.com/questions/11219582/how-to-detect-my-browser-version-and-operating-system-using-javascript
463 var platform = (function () {
485 var platform = (function () {
464 if (typeof navigator === 'undefined') {
486 if (typeof navigator === 'undefined') {
465 // navigator undefined in node
487 // navigator undefined in node
466 return 'None';
488 return 'None';
467 }
489 }
468 var OSName="None";
490 var OSName="None";
469 if (navigator.appVersion.indexOf("Win")!=-1) OSName="Windows";
491 if (navigator.appVersion.indexOf("Win")!=-1) OSName="Windows";
470 if (navigator.appVersion.indexOf("Mac")!=-1) OSName="MacOS";
492 if (navigator.appVersion.indexOf("Mac")!=-1) OSName="MacOS";
471 if (navigator.appVersion.indexOf("X11")!=-1) OSName="UNIX";
493 if (navigator.appVersion.indexOf("X11")!=-1) OSName="UNIX";
472 if (navigator.appVersion.indexOf("Linux")!=-1) OSName="Linux";
494 if (navigator.appVersion.indexOf("Linux")!=-1) OSName="Linux";
473 return OSName
495 return OSName
474 })();
496 })();
475
497
476 var is_or_has = function (a, b) {
498 var is_or_has = function (a, b) {
477 // Is b a child of a or a itself?
499 // Is b a child of a or a itself?
478 return a.has(b).length !==0 || a.is(b);
500 return a.has(b).length !==0 || a.is(b);
479 }
501 }
480
502
481 var is_focused = function (e) {
503 var is_focused = function (e) {
482 // Is element e, or one of its children focused?
504 // Is element e, or one of its children focused?
483 e = $(e);
505 e = $(e);
484 var target = $(document.activeElement);
506 var target = $(document.activeElement);
485 if (target.length > 0) {
507 if (target.length > 0) {
486 if (is_or_has(e, target)) {
508 if (is_or_has(e, target)) {
487 return true;
509 return true;
488 } else {
510 } else {
489 return false;
511 return false;
490 }
512 }
491 } else {
513 } else {
492 return false;
514 return false;
493 }
515 }
494 }
516 }
495
517
496
518
497 return {
519 return {
498 regex_split : regex_split,
520 regex_split : regex_split,
499 uuid : uuid,
521 uuid : uuid,
500 fixConsole : fixConsole,
522 fixConsole : fixConsole,
501 keycodes : keycodes,
523 keycodes : keycodes,
502 press : press,
524 press : press,
503 press_up : press_up,
525 press_up : press_up,
504 press_down : press_down,
526 press_down : press_down,
505 press_ctrl_enter : press_ctrl_enter,
527 press_ctrl_enter : press_ctrl_enter,
506 press_shift_enter : press_shift_enter,
528 press_shift_enter : press_shift_enter,
507 press_ghetto : press_ghetto,
529 press_ghetto : press_ghetto,
508 fixCarriageReturn : fixCarriageReturn,
530 fixCarriageReturn : fixCarriageReturn,
509 autoLinkUrls : autoLinkUrls,
531 autoLinkUrls : autoLinkUrls,
510 points_to_pixels : points_to_pixels,
532 points_to_pixels : points_to_pixels,
533 get_body_data : get_body_data,
534 parse_url : parse_url,
511 url_path_join : url_path_join,
535 url_path_join : url_path_join,
512 url_join_encode : url_join_encode,
536 url_join_encode : url_join_encode,
513 encode_uri_components : encode_uri_components,
537 encode_uri_components : encode_uri_components,
514 splitext : splitext,
538 splitext : splitext,
515 always_new : always_new,
539 always_new : always_new,
516 browser : browser,
540 browser : browser,
517 platform: platform,
541 platform: platform,
518 is_or_has : is_or_has,
542 is_or_has : is_or_has,
519 is_focused : is_focused
543 is_focused : is_focused
520 };
544 };
521
545
522 }(IPython));
546 }(IPython));
523
547
@@ -1,128 +1,122 b''
1 //----------------------------------------------------------------------------
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2011 The IPython Development Team
2 // Copyright (C) 2011 The IPython Development Team
3 //
3 //
4 // Distributed under the terms of the BSD License. The full license is in
4 // Distributed under the terms of the BSD License. The full license is in
5 // the file COPYING, distributed as part of this software.
5 // the file COPYING, distributed as part of this software.
6 //----------------------------------------------------------------------------
6 //----------------------------------------------------------------------------
7
7
8 //============================================================================
8 //============================================================================
9 // On document ready
9 // On document ready
10 //============================================================================
10 //============================================================================
11 "use strict";
12
11
13 // for the time beeing, we have to pass marked as a parameter here,
12 // for the time beeing, we have to pass marked as a parameter here,
14 // as injecting require.js make marked not to put itself in the globals,
13 // as injecting require.js make marked not to put itself in the globals,
15 // which make both this file fail at setting marked configuration, and textcell.js
14 // which make both this file fail at setting marked configuration, and textcell.js
16 // which search marked into global.
15 // which search marked into global.
17 require(['components/marked/lib/marked',
16 require(['components/marked/lib/marked',
18 'notebook/js/widgets/init'],
17 'notebook/js/widgets/init'],
19
18
20 function (marked) {
19 function (marked) {
20 "use strict";
21
21
22 window.marked = marked
22 window.marked = marked;
23
23
24 // monkey patch CM to be able to syntax highlight cell magics
24 // monkey patch CM to be able to syntax highlight cell magics
25 // bug reported upstream,
25 // bug reported upstream,
26 // see https://github.com/marijnh/CodeMirror2/issues/670
26 // see https://github.com/marijnh/CodeMirror2/issues/670
27 if(CodeMirror.getMode(1,'text/plain').indent == undefined ){
27 if(CodeMirror.getMode(1,'text/plain').indent === undefined ){
28 console.log('patching CM for undefined indent');
28 console.log('patching CM for undefined indent');
29 CodeMirror.modes.null = function() {
29 CodeMirror.modes.null = function() {
30 return {token: function(stream) {stream.skipToEnd();},indent : function(){return 0}}
30 return {token: function(stream) {stream.skipToEnd();},indent : function(){return 0;}};
31 }
31 };
32 }
32 }
33
33
34 CodeMirror.patchedGetMode = function(config, mode){
34 CodeMirror.patchedGetMode = function(config, mode){
35 var cmmode = CodeMirror.getMode(config, mode);
35 var cmmode = CodeMirror.getMode(config, mode);
36 if(cmmode.indent == null)
36 if(cmmode.indent === null) {
37 {
38 console.log('patch mode "' , mode, '" on the fly');
37 console.log('patch mode "' , mode, '" on the fly');
39 cmmode.indent = function(){return 0};
38 cmmode.indent = function(){return 0;};
40 }
39 }
41 return cmmode;
40 return cmmode;
42 }
41 };
43 // end monkey patching CodeMirror
42 // end monkey patching CodeMirror
44
43
45 IPython.mathjaxutils.init();
44 IPython.mathjaxutils.init();
46
45
47 $('#ipython-main-app').addClass('border-box-sizing');
46 $('#ipython-main-app').addClass('border-box-sizing');
48 $('div#notebook_panel').addClass('border-box-sizing');
47 $('div#notebook_panel').addClass('border-box-sizing');
49
48
50 var baseProjectUrl = $('body').data('baseProjectUrl');
49 var opts = {
51 var notebookPath = $('body').data('notebookPath');
50 base_url : IPython.utils.get_body_data("baseUrl"),
52 var notebookName = $('body').data('notebookName');
51 base_kernel_url : IPython.utils.get_body_data("baseKernelUrl"),
53 notebookName = decodeURIComponent(notebookName);
52 notebook_path : IPython.utils.get_body_data("notebookPath"),
54 notebookPath = decodeURIComponent(notebookPath);
53 notebook_name : IPython.utils.get_body_data('notebookName')
55 console.log(notebookName);
54 };
56 if (notebookPath == 'None'){
57 notebookPath = "";
58 }
59
55
60 IPython.page = new IPython.Page();
56 IPython.page = new IPython.Page();
61 IPython.layout_manager = new IPython.LayoutManager();
57 IPython.layout_manager = new IPython.LayoutManager();
62 IPython.pager = new IPython.Pager('div#pager', 'div#pager_splitter');
58 IPython.pager = new IPython.Pager('div#pager', 'div#pager_splitter');
63 IPython.quick_help = new IPython.QuickHelp();
59 IPython.quick_help = new IPython.QuickHelp();
64 IPython.login_widget = new IPython.LoginWidget('span#login_widget',{baseProjectUrl:baseProjectUrl});
60 IPython.login_widget = new IPython.LoginWidget('span#login_widget', opts);
65 IPython.notebook = new IPython.Notebook('div#notebook',{baseProjectUrl:baseProjectUrl, notebookPath:notebookPath, notebookName:notebookName});
61 IPython.notebook = new IPython.Notebook('div#notebook', opts);
66 IPython.keyboard_manager = new IPython.KeyboardManager();
62 IPython.keyboard_manager = new IPython.KeyboardManager();
67 IPython.save_widget = new IPython.SaveWidget('span#save_widget');
63 IPython.save_widget = new IPython.SaveWidget('span#save_widget');
68 IPython.menubar = new IPython.MenuBar('#menubar',{baseProjectUrl:baseProjectUrl, notebookPath: notebookPath})
64 IPython.menubar = new IPython.MenuBar('#menubar', opts);
69 IPython.toolbar = new IPython.MainToolBar('#maintoolbar-container')
65 IPython.toolbar = new IPython.MainToolBar('#maintoolbar-container');
70 IPython.tooltip = new IPython.Tooltip()
66 IPython.tooltip = new IPython.Tooltip();
71 IPython.notification_area = new IPython.NotificationArea('#notification_area')
67 IPython.notification_area = new IPython.NotificationArea('#notification_area');
72 IPython.notification_area.init_notification_widgets();
68 IPython.notification_area.init_notification_widgets();
73
69
74 IPython.layout_manager.do_resize();
70 IPython.layout_manager.do_resize();
75
71
76 $('body').append('<div id="fonttest"><pre><span id="test1">x</span>'+
72 $('body').append('<div id="fonttest"><pre><span id="test1">x</span>'+
77 '<span id="test2" style="font-weight: bold;">x</span>'+
73 '<span id="test2" style="font-weight: bold;">x</span>'+
78 '<span id="test3" style="font-style: italic;">x</span></pre></div>')
74 '<span id="test3" style="font-style: italic;">x</span></pre></div>');
79 var nh = $('#test1').innerHeight();
75 var nh = $('#test1').innerHeight();
80 var bh = $('#test2').innerHeight();
76 var bh = $('#test2').innerHeight();
81 var ih = $('#test3').innerHeight();
77 var ih = $('#test3').innerHeight();
82 if(nh != bh || nh != ih) {
78 if(nh != bh || nh != ih) {
83 $('head').append('<style>.CodeMirror span { vertical-align: bottom; }</style>');
79 $('head').append('<style>.CodeMirror span { vertical-align: bottom; }</style>');
84 }
80 }
85 $('#fonttest').remove();
81 $('#fonttest').remove();
86
82
87 IPython.page.show();
83 IPython.page.show();
88
84
89 IPython.layout_manager.do_resize();
85 IPython.layout_manager.do_resize();
90 var first_load = function () {
86 var first_load = function () {
91 IPython.layout_manager.do_resize();
87 IPython.layout_manager.do_resize();
92 var hash = document.location.hash;
88 var hash = document.location.hash;
93 if (hash) {
89 if (hash) {
94 document.location.hash = '';
90 document.location.hash = '';
95 document.location.hash = hash;
91 document.location.hash = hash;
96 }
92 }
97 IPython.notebook.set_autosave_interval(IPython.notebook.minimum_autosave_interval);
93 IPython.notebook.set_autosave_interval(IPython.notebook.minimum_autosave_interval);
98 // only do this once
94 // only do this once
99 $([IPython.events]).off('notebook_loaded.Notebook', first_load);
95 $([IPython.events]).off('notebook_loaded.Notebook', first_load);
100 };
96 };
101
97
102 $([IPython.events]).on('notebook_loaded.Notebook', first_load);
98 $([IPython.events]).on('notebook_loaded.Notebook', first_load);
103 $([IPython.events]).trigger('app_initialized.NotebookApp');
99 $([IPython.events]).trigger('app_initialized.NotebookApp');
104 IPython.notebook.load_notebook(notebookName, notebookPath);
100 IPython.notebook.load_notebook(opts.notebook_name, opts.notebook_path);
105
101
106 if (marked) {
102 if (marked) {
107 marked.setOptions({
103 marked.setOptions({
108 gfm : true,
104 gfm : true,
109 tables: true,
105 tables: true,
110 langPrefix: "language-",
106 langPrefix: "language-",
111 highlight: function(code, lang) {
107 highlight: function(code, lang) {
112 if (!lang) {
108 if (!lang) {
113 // no language, no highlight
109 // no language, no highlight
114 return code;
110 return code;
115 }
111 }
116 var highlighted;
112 var highlighted;
117 try {
113 try {
118 highlighted = hljs.highlight(lang, code, false);
114 highlighted = hljs.highlight(lang, code, false);
119 } catch(err) {
115 } catch(err) {
120 highlighted = hljs.highlightAuto(code);
116 highlighted = hljs.highlightAuto(code);
121 }
117 }
122 return highlighted.value;
118 return highlighted.value;
123 }
119 }
124 })
120 });
125 }
121 }
126 }
122 });
127
128 );
@@ -1,327 +1,318 b''
1 //----------------------------------------------------------------------------
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2008-2011 The IPython Development Team
2 // Copyright (C) 2008-2011 The IPython Development Team
3 //
3 //
4 // Distributed under the terms of the BSD License. The full license is in
4 // Distributed under the terms of the BSD License. The full license is in
5 // the file COPYING, distributed as part of this software.
5 // the file COPYING, distributed as part of this software.
6 //----------------------------------------------------------------------------
6 //----------------------------------------------------------------------------
7
7
8 //============================================================================
8 //============================================================================
9 // MenuBar
9 // MenuBar
10 //============================================================================
10 //============================================================================
11
11
12 /**
12 /**
13 * @module IPython
13 * @module IPython
14 * @namespace IPython
14 * @namespace IPython
15 * @submodule MenuBar
15 * @submodule MenuBar
16 */
16 */
17
17
18
18
19 var IPython = (function (IPython) {
19 var IPython = (function (IPython) {
20 "use strict";
20 "use strict";
21
21
22 var utils = IPython.utils;
22 var utils = IPython.utils;
23
23
24 /**
24 /**
25 * A MenuBar Class to generate the menubar of IPython notebook
25 * A MenuBar Class to generate the menubar of IPython notebook
26 * @Class MenuBar
26 * @Class MenuBar
27 *
27 *
28 * @constructor
28 * @constructor
29 *
29 *
30 *
30 *
31 * @param selector {string} selector for the menubar element in DOM
31 * @param selector {string} selector for the menubar element in DOM
32 * @param {object} [options]
32 * @param {object} [options]
33 * @param [options.baseProjectUrl] {String} String to use for the
33 * @param [options.base_url] {String} String to use for the
34 * Base Project url, default would be to inspect
34 * base project url. Default is to inspect
35 * $('body').data('baseProjectUrl');
35 * $('body').data('baseUrl');
36 * does not support change for now is set through this option
36 * does not support change for now is set through this option
37 */
37 */
38 var MenuBar = function (selector, options) {
38 var MenuBar = function (selector, options) {
39 options = options || {};
39 options = options || {};
40 if (options.baseProjectUrl !== undefined) {
40 this.base_url = options.base_url || IPython.utils.get_body_data("baseUrl");
41 this._baseProjectUrl = options.baseProjectUrl;
42 }
43 this.selector = selector;
41 this.selector = selector;
44 if (this.selector !== undefined) {
42 if (this.selector !== undefined) {
45 this.element = $(selector);
43 this.element = $(selector);
46 this.style();
44 this.style();
47 this.bind_events();
45 this.bind_events();
48 }
46 }
49 };
47 };
50
48
51 MenuBar.prototype.baseProjectUrl = function(){
52 return this._baseProjectUrl || $('body').data('baseProjectUrl');
53 };
54
55 MenuBar.prototype.notebookPath = function() {
56 var path = $('body').data('notebookPath');
57 path = decodeURIComponent(path);
58 return path;
59 };
60
61 MenuBar.prototype.style = function () {
49 MenuBar.prototype.style = function () {
62 this.element.addClass('border-box-sizing');
50 this.element.addClass('border-box-sizing');
63 this.element.find("li").click(function (event, ui) {
51 this.element.find("li").click(function (event, ui) {
64 // The selected cell loses focus when the menu is entered, so we
52 // The selected cell loses focus when the menu is entered, so we
65 // re-select it upon selection.
53 // re-select it upon selection.
66 var i = IPython.notebook.get_selected_index();
54 var i = IPython.notebook.get_selected_index();
67 IPython.notebook.select(i);
55 IPython.notebook.select(i);
68 }
56 }
69 );
57 );
70 };
58 };
71
59
72 MenuBar.prototype._nbconvert = function (format, download) {
60 MenuBar.prototype._nbconvert = function (format, download) {
73 download = download || false;
61 download = download || false;
74 var notebook_name = IPython.notebook.get_notebook_name();
62 var notebook_path = IPython.notebook.notebook_path;
63 var notebook_name = IPython.notebook.notebook_name;
75 if (IPython.notebook.dirty) {
64 if (IPython.notebook.dirty) {
76 IPython.notebook.save_notebook({async : false});
65 IPython.notebook.save_notebook({async : false});
77 }
66 }
78 var url = utils.url_path_join(
67 var url = utils.url_join_encode(
79 this.baseProjectUrl(),
68 this.base_url,
80 'nbconvert',
69 'nbconvert',
81 format,
70 format,
82 this.notebookPath(),
71 notebook_path,
83 notebook_name + '.ipynb'
72 notebook_name
84 ) + "?download=" + download.toString();
73 ) + "?download=" + download.toString();
85
74
86 window.open(url);
75 window.open(url);
87 }
76 };
88
77
89 MenuBar.prototype.bind_events = function () {
78 MenuBar.prototype.bind_events = function () {
90 // File
79 // File
91 var that = this;
80 var that = this;
92 this.element.find('#new_notebook').click(function () {
81 this.element.find('#new_notebook').click(function () {
93 IPython.notebook.new_notebook();
82 IPython.notebook.new_notebook();
94 });
83 });
95 this.element.find('#open_notebook').click(function () {
84 this.element.find('#open_notebook').click(function () {
96 window.open(utils.url_join_encode(
85 window.open(utils.url_join_encode(
97 that.baseProjectUrl(),
86 IPython.notebook.base_url,
98 'tree',
87 'tree',
99 that.notebookPath()
88 IPython.notebook.notebook_path
100 ));
89 ));
101 });
90 });
102 this.element.find('#copy_notebook').click(function () {
91 this.element.find('#copy_notebook').click(function () {
103 IPython.notebook.copy_notebook();
92 IPython.notebook.copy_notebook();
104 return false;
93 return false;
105 });
94 });
106 this.element.find('#download_ipynb').click(function () {
95 this.element.find('#download_ipynb').click(function () {
107 var notebook_name = IPython.notebook.get_notebook_name();
96 var base_url = IPython.notebook.base_url;
97 var notebook_path = IPython.notebook.notebook_path;
98 var notebook_name = IPython.notebook.notebook_name;
108 if (IPython.notebook.dirty) {
99 if (IPython.notebook.dirty) {
109 IPython.notebook.save_notebook({async : false});
100 IPython.notebook.save_notebook({async : false});
110 }
101 }
111
102
112 var url = utils.url_join_encode(
103 var url = utils.url_join_encode(
113 that.baseProjectUrl(),
104 base_url,
114 'files',
105 'files',
115 that.notebookPath(),
106 notebook_path,
116 notebook_name + '.ipynb'
107 notebook_name
117 );
108 );
118 window.location.assign(url);
109 window.location.assign(url);
119 });
110 });
120
111
121 this.element.find('#print_preview').click(function () {
112 this.element.find('#print_preview').click(function () {
122 that._nbconvert('html', false);
113 that._nbconvert('html', false);
123 });
114 });
124
115
125 this.element.find('#download_py').click(function () {
116 this.element.find('#download_py').click(function () {
126 that._nbconvert('python', true);
117 that._nbconvert('python', true);
127 });
118 });
128
119
129 this.element.find('#download_html').click(function () {
120 this.element.find('#download_html').click(function () {
130 that._nbconvert('html', true);
121 that._nbconvert('html', true);
131 });
122 });
132
123
133 this.element.find('#download_rst').click(function () {
124 this.element.find('#download_rst').click(function () {
134 that._nbconvert('rst', true);
125 that._nbconvert('rst', true);
135 });
126 });
136
127
137 this.element.find('#rename_notebook').click(function () {
128 this.element.find('#rename_notebook').click(function () {
138 IPython.save_widget.rename_notebook();
129 IPython.save_widget.rename_notebook();
139 });
130 });
140 this.element.find('#save_checkpoint').click(function () {
131 this.element.find('#save_checkpoint').click(function () {
141 IPython.notebook.save_checkpoint();
132 IPython.notebook.save_checkpoint();
142 });
133 });
143 this.element.find('#restore_checkpoint').click(function () {
134 this.element.find('#restore_checkpoint').click(function () {
144 });
135 });
145 this.element.find('#kill_and_exit').click(function () {
136 this.element.find('#kill_and_exit').click(function () {
146 IPython.notebook.session.delete();
137 IPython.notebook.session.delete();
147 setTimeout(function(){
138 setTimeout(function(){
148 // allow closing of new tabs in Chromium, impossible in FF
139 // allow closing of new tabs in Chromium, impossible in FF
149 window.open('', '_self', '');
140 window.open('', '_self', '');
150 window.close();
141 window.close();
151 }, 500);
142 }, 500);
152 });
143 });
153 // Edit
144 // Edit
154 this.element.find('#cut_cell').click(function () {
145 this.element.find('#cut_cell').click(function () {
155 IPython.notebook.cut_cell();
146 IPython.notebook.cut_cell();
156 });
147 });
157 this.element.find('#copy_cell').click(function () {
148 this.element.find('#copy_cell').click(function () {
158 IPython.notebook.copy_cell();
149 IPython.notebook.copy_cell();
159 });
150 });
160 this.element.find('#delete_cell').click(function () {
151 this.element.find('#delete_cell').click(function () {
161 IPython.notebook.delete_cell();
152 IPython.notebook.delete_cell();
162 });
153 });
163 this.element.find('#undelete_cell').click(function () {
154 this.element.find('#undelete_cell').click(function () {
164 IPython.notebook.undelete_cell();
155 IPython.notebook.undelete_cell();
165 });
156 });
166 this.element.find('#split_cell').click(function () {
157 this.element.find('#split_cell').click(function () {
167 IPython.notebook.split_cell();
158 IPython.notebook.split_cell();
168 });
159 });
169 this.element.find('#merge_cell_above').click(function () {
160 this.element.find('#merge_cell_above').click(function () {
170 IPython.notebook.merge_cell_above();
161 IPython.notebook.merge_cell_above();
171 });
162 });
172 this.element.find('#merge_cell_below').click(function () {
163 this.element.find('#merge_cell_below').click(function () {
173 IPython.notebook.merge_cell_below();
164 IPython.notebook.merge_cell_below();
174 });
165 });
175 this.element.find('#move_cell_up').click(function () {
166 this.element.find('#move_cell_up').click(function () {
176 IPython.notebook.move_cell_up();
167 IPython.notebook.move_cell_up();
177 });
168 });
178 this.element.find('#move_cell_down').click(function () {
169 this.element.find('#move_cell_down').click(function () {
179 IPython.notebook.move_cell_down();
170 IPython.notebook.move_cell_down();
180 });
171 });
181 this.element.find('#edit_nb_metadata').click(function () {
172 this.element.find('#edit_nb_metadata').click(function () {
182 IPython.notebook.edit_metadata();
173 IPython.notebook.edit_metadata();
183 });
174 });
184
175
185 // View
176 // View
186 this.element.find('#toggle_header').click(function () {
177 this.element.find('#toggle_header').click(function () {
187 $('div#header').toggle();
178 $('div#header').toggle();
188 IPython.layout_manager.do_resize();
179 IPython.layout_manager.do_resize();
189 });
180 });
190 this.element.find('#toggle_toolbar').click(function () {
181 this.element.find('#toggle_toolbar').click(function () {
191 $('div#maintoolbar').toggle();
182 $('div#maintoolbar').toggle();
192 IPython.layout_manager.do_resize();
183 IPython.layout_manager.do_resize();
193 });
184 });
194 // Insert
185 // Insert
195 this.element.find('#insert_cell_above').click(function () {
186 this.element.find('#insert_cell_above').click(function () {
196 IPython.notebook.insert_cell_above('code');
187 IPython.notebook.insert_cell_above('code');
197 IPython.notebook.select_prev();
188 IPython.notebook.select_prev();
198 });
189 });
199 this.element.find('#insert_cell_below').click(function () {
190 this.element.find('#insert_cell_below').click(function () {
200 IPython.notebook.insert_cell_below('code');
191 IPython.notebook.insert_cell_below('code');
201 IPython.notebook.select_next();
192 IPython.notebook.select_next();
202 });
193 });
203 // Cell
194 // Cell
204 this.element.find('#run_cell').click(function () {
195 this.element.find('#run_cell').click(function () {
205 IPython.notebook.execute_cell();
196 IPython.notebook.execute_cell();
206 });
197 });
207 this.element.find('#run_cell_select_below').click(function () {
198 this.element.find('#run_cell_select_below').click(function () {
208 IPython.notebook.execute_cell_and_select_below();
199 IPython.notebook.execute_cell_and_select_below();
209 });
200 });
210 this.element.find('#run_cell_insert_below').click(function () {
201 this.element.find('#run_cell_insert_below').click(function () {
211 IPython.notebook.execute_cell_and_insert_below();
202 IPython.notebook.execute_cell_and_insert_below();
212 });
203 });
213 this.element.find('#run_all_cells').click(function () {
204 this.element.find('#run_all_cells').click(function () {
214 IPython.notebook.execute_all_cells();
205 IPython.notebook.execute_all_cells();
215 });
206 });
216 this.element.find('#run_all_cells_above').click(function () {
207 this.element.find('#run_all_cells_above').click(function () {
217 IPython.notebook.execute_cells_above();
208 IPython.notebook.execute_cells_above();
218 });
209 });
219 this.element.find('#run_all_cells_below').click(function () {
210 this.element.find('#run_all_cells_below').click(function () {
220 IPython.notebook.execute_cells_below();
211 IPython.notebook.execute_cells_below();
221 });
212 });
222 this.element.find('#to_code').click(function () {
213 this.element.find('#to_code').click(function () {
223 IPython.notebook.to_code();
214 IPython.notebook.to_code();
224 });
215 });
225 this.element.find('#to_markdown').click(function () {
216 this.element.find('#to_markdown').click(function () {
226 IPython.notebook.to_markdown();
217 IPython.notebook.to_markdown();
227 });
218 });
228 this.element.find('#to_raw').click(function () {
219 this.element.find('#to_raw').click(function () {
229 IPython.notebook.to_raw();
220 IPython.notebook.to_raw();
230 });
221 });
231 this.element.find('#to_heading1').click(function () {
222 this.element.find('#to_heading1').click(function () {
232 IPython.notebook.to_heading(undefined, 1);
223 IPython.notebook.to_heading(undefined, 1);
233 });
224 });
234 this.element.find('#to_heading2').click(function () {
225 this.element.find('#to_heading2').click(function () {
235 IPython.notebook.to_heading(undefined, 2);
226 IPython.notebook.to_heading(undefined, 2);
236 });
227 });
237 this.element.find('#to_heading3').click(function () {
228 this.element.find('#to_heading3').click(function () {
238 IPython.notebook.to_heading(undefined, 3);
229 IPython.notebook.to_heading(undefined, 3);
239 });
230 });
240 this.element.find('#to_heading4').click(function () {
231 this.element.find('#to_heading4').click(function () {
241 IPython.notebook.to_heading(undefined, 4);
232 IPython.notebook.to_heading(undefined, 4);
242 });
233 });
243 this.element.find('#to_heading5').click(function () {
234 this.element.find('#to_heading5').click(function () {
244 IPython.notebook.to_heading(undefined, 5);
235 IPython.notebook.to_heading(undefined, 5);
245 });
236 });
246 this.element.find('#to_heading6').click(function () {
237 this.element.find('#to_heading6').click(function () {
247 IPython.notebook.to_heading(undefined, 6);
238 IPython.notebook.to_heading(undefined, 6);
248 });
239 });
249
240
250 this.element.find('#toggle_current_output').click(function () {
241 this.element.find('#toggle_current_output').click(function () {
251 IPython.notebook.toggle_output();
242 IPython.notebook.toggle_output();
252 });
243 });
253 this.element.find('#toggle_current_output_scroll').click(function () {
244 this.element.find('#toggle_current_output_scroll').click(function () {
254 IPython.notebook.toggle_output_scroll();
245 IPython.notebook.toggle_output_scroll();
255 });
246 });
256 this.element.find('#clear_current_output').click(function () {
247 this.element.find('#clear_current_output').click(function () {
257 IPython.notebook.clear_output();
248 IPython.notebook.clear_output();
258 });
249 });
259
250
260 this.element.find('#toggle_all_output').click(function () {
251 this.element.find('#toggle_all_output').click(function () {
261 IPython.notebook.toggle_all_output();
252 IPython.notebook.toggle_all_output();
262 });
253 });
263 this.element.find('#toggle_all_output_scroll').click(function () {
254 this.element.find('#toggle_all_output_scroll').click(function () {
264 IPython.notebook.toggle_all_output_scroll();
255 IPython.notebook.toggle_all_output_scroll();
265 });
256 });
266 this.element.find('#clear_all_output').click(function () {
257 this.element.find('#clear_all_output').click(function () {
267 IPython.notebook.clear_all_output();
258 IPython.notebook.clear_all_output();
268 });
259 });
269
260
270 // Kernel
261 // Kernel
271 this.element.find('#int_kernel').click(function () {
262 this.element.find('#int_kernel').click(function () {
272 IPython.notebook.session.interrupt_kernel();
263 IPython.notebook.session.interrupt_kernel();
273 });
264 });
274 this.element.find('#restart_kernel').click(function () {
265 this.element.find('#restart_kernel').click(function () {
275 IPython.notebook.restart_kernel();
266 IPython.notebook.restart_kernel();
276 });
267 });
277 // Help
268 // Help
278 this.element.find('#keyboard_shortcuts').click(function () {
269 this.element.find('#keyboard_shortcuts').click(function () {
279 IPython.quick_help.show_keyboard_shortcuts();
270 IPython.quick_help.show_keyboard_shortcuts();
280 });
271 });
281
272
282 this.update_restore_checkpoint(null);
273 this.update_restore_checkpoint(null);
283
274
284 $([IPython.events]).on('checkpoints_listed.Notebook', function (event, data) {
275 $([IPython.events]).on('checkpoints_listed.Notebook', function (event, data) {
285 that.update_restore_checkpoint(IPython.notebook.checkpoints);
276 that.update_restore_checkpoint(IPython.notebook.checkpoints);
286 });
277 });
287
278
288 $([IPython.events]).on('checkpoint_created.Notebook', function (event, data) {
279 $([IPython.events]).on('checkpoint_created.Notebook', function (event, data) {
289 that.update_restore_checkpoint(IPython.notebook.checkpoints);
280 that.update_restore_checkpoint(IPython.notebook.checkpoints);
290 });
281 });
291 };
282 };
292
283
293 MenuBar.prototype.update_restore_checkpoint = function(checkpoints) {
284 MenuBar.prototype.update_restore_checkpoint = function(checkpoints) {
294 var ul = this.element.find("#restore_checkpoint").find("ul");
285 var ul = this.element.find("#restore_checkpoint").find("ul");
295 ul.empty();
286 ul.empty();
296 if (!checkpoints || checkpoints.length === 0) {
287 if (!checkpoints || checkpoints.length === 0) {
297 ul.append(
288 ul.append(
298 $("<li/>")
289 $("<li/>")
299 .addClass("disabled")
290 .addClass("disabled")
300 .append(
291 .append(
301 $("<a/>")
292 $("<a/>")
302 .text("No checkpoints")
293 .text("No checkpoints")
303 )
294 )
304 );
295 );
305 return;
296 return;
306 }
297 }
307
298
308 checkpoints.map(function (checkpoint) {
299 checkpoints.map(function (checkpoint) {
309 var d = new Date(checkpoint.last_modified);
300 var d = new Date(checkpoint.last_modified);
310 ul.append(
301 ul.append(
311 $("<li/>").append(
302 $("<li/>").append(
312 $("<a/>")
303 $("<a/>")
313 .attr("href", "#")
304 .attr("href", "#")
314 .text(d.format("mmm dd HH:MM:ss"))
305 .text(d.format("mmm dd HH:MM:ss"))
315 .click(function () {
306 .click(function () {
316 IPython.notebook.restore_checkpoint_dialog(checkpoint);
307 IPython.notebook.restore_checkpoint_dialog(checkpoint);
317 })
308 })
318 )
309 )
319 );
310 );
320 });
311 });
321 };
312 };
322
313
323 IPython.MenuBar = MenuBar;
314 IPython.MenuBar = MenuBar;
324
315
325 return IPython;
316 return IPython;
326
317
327 }(IPython));
318 }(IPython));
@@ -1,2303 +1,2288 b''
1 //----------------------------------------------------------------------------
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2011 The IPython Development Team
2 // Copyright (C) 2011 The IPython Development Team
3 //
3 //
4 // Distributed under the terms of the BSD License. The full license is in
4 // Distributed under the terms of the BSD License. The full license is in
5 // the file COPYING, distributed as part of this software.
5 // the file COPYING, distributed as part of this software.
6 //----------------------------------------------------------------------------
6 //----------------------------------------------------------------------------
7
7
8 //============================================================================
8 //============================================================================
9 // Notebook
9 // Notebook
10 //============================================================================
10 //============================================================================
11
11
12 var IPython = (function (IPython) {
12 var IPython = (function (IPython) {
13 "use strict";
13 "use strict";
14
14
15 var utils = IPython.utils;
15 var utils = IPython.utils;
16
16
17 /**
17 /**
18 * A notebook contains and manages cells.
18 * A notebook contains and manages cells.
19 *
19 *
20 * @class Notebook
20 * @class Notebook
21 * @constructor
21 * @constructor
22 * @param {String} selector A jQuery selector for the notebook's DOM element
22 * @param {String} selector A jQuery selector for the notebook's DOM element
23 * @param {Object} [options] A config object
23 * @param {Object} [options] A config object
24 */
24 */
25 var Notebook = function (selector, options) {
25 var Notebook = function (selector, options) {
26 var options = options || {};
26 this.options = options = options || {};
27 this._baseProjectUrl = options.baseProjectUrl;
27 this.base_url = options.base_url;
28 this.notebook_path = options.notebookPath;
28 this.notebook_path = options.notebook_path;
29 this.notebook_name = options.notebookName;
29 this.notebook_name = options.notebook_name;
30 this.element = $(selector);
30 this.element = $(selector);
31 this.element.scroll();
31 this.element.scroll();
32 this.element.data("notebook", this);
32 this.element.data("notebook", this);
33 this.next_prompt_number = 1;
33 this.next_prompt_number = 1;
34 this.session = null;
34 this.session = null;
35 this.kernel = null;
35 this.kernel = null;
36 this.clipboard = null;
36 this.clipboard = null;
37 this.undelete_backup = null;
37 this.undelete_backup = null;
38 this.undelete_index = null;
38 this.undelete_index = null;
39 this.undelete_below = false;
39 this.undelete_below = false;
40 this.paste_enabled = false;
40 this.paste_enabled = false;
41 // It is important to start out in command mode to match the intial mode
41 // It is important to start out in command mode to match the intial mode
42 // of the KeyboardManager.
42 // of the KeyboardManager.
43 this.mode = 'command';
43 this.mode = 'command';
44 this.set_dirty(false);
44 this.set_dirty(false);
45 this.metadata = {};
45 this.metadata = {};
46 this._checkpoint_after_save = false;
46 this._checkpoint_after_save = false;
47 this.last_checkpoint = null;
47 this.last_checkpoint = null;
48 this.checkpoints = [];
48 this.checkpoints = [];
49 this.autosave_interval = 0;
49 this.autosave_interval = 0;
50 this.autosave_timer = null;
50 this.autosave_timer = null;
51 // autosave *at most* every two minutes
51 // autosave *at most* every two minutes
52 this.minimum_autosave_interval = 120000;
52 this.minimum_autosave_interval = 120000;
53 // single worksheet for now
53 // single worksheet for now
54 this.worksheet_metadata = {};
54 this.worksheet_metadata = {};
55 this.notebook_name_blacklist_re = /[\/\\:]/;
55 this.notebook_name_blacklist_re = /[\/\\:]/;
56 this.nbformat = 3 // Increment this when changing the nbformat
56 this.nbformat = 3; // Increment this when changing the nbformat
57 this.nbformat_minor = 0 // Increment this when changing the nbformat
57 this.nbformat_minor = 0; // Increment this when changing the nbformat
58 this.style();
58 this.style();
59 this.create_elements();
59 this.create_elements();
60 this.bind_events();
60 this.bind_events();
61 };
61 };
62
62
63 /**
63 /**
64 * Tweak the notebook's CSS style.
64 * Tweak the notebook's CSS style.
65 *
65 *
66 * @method style
66 * @method style
67 */
67 */
68 Notebook.prototype.style = function () {
68 Notebook.prototype.style = function () {
69 $('div#notebook').addClass('border-box-sizing');
69 $('div#notebook').addClass('border-box-sizing');
70 };
70 };
71
71
72 /**
72 /**
73 * Get the root URL of the notebook server.
74 *
75 * @method baseProjectUrl
76 * @return {String} The base project URL
77 */
78 Notebook.prototype.baseProjectUrl = function() {
79 return this._baseProjectUrl || $('body').data('baseProjectUrl');
80 };
81
82 Notebook.prototype.notebookName = function() {
83 return $('body').data('notebookName');
84 };
85
86 Notebook.prototype.notebookPath = function() {
87 return $('body').data('notebookPath');
88 };
89
90 /**
91 * Create an HTML and CSS representation of the notebook.
73 * Create an HTML and CSS representation of the notebook.
92 *
74 *
93 * @method create_elements
75 * @method create_elements
94 */
76 */
95 Notebook.prototype.create_elements = function () {
77 Notebook.prototype.create_elements = function () {
96 var that = this;
78 var that = this;
97 this.element.attr('tabindex','-1');
79 this.element.attr('tabindex','-1');
98 this.container = $("<div/>").addClass("container").attr("id", "notebook-container");
80 this.container = $("<div/>").addClass("container").attr("id", "notebook-container");
99 // We add this end_space div to the end of the notebook div to:
81 // We add this end_space div to the end of the notebook div to:
100 // i) provide a margin between the last cell and the end of the notebook
82 // i) provide a margin between the last cell and the end of the notebook
101 // ii) to prevent the div from scrolling up when the last cell is being
83 // ii) to prevent the div from scrolling up when the last cell is being
102 // edited, but is too low on the page, which browsers will do automatically.
84 // edited, but is too low on the page, which browsers will do automatically.
103 var end_space = $('<div/>').addClass('end_space');
85 var end_space = $('<div/>').addClass('end_space');
104 end_space.dblclick(function (e) {
86 end_space.dblclick(function (e) {
105 var ncells = that.ncells();
87 var ncells = that.ncells();
106 that.insert_cell_below('code',ncells-1);
88 that.insert_cell_below('code',ncells-1);
107 });
89 });
108 this.element.append(this.container);
90 this.element.append(this.container);
109 this.container.append(end_space);
91 this.container.append(end_space);
110 };
92 };
111
93
112 /**
94 /**
113 * Bind JavaScript events: key presses and custom IPython events.
95 * Bind JavaScript events: key presses and custom IPython events.
114 *
96 *
115 * @method bind_events
97 * @method bind_events
116 */
98 */
117 Notebook.prototype.bind_events = function () {
99 Notebook.prototype.bind_events = function () {
118 var that = this;
100 var that = this;
119
101
120 $([IPython.events]).on('set_next_input.Notebook', function (event, data) {
102 $([IPython.events]).on('set_next_input.Notebook', function (event, data) {
121 var index = that.find_cell_index(data.cell);
103 var index = that.find_cell_index(data.cell);
122 var new_cell = that.insert_cell_below('code',index);
104 var new_cell = that.insert_cell_below('code',index);
123 new_cell.set_text(data.text);
105 new_cell.set_text(data.text);
124 that.dirty = true;
106 that.dirty = true;
125 });
107 });
126
108
127 $([IPython.events]).on('set_dirty.Notebook', function (event, data) {
109 $([IPython.events]).on('set_dirty.Notebook', function (event, data) {
128 that.dirty = data.value;
110 that.dirty = data.value;
129 });
111 });
130
112
131 $([IPython.events]).on('select.Cell', function (event, data) {
113 $([IPython.events]).on('select.Cell', function (event, data) {
132 var index = that.find_cell_index(data.cell);
114 var index = that.find_cell_index(data.cell);
133 that.select(index);
115 that.select(index);
134 });
116 });
135
117
136 $([IPython.events]).on('edit_mode.Cell', function (event, data) {
118 $([IPython.events]).on('edit_mode.Cell', function (event, data) {
137 var index = that.find_cell_index(data.cell);
119 var index = that.find_cell_index(data.cell);
138 that.select(index);
120 that.select(index);
139 that.edit_mode();
121 that.edit_mode();
140 });
122 });
141
123
142 $([IPython.events]).on('command_mode.Cell', function (event, data) {
124 $([IPython.events]).on('command_mode.Cell', function (event, data) {
143 that.command_mode();
125 that.command_mode();
144 });
126 });
145
127
146 $([IPython.events]).on('status_autorestarting.Kernel', function () {
128 $([IPython.events]).on('status_autorestarting.Kernel', function () {
147 IPython.dialog.modal({
129 IPython.dialog.modal({
148 title: "Kernel Restarting",
130 title: "Kernel Restarting",
149 body: "The kernel appears to have died. It will restart automatically.",
131 body: "The kernel appears to have died. It will restart automatically.",
150 buttons: {
132 buttons: {
151 OK : {
133 OK : {
152 class : "btn-primary"
134 class : "btn-primary"
153 }
135 }
154 }
136 }
155 });
137 });
156 });
138 });
157
139
158 var collapse_time = function (time) {
140 var collapse_time = function (time) {
159 var app_height = $('#ipython-main-app').height(); // content height
141 var app_height = $('#ipython-main-app').height(); // content height
160 var splitter_height = $('div#pager_splitter').outerHeight(true);
142 var splitter_height = $('div#pager_splitter').outerHeight(true);
161 var new_height = app_height - splitter_height;
143 var new_height = app_height - splitter_height;
162 that.element.animate({height : new_height + 'px'}, time);
144 that.element.animate({height : new_height + 'px'}, time);
163 };
145 };
164
146
165 this.element.bind('collapse_pager', function (event, extrap) {
147 this.element.bind('collapse_pager', function (event, extrap) {
166 var time = (extrap != undefined) ? ((extrap.duration != undefined ) ? extrap.duration : 'fast') : 'fast';
148 var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
167 collapse_time(time);
149 collapse_time(time);
168 });
150 });
169
151
170 var expand_time = function (time) {
152 var expand_time = function (time) {
171 var app_height = $('#ipython-main-app').height(); // content height
153 var app_height = $('#ipython-main-app').height(); // content height
172 var splitter_height = $('div#pager_splitter').outerHeight(true);
154 var splitter_height = $('div#pager_splitter').outerHeight(true);
173 var pager_height = $('div#pager').outerHeight(true);
155 var pager_height = $('div#pager').outerHeight(true);
174 var new_height = app_height - pager_height - splitter_height;
156 var new_height = app_height - pager_height - splitter_height;
175 that.element.animate({height : new_height + 'px'}, time);
157 that.element.animate({height : new_height + 'px'}, time);
176 };
158 };
177
159
178 this.element.bind('expand_pager', function (event, extrap) {
160 this.element.bind('expand_pager', function (event, extrap) {
179 var time = (extrap != undefined) ? ((extrap.duration != undefined ) ? extrap.duration : 'fast') : 'fast';
161 var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
180 expand_time(time);
162 expand_time(time);
181 });
163 });
182
164
183 // Firefox 22 broke $(window).on("beforeunload")
165 // Firefox 22 broke $(window).on("beforeunload")
184 // I'm not sure why or how.
166 // I'm not sure why or how.
185 window.onbeforeunload = function (e) {
167 window.onbeforeunload = function (e) {
186 // TODO: Make killing the kernel configurable.
168 // TODO: Make killing the kernel configurable.
187 var kill_kernel = false;
169 var kill_kernel = false;
188 if (kill_kernel) {
170 if (kill_kernel) {
189 that.session.kill_kernel();
171 that.session.kill_kernel();
190 }
172 }
191 // if we are autosaving, trigger an autosave on nav-away.
173 // if we are autosaving, trigger an autosave on nav-away.
192 // still warn, because if we don't the autosave may fail.
174 // still warn, because if we don't the autosave may fail.
193 if (that.dirty) {
175 if (that.dirty) {
194 if ( that.autosave_interval ) {
176 if ( that.autosave_interval ) {
195 // schedule autosave in a timeout
177 // schedule autosave in a timeout
196 // this gives you a chance to forcefully discard changes
178 // this gives you a chance to forcefully discard changes
197 // by reloading the page if you *really* want to.
179 // by reloading the page if you *really* want to.
198 // the timer doesn't start until you *dismiss* the dialog.
180 // the timer doesn't start until you *dismiss* the dialog.
199 setTimeout(function () {
181 setTimeout(function () {
200 if (that.dirty) {
182 if (that.dirty) {
201 that.save_notebook();
183 that.save_notebook();
202 }
184 }
203 }, 1000);
185 }, 1000);
204 return "Autosave in progress, latest changes may be lost.";
186 return "Autosave in progress, latest changes may be lost.";
205 } else {
187 } else {
206 return "Unsaved changes will be lost.";
188 return "Unsaved changes will be lost.";
207 }
189 }
208 };
190 }
209 // Null is the *only* return value that will make the browser not
191 // Null is the *only* return value that will make the browser not
210 // pop up the "don't leave" dialog.
192 // pop up the "don't leave" dialog.
211 return null;
193 return null;
212 };
194 };
213 };
195 };
214
196
215 /**
197 /**
216 * Set the dirty flag, and trigger the set_dirty.Notebook event
198 * Set the dirty flag, and trigger the set_dirty.Notebook event
217 *
199 *
218 * @method set_dirty
200 * @method set_dirty
219 */
201 */
220 Notebook.prototype.set_dirty = function (value) {
202 Notebook.prototype.set_dirty = function (value) {
221 if (value === undefined) {
203 if (value === undefined) {
222 value = true;
204 value = true;
223 }
205 }
224 if (this.dirty == value) {
206 if (this.dirty == value) {
225 return;
207 return;
226 }
208 }
227 $([IPython.events]).trigger('set_dirty.Notebook', {value: value});
209 $([IPython.events]).trigger('set_dirty.Notebook', {value: value});
228 };
210 };
229
211
230 /**
212 /**
231 * Scroll the top of the page to a given cell.
213 * Scroll the top of the page to a given cell.
232 *
214 *
233 * @method scroll_to_cell
215 * @method scroll_to_cell
234 * @param {Number} cell_number An index of the cell to view
216 * @param {Number} cell_number An index of the cell to view
235 * @param {Number} time Animation time in milliseconds
217 * @param {Number} time Animation time in milliseconds
236 * @return {Number} Pixel offset from the top of the container
218 * @return {Number} Pixel offset from the top of the container
237 */
219 */
238 Notebook.prototype.scroll_to_cell = function (cell_number, time) {
220 Notebook.prototype.scroll_to_cell = function (cell_number, time) {
239 var cells = this.get_cells();
221 var cells = this.get_cells();
240 var time = time || 0;
222 time = time || 0;
241 cell_number = Math.min(cells.length-1,cell_number);
223 cell_number = Math.min(cells.length-1,cell_number);
242 cell_number = Math.max(0 ,cell_number);
224 cell_number = Math.max(0 ,cell_number);
243 var scroll_value = cells[cell_number].element.position().top-cells[0].element.position().top ;
225 var scroll_value = cells[cell_number].element.position().top-cells[0].element.position().top ;
244 this.element.animate({scrollTop:scroll_value}, time);
226 this.element.animate({scrollTop:scroll_value}, time);
245 return scroll_value;
227 return scroll_value;
246 };
228 };
247
229
248 /**
230 /**
249 * Scroll to the bottom of the page.
231 * Scroll to the bottom of the page.
250 *
232 *
251 * @method scroll_to_bottom
233 * @method scroll_to_bottom
252 */
234 */
253 Notebook.prototype.scroll_to_bottom = function () {
235 Notebook.prototype.scroll_to_bottom = function () {
254 this.element.animate({scrollTop:this.element.get(0).scrollHeight}, 0);
236 this.element.animate({scrollTop:this.element.get(0).scrollHeight}, 0);
255 };
237 };
256
238
257 /**
239 /**
258 * Scroll to the top of the page.
240 * Scroll to the top of the page.
259 *
241 *
260 * @method scroll_to_top
242 * @method scroll_to_top
261 */
243 */
262 Notebook.prototype.scroll_to_top = function () {
244 Notebook.prototype.scroll_to_top = function () {
263 this.element.animate({scrollTop:0}, 0);
245 this.element.animate({scrollTop:0}, 0);
264 };
246 };
265
247
266 // Edit Notebook metadata
248 // Edit Notebook metadata
267
249
268 Notebook.prototype.edit_metadata = function () {
250 Notebook.prototype.edit_metadata = function () {
269 var that = this;
251 var that = this;
270 IPython.dialog.edit_metadata(this.metadata, function (md) {
252 IPython.dialog.edit_metadata(this.metadata, function (md) {
271 that.metadata = md;
253 that.metadata = md;
272 }, 'Notebook');
254 }, 'Notebook');
273 };
255 };
274
256
275 // Cell indexing, retrieval, etc.
257 // Cell indexing, retrieval, etc.
276
258
277 /**
259 /**
278 * Get all cell elements in the notebook.
260 * Get all cell elements in the notebook.
279 *
261 *
280 * @method get_cell_elements
262 * @method get_cell_elements
281 * @return {jQuery} A selector of all cell elements
263 * @return {jQuery} A selector of all cell elements
282 */
264 */
283 Notebook.prototype.get_cell_elements = function () {
265 Notebook.prototype.get_cell_elements = function () {
284 return this.container.children("div.cell");
266 return this.container.children("div.cell");
285 };
267 };
286
268
287 /**
269 /**
288 * Get a particular cell element.
270 * Get a particular cell element.
289 *
271 *
290 * @method get_cell_element
272 * @method get_cell_element
291 * @param {Number} index An index of a cell to select
273 * @param {Number} index An index of a cell to select
292 * @return {jQuery} A selector of the given cell.
274 * @return {jQuery} A selector of the given cell.
293 */
275 */
294 Notebook.prototype.get_cell_element = function (index) {
276 Notebook.prototype.get_cell_element = function (index) {
295 var result = null;
277 var result = null;
296 var e = this.get_cell_elements().eq(index);
278 var e = this.get_cell_elements().eq(index);
297 if (e.length !== 0) {
279 if (e.length !== 0) {
298 result = e;
280 result = e;
299 }
281 }
300 return result;
282 return result;
301 };
283 };
302
284
303 /**
285 /**
304 * Try to get a particular cell by msg_id.
286 * Try to get a particular cell by msg_id.
305 *
287 *
306 * @method get_msg_cell
288 * @method get_msg_cell
307 * @param {String} msg_id A message UUID
289 * @param {String} msg_id A message UUID
308 * @return {Cell} Cell or null if no cell was found.
290 * @return {Cell} Cell or null if no cell was found.
309 */
291 */
310 Notebook.prototype.get_msg_cell = function (msg_id) {
292 Notebook.prototype.get_msg_cell = function (msg_id) {
311 return IPython.CodeCell.msg_cells[msg_id] || null;
293 return IPython.CodeCell.msg_cells[msg_id] || null;
312 };
294 };
313
295
314 /**
296 /**
315 * Count the cells in this notebook.
297 * Count the cells in this notebook.
316 *
298 *
317 * @method ncells
299 * @method ncells
318 * @return {Number} The number of cells in this notebook
300 * @return {Number} The number of cells in this notebook
319 */
301 */
320 Notebook.prototype.ncells = function () {
302 Notebook.prototype.ncells = function () {
321 return this.get_cell_elements().length;
303 return this.get_cell_elements().length;
322 };
304 };
323
305
324 /**
306 /**
325 * Get all Cell objects in this notebook.
307 * Get all Cell objects in this notebook.
326 *
308 *
327 * @method get_cells
309 * @method get_cells
328 * @return {Array} This notebook's Cell objects
310 * @return {Array} This notebook's Cell objects
329 */
311 */
330 // TODO: we are often calling cells as cells()[i], which we should optimize
312 // TODO: we are often calling cells as cells()[i], which we should optimize
331 // to cells(i) or a new method.
313 // to cells(i) or a new method.
332 Notebook.prototype.get_cells = function () {
314 Notebook.prototype.get_cells = function () {
333 return this.get_cell_elements().toArray().map(function (e) {
315 return this.get_cell_elements().toArray().map(function (e) {
334 return $(e).data("cell");
316 return $(e).data("cell");
335 });
317 });
336 };
318 };
337
319
338 /**
320 /**
339 * Get a Cell object from this notebook.
321 * Get a Cell object from this notebook.
340 *
322 *
341 * @method get_cell
323 * @method get_cell
342 * @param {Number} index An index of a cell to retrieve
324 * @param {Number} index An index of a cell to retrieve
343 * @return {Cell} A particular cell
325 * @return {Cell} A particular cell
344 */
326 */
345 Notebook.prototype.get_cell = function (index) {
327 Notebook.prototype.get_cell = function (index) {
346 var result = null;
328 var result = null;
347 var ce = this.get_cell_element(index);
329 var ce = this.get_cell_element(index);
348 if (ce !== null) {
330 if (ce !== null) {
349 result = ce.data('cell');
331 result = ce.data('cell');
350 }
332 }
351 return result;
333 return result;
352 }
334 };
353
335
354 /**
336 /**
355 * Get the cell below a given cell.
337 * Get the cell below a given cell.
356 *
338 *
357 * @method get_next_cell
339 * @method get_next_cell
358 * @param {Cell} cell The provided cell
340 * @param {Cell} cell The provided cell
359 * @return {Cell} The next cell
341 * @return {Cell} The next cell
360 */
342 */
361 Notebook.prototype.get_next_cell = function (cell) {
343 Notebook.prototype.get_next_cell = function (cell) {
362 var result = null;
344 var result = null;
363 var index = this.find_cell_index(cell);
345 var index = this.find_cell_index(cell);
364 if (this.is_valid_cell_index(index+1)) {
346 if (this.is_valid_cell_index(index+1)) {
365 result = this.get_cell(index+1);
347 result = this.get_cell(index+1);
366 }
348 }
367 return result;
349 return result;
368 }
350 };
369
351
370 /**
352 /**
371 * Get the cell above a given cell.
353 * Get the cell above a given cell.
372 *
354 *
373 * @method get_prev_cell
355 * @method get_prev_cell
374 * @param {Cell} cell The provided cell
356 * @param {Cell} cell The provided cell
375 * @return {Cell} The previous cell
357 * @return {Cell} The previous cell
376 */
358 */
377 Notebook.prototype.get_prev_cell = function (cell) {
359 Notebook.prototype.get_prev_cell = function (cell) {
378 // TODO: off-by-one
360 // TODO: off-by-one
379 // nb.get_prev_cell(nb.get_cell(1)) is null
361 // nb.get_prev_cell(nb.get_cell(1)) is null
380 var result = null;
362 var result = null;
381 var index = this.find_cell_index(cell);
363 var index = this.find_cell_index(cell);
382 if (index !== null && index > 1) {
364 if (index !== null && index > 1) {
383 result = this.get_cell(index-1);
365 result = this.get_cell(index-1);
384 }
366 }
385 return result;
367 return result;
386 }
368 };
387
369
388 /**
370 /**
389 * Get the numeric index of a given cell.
371 * Get the numeric index of a given cell.
390 *
372 *
391 * @method find_cell_index
373 * @method find_cell_index
392 * @param {Cell} cell The provided cell
374 * @param {Cell} cell The provided cell
393 * @return {Number} The cell's numeric index
375 * @return {Number} The cell's numeric index
394 */
376 */
395 Notebook.prototype.find_cell_index = function (cell) {
377 Notebook.prototype.find_cell_index = function (cell) {
396 var result = null;
378 var result = null;
397 this.get_cell_elements().filter(function (index) {
379 this.get_cell_elements().filter(function (index) {
398 if ($(this).data("cell") === cell) {
380 if ($(this).data("cell") === cell) {
399 result = index;
381 result = index;
400 };
382 }
401 });
383 });
402 return result;
384 return result;
403 };
385 };
404
386
405 /**
387 /**
406 * Get a given index , or the selected index if none is provided.
388 * Get a given index , or the selected index if none is provided.
407 *
389 *
408 * @method index_or_selected
390 * @method index_or_selected
409 * @param {Number} index A cell's index
391 * @param {Number} index A cell's index
410 * @return {Number} The given index, or selected index if none is provided.
392 * @return {Number} The given index, or selected index if none is provided.
411 */
393 */
412 Notebook.prototype.index_or_selected = function (index) {
394 Notebook.prototype.index_or_selected = function (index) {
413 var i;
395 var i;
414 if (index === undefined || index === null) {
396 if (index === undefined || index === null) {
415 i = this.get_selected_index();
397 i = this.get_selected_index();
416 if (i === null) {
398 if (i === null) {
417 i = 0;
399 i = 0;
418 }
400 }
419 } else {
401 } else {
420 i = index;
402 i = index;
421 }
403 }
422 return i;
404 return i;
423 };
405 };
424
406
425 /**
407 /**
426 * Get the currently selected cell.
408 * Get the currently selected cell.
427 * @method get_selected_cell
409 * @method get_selected_cell
428 * @return {Cell} The selected cell
410 * @return {Cell} The selected cell
429 */
411 */
430 Notebook.prototype.get_selected_cell = function () {
412 Notebook.prototype.get_selected_cell = function () {
431 var index = this.get_selected_index();
413 var index = this.get_selected_index();
432 return this.get_cell(index);
414 return this.get_cell(index);
433 };
415 };
434
416
435 /**
417 /**
436 * Check whether a cell index is valid.
418 * Check whether a cell index is valid.
437 *
419 *
438 * @method is_valid_cell_index
420 * @method is_valid_cell_index
439 * @param {Number} index A cell index
421 * @param {Number} index A cell index
440 * @return True if the index is valid, false otherwise
422 * @return True if the index is valid, false otherwise
441 */
423 */
442 Notebook.prototype.is_valid_cell_index = function (index) {
424 Notebook.prototype.is_valid_cell_index = function (index) {
443 if (index !== null && index >= 0 && index < this.ncells()) {
425 if (index !== null && index >= 0 && index < this.ncells()) {
444 return true;
426 return true;
445 } else {
427 } else {
446 return false;
428 return false;
447 };
429 }
448 }
430 };
449
431
450 /**
432 /**
451 * Get the index of the currently selected cell.
433 * Get the index of the currently selected cell.
452
434
453 * @method get_selected_index
435 * @method get_selected_index
454 * @return {Number} The selected cell's numeric index
436 * @return {Number} The selected cell's numeric index
455 */
437 */
456 Notebook.prototype.get_selected_index = function () {
438 Notebook.prototype.get_selected_index = function () {
457 var result = null;
439 var result = null;
458 this.get_cell_elements().filter(function (index) {
440 this.get_cell_elements().filter(function (index) {
459 if ($(this).data("cell").selected === true) {
441 if ($(this).data("cell").selected === true) {
460 result = index;
442 result = index;
461 };
443 }
462 });
444 });
463 return result;
445 return result;
464 };
446 };
465
447
466
448
467 // Cell selection.
449 // Cell selection.
468
450
469 /**
451 /**
470 * Programmatically select a cell.
452 * Programmatically select a cell.
471 *
453 *
472 * @method select
454 * @method select
473 * @param {Number} index A cell's index
455 * @param {Number} index A cell's index
474 * @return {Notebook} This notebook
456 * @return {Notebook} This notebook
475 */
457 */
476 Notebook.prototype.select = function (index) {
458 Notebook.prototype.select = function (index) {
477 if (this.is_valid_cell_index(index)) {
459 if (this.is_valid_cell_index(index)) {
478 var sindex = this.get_selected_index()
460 var sindex = this.get_selected_index();
479 if (sindex !== null && index !== sindex) {
461 if (sindex !== null && index !== sindex) {
480 this.command_mode();
462 this.command_mode();
481 this.get_cell(sindex).unselect();
463 this.get_cell(sindex).unselect();
482 };
464 }
483 var cell = this.get_cell(index);
465 var cell = this.get_cell(index);
484 cell.select();
466 cell.select();
485 if (cell.cell_type === 'heading') {
467 if (cell.cell_type === 'heading') {
486 $([IPython.events]).trigger('selected_cell_type_changed.Notebook',
468 $([IPython.events]).trigger('selected_cell_type_changed.Notebook',
487 {'cell_type':cell.cell_type,level:cell.level}
469 {'cell_type':cell.cell_type,level:cell.level}
488 );
470 );
489 } else {
471 } else {
490 $([IPython.events]).trigger('selected_cell_type_changed.Notebook',
472 $([IPython.events]).trigger('selected_cell_type_changed.Notebook',
491 {'cell_type':cell.cell_type}
473 {'cell_type':cell.cell_type}
492 );
474 );
493 };
475 }
494 };
476 }
495 return this;
477 return this;
496 };
478 };
497
479
498 /**
480 /**
499 * Programmatically select the next cell.
481 * Programmatically select the next cell.
500 *
482 *
501 * @method select_next
483 * @method select_next
502 * @return {Notebook} This notebook
484 * @return {Notebook} This notebook
503 */
485 */
504 Notebook.prototype.select_next = function () {
486 Notebook.prototype.select_next = function () {
505 var index = this.get_selected_index();
487 var index = this.get_selected_index();
506 this.select(index+1);
488 this.select(index+1);
507 return this;
489 return this;
508 };
490 };
509
491
510 /**
492 /**
511 * Programmatically select the previous cell.
493 * Programmatically select the previous cell.
512 *
494 *
513 * @method select_prev
495 * @method select_prev
514 * @return {Notebook} This notebook
496 * @return {Notebook} This notebook
515 */
497 */
516 Notebook.prototype.select_prev = function () {
498 Notebook.prototype.select_prev = function () {
517 var index = this.get_selected_index();
499 var index = this.get_selected_index();
518 this.select(index-1);
500 this.select(index-1);
519 return this;
501 return this;
520 };
502 };
521
503
522
504
523 // Edit/Command mode
505 // Edit/Command mode
524
506
525 Notebook.prototype.get_edit_index = function () {
507 Notebook.prototype.get_edit_index = function () {
526 var result = null;
508 var result = null;
527 this.get_cell_elements().filter(function (index) {
509 this.get_cell_elements().filter(function (index) {
528 if ($(this).data("cell").mode === 'edit') {
510 if ($(this).data("cell").mode === 'edit') {
529 result = index;
511 result = index;
530 };
512 }
531 });
513 });
532 return result;
514 return result;
533 };
515 };
534
516
535 Notebook.prototype.command_mode = function () {
517 Notebook.prototype.command_mode = function () {
536 if (this.mode !== 'command') {
518 if (this.mode !== 'command') {
537 $([IPython.events]).trigger('command_mode.Notebook');
519 $([IPython.events]).trigger('command_mode.Notebook');
538 var index = this.get_edit_index();
520 var index = this.get_edit_index();
539 var cell = this.get_cell(index);
521 var cell = this.get_cell(index);
540 if (cell) {
522 if (cell) {
541 cell.command_mode();
523 cell.command_mode();
542 };
524 }
543 this.mode = 'command';
525 this.mode = 'command';
544 IPython.keyboard_manager.command_mode();
526 IPython.keyboard_manager.command_mode();
545 };
527 }
546 };
528 };
547
529
548 Notebook.prototype.edit_mode = function () {
530 Notebook.prototype.edit_mode = function () {
549 if (this.mode !== 'edit') {
531 if (this.mode !== 'edit') {
550 $([IPython.events]).trigger('edit_mode.Notebook');
532 $([IPython.events]).trigger('edit_mode.Notebook');
551 var cell = this.get_selected_cell();
533 var cell = this.get_selected_cell();
552 if (cell === null) {return;} // No cell is selected
534 if (cell === null) {return;} // No cell is selected
553 // We need to set the mode to edit to prevent reentering this method
535 // We need to set the mode to edit to prevent reentering this method
554 // when cell.edit_mode() is called below.
536 // when cell.edit_mode() is called below.
555 this.mode = 'edit';
537 this.mode = 'edit';
556 IPython.keyboard_manager.edit_mode();
538 IPython.keyboard_manager.edit_mode();
557 cell.edit_mode();
539 cell.edit_mode();
558 };
540 }
559 };
541 };
560
542
561 Notebook.prototype.focus_cell = function () {
543 Notebook.prototype.focus_cell = function () {
562 var cell = this.get_selected_cell();
544 var cell = this.get_selected_cell();
563 if (cell === null) {return;} // No cell is selected
545 if (cell === null) {return;} // No cell is selected
564 cell.focus_cell();
546 cell.focus_cell();
565 };
547 };
566
548
567 // Cell movement
549 // Cell movement
568
550
569 /**
551 /**
570 * Move given (or selected) cell up and select it.
552 * Move given (or selected) cell up and select it.
571 *
553 *
572 * @method move_cell_up
554 * @method move_cell_up
573 * @param [index] {integer} cell index
555 * @param [index] {integer} cell index
574 * @return {Notebook} This notebook
556 * @return {Notebook} This notebook
575 **/
557 **/
576 Notebook.prototype.move_cell_up = function (index) {
558 Notebook.prototype.move_cell_up = function (index) {
577 var i = this.index_or_selected(index);
559 var i = this.index_or_selected(index);
578 if (this.is_valid_cell_index(i) && i > 0) {
560 if (this.is_valid_cell_index(i) && i > 0) {
579 var pivot = this.get_cell_element(i-1);
561 var pivot = this.get_cell_element(i-1);
580 var tomove = this.get_cell_element(i);
562 var tomove = this.get_cell_element(i);
581 if (pivot !== null && tomove !== null) {
563 if (pivot !== null && tomove !== null) {
582 tomove.detach();
564 tomove.detach();
583 pivot.before(tomove);
565 pivot.before(tomove);
584 this.select(i-1);
566 this.select(i-1);
585 var cell = this.get_selected_cell();
567 var cell = this.get_selected_cell();
586 cell.focus_cell();
568 cell.focus_cell();
587 };
569 }
588 this.set_dirty(true);
570 this.set_dirty(true);
589 };
571 }
590 return this;
572 return this;
591 };
573 };
592
574
593
575
594 /**
576 /**
595 * Move given (or selected) cell down and select it
577 * Move given (or selected) cell down and select it
596 *
578 *
597 * @method move_cell_down
579 * @method move_cell_down
598 * @param [index] {integer} cell index
580 * @param [index] {integer} cell index
599 * @return {Notebook} This notebook
581 * @return {Notebook} This notebook
600 **/
582 **/
601 Notebook.prototype.move_cell_down = function (index) {
583 Notebook.prototype.move_cell_down = function (index) {
602 var i = this.index_or_selected(index);
584 var i = this.index_or_selected(index);
603 if (this.is_valid_cell_index(i) && this.is_valid_cell_index(i+1)) {
585 if (this.is_valid_cell_index(i) && this.is_valid_cell_index(i+1)) {
604 var pivot = this.get_cell_element(i+1);
586 var pivot = this.get_cell_element(i+1);
605 var tomove = this.get_cell_element(i);
587 var tomove = this.get_cell_element(i);
606 if (pivot !== null && tomove !== null) {
588 if (pivot !== null && tomove !== null) {
607 tomove.detach();
589 tomove.detach();
608 pivot.after(tomove);
590 pivot.after(tomove);
609 this.select(i+1);
591 this.select(i+1);
610 var cell = this.get_selected_cell();
592 var cell = this.get_selected_cell();
611 cell.focus_cell();
593 cell.focus_cell();
612 };
594 }
613 };
595 }
614 this.set_dirty();
596 this.set_dirty();
615 return this;
597 return this;
616 };
598 };
617
599
618
600
619 // Insertion, deletion.
601 // Insertion, deletion.
620
602
621 /**
603 /**
622 * Delete a cell from the notebook.
604 * Delete a cell from the notebook.
623 *
605 *
624 * @method delete_cell
606 * @method delete_cell
625 * @param [index] A cell's numeric index
607 * @param [index] A cell's numeric index
626 * @return {Notebook} This notebook
608 * @return {Notebook} This notebook
627 */
609 */
628 Notebook.prototype.delete_cell = function (index) {
610 Notebook.prototype.delete_cell = function (index) {
629 var i = this.index_or_selected(index);
611 var i = this.index_or_selected(index);
630 var cell = this.get_selected_cell();
612 var cell = this.get_selected_cell();
631 this.undelete_backup = cell.toJSON();
613 this.undelete_backup = cell.toJSON();
632 $('#undelete_cell').removeClass('disabled');
614 $('#undelete_cell').removeClass('disabled');
633 if (this.is_valid_cell_index(i)) {
615 if (this.is_valid_cell_index(i)) {
634 var old_ncells = this.ncells();
616 var old_ncells = this.ncells();
635 var ce = this.get_cell_element(i);
617 var ce = this.get_cell_element(i);
636 ce.remove();
618 ce.remove();
637 if (i === 0) {
619 if (i === 0) {
638 // Always make sure we have at least one cell.
620 // Always make sure we have at least one cell.
639 if (old_ncells === 1) {
621 if (old_ncells === 1) {
640 this.insert_cell_below('code');
622 this.insert_cell_below('code');
641 }
623 }
642 this.select(0);
624 this.select(0);
643 this.undelete_index = 0;
625 this.undelete_index = 0;
644 this.undelete_below = false;
626 this.undelete_below = false;
645 } else if (i === old_ncells-1 && i !== 0) {
627 } else if (i === old_ncells-1 && i !== 0) {
646 this.select(i-1);
628 this.select(i-1);
647 this.undelete_index = i - 1;
629 this.undelete_index = i - 1;
648 this.undelete_below = true;
630 this.undelete_below = true;
649 } else {
631 } else {
650 this.select(i);
632 this.select(i);
651 this.undelete_index = i;
633 this.undelete_index = i;
652 this.undelete_below = false;
634 this.undelete_below = false;
653 };
635 }
654 $([IPython.events]).trigger('delete.Cell', {'cell': cell, 'index': i});
636 $([IPython.events]).trigger('delete.Cell', {'cell': cell, 'index': i});
655 this.set_dirty(true);
637 this.set_dirty(true);
656 };
638 }
657 return this;
639 return this;
658 };
640 };
659
641
660 /**
642 /**
661 * Restore the most recently deleted cell.
643 * Restore the most recently deleted cell.
662 *
644 *
663 * @method undelete
645 * @method undelete
664 */
646 */
665 Notebook.prototype.undelete_cell = function() {
647 Notebook.prototype.undelete_cell = function() {
666 if (this.undelete_backup !== null && this.undelete_index !== null) {
648 if (this.undelete_backup !== null && this.undelete_index !== null) {
667 var current_index = this.get_selected_index();
649 var current_index = this.get_selected_index();
668 if (this.undelete_index < current_index) {
650 if (this.undelete_index < current_index) {
669 current_index = current_index + 1;
651 current_index = current_index + 1;
670 }
652 }
671 if (this.undelete_index >= this.ncells()) {
653 if (this.undelete_index >= this.ncells()) {
672 this.select(this.ncells() - 1);
654 this.select(this.ncells() - 1);
673 }
655 }
674 else {
656 else {
675 this.select(this.undelete_index);
657 this.select(this.undelete_index);
676 }
658 }
677 var cell_data = this.undelete_backup;
659 var cell_data = this.undelete_backup;
678 var new_cell = null;
660 var new_cell = null;
679 if (this.undelete_below) {
661 if (this.undelete_below) {
680 new_cell = this.insert_cell_below(cell_data.cell_type);
662 new_cell = this.insert_cell_below(cell_data.cell_type);
681 } else {
663 } else {
682 new_cell = this.insert_cell_above(cell_data.cell_type);
664 new_cell = this.insert_cell_above(cell_data.cell_type);
683 }
665 }
684 new_cell.fromJSON(cell_data);
666 new_cell.fromJSON(cell_data);
685 if (this.undelete_below) {
667 if (this.undelete_below) {
686 this.select(current_index+1);
668 this.select(current_index+1);
687 } else {
669 } else {
688 this.select(current_index);
670 this.select(current_index);
689 }
671 }
690 this.undelete_backup = null;
672 this.undelete_backup = null;
691 this.undelete_index = null;
673 this.undelete_index = null;
692 }
674 }
693 $('#undelete_cell').addClass('disabled');
675 $('#undelete_cell').addClass('disabled');
694 }
676 };
695
677
696 /**
678 /**
697 * Insert a cell so that after insertion the cell is at given index.
679 * Insert a cell so that after insertion the cell is at given index.
698 *
680 *
699 * Similar to insert_above, but index parameter is mandatory
681 * Similar to insert_above, but index parameter is mandatory
700 *
682 *
701 * Index will be brought back into the accissible range [0,n]
683 * Index will be brought back into the accissible range [0,n]
702 *
684 *
703 * @method insert_cell_at_index
685 * @method insert_cell_at_index
704 * @param type {string} in ['code','markdown','heading']
686 * @param type {string} in ['code','markdown','heading']
705 * @param [index] {int} a valid index where to inser cell
687 * @param [index] {int} a valid index where to inser cell
706 *
688 *
707 * @return cell {cell|null} created cell or null
689 * @return cell {cell|null} created cell or null
708 **/
690 **/
709 Notebook.prototype.insert_cell_at_index = function(type, index){
691 Notebook.prototype.insert_cell_at_index = function(type, index){
710
692
711 var ncells = this.ncells();
693 var ncells = this.ncells();
712 var index = Math.min(index,ncells);
694 index = Math.min(index,ncells);
713 index = Math.max(index,0);
695 index = Math.max(index,0);
714 var cell = null;
696 var cell = null;
715
697
716 if (ncells === 0 || this.is_valid_cell_index(index) || index === ncells) {
698 if (ncells === 0 || this.is_valid_cell_index(index) || index === ncells) {
717 if (type === 'code') {
699 if (type === 'code') {
718 cell = new IPython.CodeCell(this.kernel);
700 cell = new IPython.CodeCell(this.kernel);
719 cell.set_input_prompt();
701 cell.set_input_prompt();
720 } else if (type === 'markdown') {
702 } else if (type === 'markdown') {
721 cell = new IPython.MarkdownCell();
703 cell = new IPython.MarkdownCell();
722 } else if (type === 'raw') {
704 } else if (type === 'raw') {
723 cell = new IPython.RawCell();
705 cell = new IPython.RawCell();
724 } else if (type === 'heading') {
706 } else if (type === 'heading') {
725 cell = new IPython.HeadingCell();
707 cell = new IPython.HeadingCell();
726 }
708 }
727
709
728 if(this._insert_element_at_index(cell.element,index)) {
710 if(this._insert_element_at_index(cell.element,index)) {
729 cell.render();
711 cell.render();
730 $([IPython.events]).trigger('create.Cell', {'cell': cell, 'index': index});
712 $([IPython.events]).trigger('create.Cell', {'cell': cell, 'index': index});
731 cell.refresh();
713 cell.refresh();
732 // We used to select the cell after we refresh it, but there
714 // We used to select the cell after we refresh it, but there
733 // are now cases were this method is called where select is
715 // are now cases were this method is called where select is
734 // not appropriate. The selection logic should be handled by the
716 // not appropriate. The selection logic should be handled by the
735 // caller of the the top level insert_cell methods.
717 // caller of the the top level insert_cell methods.
736 this.set_dirty(true);
718 this.set_dirty(true);
737 }
719 }
738 }
720 }
739 return cell;
721 return cell;
740
722
741 };
723 };
742
724
743 /**
725 /**
744 * Insert an element at given cell index.
726 * Insert an element at given cell index.
745 *
727 *
746 * @method _insert_element_at_index
728 * @method _insert_element_at_index
747 * @param element {dom element} a cell element
729 * @param element {dom element} a cell element
748 * @param [index] {int} a valid index where to inser cell
730 * @param [index] {int} a valid index where to inser cell
749 * @private
731 * @private
750 *
732 *
751 * return true if everything whent fine.
733 * return true if everything whent fine.
752 **/
734 **/
753 Notebook.prototype._insert_element_at_index = function(element, index){
735 Notebook.prototype._insert_element_at_index = function(element, index){
754 if (element === undefined){
736 if (element === undefined){
755 return false;
737 return false;
756 }
738 }
757
739
758 var ncells = this.ncells();
740 var ncells = this.ncells();
759
741
760 if (ncells === 0) {
742 if (ncells === 0) {
761 // special case append if empty
743 // special case append if empty
762 this.element.find('div.end_space').before(element);
744 this.element.find('div.end_space').before(element);
763 } else if ( ncells === index ) {
745 } else if ( ncells === index ) {
764 // special case append it the end, but not empty
746 // special case append it the end, but not empty
765 this.get_cell_element(index-1).after(element);
747 this.get_cell_element(index-1).after(element);
766 } else if (this.is_valid_cell_index(index)) {
748 } else if (this.is_valid_cell_index(index)) {
767 // otherwise always somewhere to append to
749 // otherwise always somewhere to append to
768 this.get_cell_element(index).before(element);
750 this.get_cell_element(index).before(element);
769 } else {
751 } else {
770 return false;
752 return false;
771 }
753 }
772
754
773 if (this.undelete_index !== null && index <= this.undelete_index) {
755 if (this.undelete_index !== null && index <= this.undelete_index) {
774 this.undelete_index = this.undelete_index + 1;
756 this.undelete_index = this.undelete_index + 1;
775 this.set_dirty(true);
757 this.set_dirty(true);
776 }
758 }
777 return true;
759 return true;
778 };
760 };
779
761
780 /**
762 /**
781 * Insert a cell of given type above given index, or at top
763 * Insert a cell of given type above given index, or at top
782 * of notebook if index smaller than 0.
764 * of notebook if index smaller than 0.
783 *
765 *
784 * default index value is the one of currently selected cell
766 * default index value is the one of currently selected cell
785 *
767 *
786 * @method insert_cell_above
768 * @method insert_cell_above
787 * @param type {string} cell type
769 * @param type {string} cell type
788 * @param [index] {integer}
770 * @param [index] {integer}
789 *
771 *
790 * @return handle to created cell or null
772 * @return handle to created cell or null
791 **/
773 **/
792 Notebook.prototype.insert_cell_above = function (type, index) {
774 Notebook.prototype.insert_cell_above = function (type, index) {
793 index = this.index_or_selected(index);
775 index = this.index_or_selected(index);
794 return this.insert_cell_at_index(type, index);
776 return this.insert_cell_at_index(type, index);
795 };
777 };
796
778
797 /**
779 /**
798 * Insert a cell of given type below given index, or at bottom
780 * Insert a cell of given type below given index, or at bottom
799 * of notebook if index greater thatn number of cell
781 * of notebook if index greater thatn number of cell
800 *
782 *
801 * default index value is the one of currently selected cell
783 * default index value is the one of currently selected cell
802 *
784 *
803 * @method insert_cell_below
785 * @method insert_cell_below
804 * @param type {string} cell type
786 * @param type {string} cell type
805 * @param [index] {integer}
787 * @param [index] {integer}
806 *
788 *
807 * @return handle to created cell or null
789 * @return handle to created cell or null
808 *
790 *
809 **/
791 **/
810 Notebook.prototype.insert_cell_below = function (type, index) {
792 Notebook.prototype.insert_cell_below = function (type, index) {
811 index = this.index_or_selected(index);
793 index = this.index_or_selected(index);
812 return this.insert_cell_at_index(type, index+1);
794 return this.insert_cell_at_index(type, index+1);
813 };
795 };
814
796
815
797
816 /**
798 /**
817 * Insert cell at end of notebook
799 * Insert cell at end of notebook
818 *
800 *
819 * @method insert_cell_at_bottom
801 * @method insert_cell_at_bottom
820 * @param {String} type cell type
802 * @param {String} type cell type
821 *
803 *
822 * @return the added cell; or null
804 * @return the added cell; or null
823 **/
805 **/
824 Notebook.prototype.insert_cell_at_bottom = function (type){
806 Notebook.prototype.insert_cell_at_bottom = function (type){
825 var len = this.ncells();
807 var len = this.ncells();
826 return this.insert_cell_below(type,len-1);
808 return this.insert_cell_below(type,len-1);
827 };
809 };
828
810
829 /**
811 /**
830 * Turn a cell into a code cell.
812 * Turn a cell into a code cell.
831 *
813 *
832 * @method to_code
814 * @method to_code
833 * @param {Number} [index] A cell's index
815 * @param {Number} [index] A cell's index
834 */
816 */
835 Notebook.prototype.to_code = function (index) {
817 Notebook.prototype.to_code = function (index) {
836 var i = this.index_or_selected(index);
818 var i = this.index_or_selected(index);
837 if (this.is_valid_cell_index(i)) {
819 if (this.is_valid_cell_index(i)) {
838 var source_element = this.get_cell_element(i);
820 var source_element = this.get_cell_element(i);
839 var source_cell = source_element.data("cell");
821 var source_cell = source_element.data("cell");
840 if (!(source_cell instanceof IPython.CodeCell)) {
822 if (!(source_cell instanceof IPython.CodeCell)) {
841 var target_cell = this.insert_cell_below('code',i);
823 var target_cell = this.insert_cell_below('code',i);
842 var text = source_cell.get_text();
824 var text = source_cell.get_text();
843 if (text === source_cell.placeholder) {
825 if (text === source_cell.placeholder) {
844 text = '';
826 text = '';
845 }
827 }
846 target_cell.set_text(text);
828 target_cell.set_text(text);
847 // make this value the starting point, so that we can only undo
829 // make this value the starting point, so that we can only undo
848 // to this state, instead of a blank cell
830 // to this state, instead of a blank cell
849 target_cell.code_mirror.clearHistory();
831 target_cell.code_mirror.clearHistory();
850 source_element.remove();
832 source_element.remove();
851 this.select(i);
833 this.select(i);
852 this.set_dirty(true);
834 this.set_dirty(true);
853 };
835 }
854 };
836 }
855 };
837 };
856
838
857 /**
839 /**
858 * Turn a cell into a Markdown cell.
840 * Turn a cell into a Markdown cell.
859 *
841 *
860 * @method to_markdown
842 * @method to_markdown
861 * @param {Number} [index] A cell's index
843 * @param {Number} [index] A cell's index
862 */
844 */
863 Notebook.prototype.to_markdown = function (index) {
845 Notebook.prototype.to_markdown = function (index) {
864 var i = this.index_or_selected(index);
846 var i = this.index_or_selected(index);
865 if (this.is_valid_cell_index(i)) {
847 if (this.is_valid_cell_index(i)) {
866 var source_element = this.get_cell_element(i);
848 var source_element = this.get_cell_element(i);
867 var source_cell = source_element.data("cell");
849 var source_cell = source_element.data("cell");
868 if (!(source_cell instanceof IPython.MarkdownCell)) {
850 if (!(source_cell instanceof IPython.MarkdownCell)) {
869 var target_cell = this.insert_cell_below('markdown',i);
851 var target_cell = this.insert_cell_below('markdown',i);
870 var text = source_cell.get_text();
852 var text = source_cell.get_text();
871 if (text === source_cell.placeholder) {
853 if (text === source_cell.placeholder) {
872 text = '';
854 text = '';
873 };
855 }
874 // We must show the editor before setting its contents
856 // We must show the editor before setting its contents
875 target_cell.unrender();
857 target_cell.unrender();
876 target_cell.set_text(text);
858 target_cell.set_text(text);
877 // make this value the starting point, so that we can only undo
859 // make this value the starting point, so that we can only undo
878 // to this state, instead of a blank cell
860 // to this state, instead of a blank cell
879 target_cell.code_mirror.clearHistory();
861 target_cell.code_mirror.clearHistory();
880 source_element.remove();
862 source_element.remove();
881 this.select(i);
863 this.select(i);
882 if ((source_cell instanceof IPython.TextCell) && source_cell.rendered) {
864 if ((source_cell instanceof IPython.TextCell) && source_cell.rendered) {
883 target_cell.render();
865 target_cell.render();
884 }
866 }
885 this.set_dirty(true);
867 this.set_dirty(true);
886 };
868 }
887 };
869 }
888 };
870 };
889
871
890 /**
872 /**
891 * Turn a cell into a raw text cell.
873 * Turn a cell into a raw text cell.
892 *
874 *
893 * @method to_raw
875 * @method to_raw
894 * @param {Number} [index] A cell's index
876 * @param {Number} [index] A cell's index
895 */
877 */
896 Notebook.prototype.to_raw = function (index) {
878 Notebook.prototype.to_raw = function (index) {
897 var i = this.index_or_selected(index);
879 var i = this.index_or_selected(index);
898 if (this.is_valid_cell_index(i)) {
880 if (this.is_valid_cell_index(i)) {
899 var source_element = this.get_cell_element(i);
881 var source_element = this.get_cell_element(i);
900 var source_cell = source_element.data("cell");
882 var source_cell = source_element.data("cell");
901 var target_cell = null;
883 var target_cell = null;
902 if (!(source_cell instanceof IPython.RawCell)) {
884 if (!(source_cell instanceof IPython.RawCell)) {
903 target_cell = this.insert_cell_below('raw',i);
885 target_cell = this.insert_cell_below('raw',i);
904 var text = source_cell.get_text();
886 var text = source_cell.get_text();
905 if (text === source_cell.placeholder) {
887 if (text === source_cell.placeholder) {
906 text = '';
888 text = '';
907 };
889 }
908 // We must show the editor before setting its contents
890 // We must show the editor before setting its contents
909 target_cell.unrender();
891 target_cell.unrender();
910 target_cell.set_text(text);
892 target_cell.set_text(text);
911 // make this value the starting point, so that we can only undo
893 // make this value the starting point, so that we can only undo
912 // to this state, instead of a blank cell
894 // to this state, instead of a blank cell
913 target_cell.code_mirror.clearHistory();
895 target_cell.code_mirror.clearHistory();
914 source_element.remove();
896 source_element.remove();
915 this.select(i);
897 this.select(i);
916 this.set_dirty(true);
898 this.set_dirty(true);
917 };
899 }
918 };
900 }
919 };
901 };
920
902
921 /**
903 /**
922 * Turn a cell into a heading cell.
904 * Turn a cell into a heading cell.
923 *
905 *
924 * @method to_heading
906 * @method to_heading
925 * @param {Number} [index] A cell's index
907 * @param {Number} [index] A cell's index
926 * @param {Number} [level] A heading level (e.g., 1 becomes &lt;h1&gt;)
908 * @param {Number} [level] A heading level (e.g., 1 becomes &lt;h1&gt;)
927 */
909 */
928 Notebook.prototype.to_heading = function (index, level) {
910 Notebook.prototype.to_heading = function (index, level) {
929 level = level || 1;
911 level = level || 1;
930 var i = this.index_or_selected(index);
912 var i = this.index_or_selected(index);
931 if (this.is_valid_cell_index(i)) {
913 if (this.is_valid_cell_index(i)) {
932 var source_element = this.get_cell_element(i);
914 var source_element = this.get_cell_element(i);
933 var source_cell = source_element.data("cell");
915 var source_cell = source_element.data("cell");
934 var target_cell = null;
916 var target_cell = null;
935 if (source_cell instanceof IPython.HeadingCell) {
917 if (source_cell instanceof IPython.HeadingCell) {
936 source_cell.set_level(level);
918 source_cell.set_level(level);
937 } else {
919 } else {
938 target_cell = this.insert_cell_below('heading',i);
920 target_cell = this.insert_cell_below('heading',i);
939 var text = source_cell.get_text();
921 var text = source_cell.get_text();
940 if (text === source_cell.placeholder) {
922 if (text === source_cell.placeholder) {
941 text = '';
923 text = '';
942 };
924 }
943 // We must show the editor before setting its contents
925 // We must show the editor before setting its contents
944 target_cell.set_level(level);
926 target_cell.set_level(level);
945 target_cell.unrender();
927 target_cell.unrender();
946 target_cell.set_text(text);
928 target_cell.set_text(text);
947 // make this value the starting point, so that we can only undo
929 // make this value the starting point, so that we can only undo
948 // to this state, instead of a blank cell
930 // to this state, instead of a blank cell
949 target_cell.code_mirror.clearHistory();
931 target_cell.code_mirror.clearHistory();
950 source_element.remove();
932 source_element.remove();
951 this.select(i);
933 this.select(i);
952 if ((source_cell instanceof IPython.TextCell) && source_cell.rendered) {
934 if ((source_cell instanceof IPython.TextCell) && source_cell.rendered) {
953 target_cell.render();
935 target_cell.render();
954 }
936 }
955 };
937 }
956 this.set_dirty(true);
938 this.set_dirty(true);
957 $([IPython.events]).trigger('selected_cell_type_changed.Notebook',
939 $([IPython.events]).trigger('selected_cell_type_changed.Notebook',
958 {'cell_type':'heading',level:level}
940 {'cell_type':'heading',level:level}
959 );
941 );
960 };
942 }
961 };
943 };
962
944
963
945
964 // Cut/Copy/Paste
946 // Cut/Copy/Paste
965
947
966 /**
948 /**
967 * Enable UI elements for pasting cells.
949 * Enable UI elements for pasting cells.
968 *
950 *
969 * @method enable_paste
951 * @method enable_paste
970 */
952 */
971 Notebook.prototype.enable_paste = function () {
953 Notebook.prototype.enable_paste = function () {
972 var that = this;
954 var that = this;
973 if (!this.paste_enabled) {
955 if (!this.paste_enabled) {
974 $('#paste_cell_replace').removeClass('disabled')
956 $('#paste_cell_replace').removeClass('disabled')
975 .on('click', function () {that.paste_cell_replace();});
957 .on('click', function () {that.paste_cell_replace();});
976 $('#paste_cell_above').removeClass('disabled')
958 $('#paste_cell_above').removeClass('disabled')
977 .on('click', function () {that.paste_cell_above();});
959 .on('click', function () {that.paste_cell_above();});
978 $('#paste_cell_below').removeClass('disabled')
960 $('#paste_cell_below').removeClass('disabled')
979 .on('click', function () {that.paste_cell_below();});
961 .on('click', function () {that.paste_cell_below();});
980 this.paste_enabled = true;
962 this.paste_enabled = true;
981 };
963 }
982 };
964 };
983
965
984 /**
966 /**
985 * Disable UI elements for pasting cells.
967 * Disable UI elements for pasting cells.
986 *
968 *
987 * @method disable_paste
969 * @method disable_paste
988 */
970 */
989 Notebook.prototype.disable_paste = function () {
971 Notebook.prototype.disable_paste = function () {
990 if (this.paste_enabled) {
972 if (this.paste_enabled) {
991 $('#paste_cell_replace').addClass('disabled').off('click');
973 $('#paste_cell_replace').addClass('disabled').off('click');
992 $('#paste_cell_above').addClass('disabled').off('click');
974 $('#paste_cell_above').addClass('disabled').off('click');
993 $('#paste_cell_below').addClass('disabled').off('click');
975 $('#paste_cell_below').addClass('disabled').off('click');
994 this.paste_enabled = false;
976 this.paste_enabled = false;
995 };
977 }
996 };
978 };
997
979
998 /**
980 /**
999 * Cut a cell.
981 * Cut a cell.
1000 *
982 *
1001 * @method cut_cell
983 * @method cut_cell
1002 */
984 */
1003 Notebook.prototype.cut_cell = function () {
985 Notebook.prototype.cut_cell = function () {
1004 this.copy_cell();
986 this.copy_cell();
1005 this.delete_cell();
987 this.delete_cell();
1006 }
988 };
1007
989
1008 /**
990 /**
1009 * Copy a cell.
991 * Copy a cell.
1010 *
992 *
1011 * @method copy_cell
993 * @method copy_cell
1012 */
994 */
1013 Notebook.prototype.copy_cell = function () {
995 Notebook.prototype.copy_cell = function () {
1014 var cell = this.get_selected_cell();
996 var cell = this.get_selected_cell();
1015 this.clipboard = cell.toJSON();
997 this.clipboard = cell.toJSON();
1016 this.enable_paste();
998 this.enable_paste();
1017 };
999 };
1018
1000
1019 /**
1001 /**
1020 * Replace the selected cell with a cell in the clipboard.
1002 * Replace the selected cell with a cell in the clipboard.
1021 *
1003 *
1022 * @method paste_cell_replace
1004 * @method paste_cell_replace
1023 */
1005 */
1024 Notebook.prototype.paste_cell_replace = function () {
1006 Notebook.prototype.paste_cell_replace = function () {
1025 if (this.clipboard !== null && this.paste_enabled) {
1007 if (this.clipboard !== null && this.paste_enabled) {
1026 var cell_data = this.clipboard;
1008 var cell_data = this.clipboard;
1027 var new_cell = this.insert_cell_above(cell_data.cell_type);
1009 var new_cell = this.insert_cell_above(cell_data.cell_type);
1028 new_cell.fromJSON(cell_data);
1010 new_cell.fromJSON(cell_data);
1029 var old_cell = this.get_next_cell(new_cell);
1011 var old_cell = this.get_next_cell(new_cell);
1030 this.delete_cell(this.find_cell_index(old_cell));
1012 this.delete_cell(this.find_cell_index(old_cell));
1031 this.select(this.find_cell_index(new_cell));
1013 this.select(this.find_cell_index(new_cell));
1032 };
1014 }
1033 };
1015 };
1034
1016
1035 /**
1017 /**
1036 * Paste a cell from the clipboard above the selected cell.
1018 * Paste a cell from the clipboard above the selected cell.
1037 *
1019 *
1038 * @method paste_cell_above
1020 * @method paste_cell_above
1039 */
1021 */
1040 Notebook.prototype.paste_cell_above = function () {
1022 Notebook.prototype.paste_cell_above = function () {
1041 if (this.clipboard !== null && this.paste_enabled) {
1023 if (this.clipboard !== null && this.paste_enabled) {
1042 var cell_data = this.clipboard;
1024 var cell_data = this.clipboard;
1043 var new_cell = this.insert_cell_above(cell_data.cell_type);
1025 var new_cell = this.insert_cell_above(cell_data.cell_type);
1044 new_cell.fromJSON(cell_data);
1026 new_cell.fromJSON(cell_data);
1045 new_cell.focus_cell();
1027 new_cell.focus_cell();
1046 };
1028 }
1047 };
1029 };
1048
1030
1049 /**
1031 /**
1050 * Paste a cell from the clipboard below the selected cell.
1032 * Paste a cell from the clipboard below the selected cell.
1051 *
1033 *
1052 * @method paste_cell_below
1034 * @method paste_cell_below
1053 */
1035 */
1054 Notebook.prototype.paste_cell_below = function () {
1036 Notebook.prototype.paste_cell_below = function () {
1055 if (this.clipboard !== null && this.paste_enabled) {
1037 if (this.clipboard !== null && this.paste_enabled) {
1056 var cell_data = this.clipboard;
1038 var cell_data = this.clipboard;
1057 var new_cell = this.insert_cell_below(cell_data.cell_type);
1039 var new_cell = this.insert_cell_below(cell_data.cell_type);
1058 new_cell.fromJSON(cell_data);
1040 new_cell.fromJSON(cell_data);
1059 new_cell.focus_cell();
1041 new_cell.focus_cell();
1060 };
1042 }
1061 };
1043 };
1062
1044
1063 // Split/merge
1045 // Split/merge
1064
1046
1065 /**
1047 /**
1066 * Split the selected cell into two, at the cursor.
1048 * Split the selected cell into two, at the cursor.
1067 *
1049 *
1068 * @method split_cell
1050 * @method split_cell
1069 */
1051 */
1070 Notebook.prototype.split_cell = function () {
1052 Notebook.prototype.split_cell = function () {
1071 var mdc = IPython.MarkdownCell;
1053 var mdc = IPython.MarkdownCell;
1072 var rc = IPython.RawCell;
1054 var rc = IPython.RawCell;
1073 var cell = this.get_selected_cell();
1055 var cell = this.get_selected_cell();
1074 if (cell.is_splittable()) {
1056 if (cell.is_splittable()) {
1075 var texta = cell.get_pre_cursor();
1057 var texta = cell.get_pre_cursor();
1076 var textb = cell.get_post_cursor();
1058 var textb = cell.get_post_cursor();
1077 if (cell instanceof IPython.CodeCell) {
1059 if (cell instanceof IPython.CodeCell) {
1078 // In this case the operations keep the notebook in its existing mode
1060 // In this case the operations keep the notebook in its existing mode
1079 // so we don't need to do any post-op mode changes.
1061 // so we don't need to do any post-op mode changes.
1080 cell.set_text(textb);
1062 cell.set_text(textb);
1081 var new_cell = this.insert_cell_above('code');
1063 var new_cell = this.insert_cell_above('code');
1082 new_cell.set_text(texta);
1064 new_cell.set_text(texta);
1083 } else if ((cell instanceof mdc && !cell.rendered) || (cell instanceof rc)) {
1065 } else if ((cell instanceof mdc && !cell.rendered) || (cell instanceof rc)) {
1084 // We know cell is !rendered so we can use set_text.
1066 // We know cell is !rendered so we can use set_text.
1085 cell.set_text(textb);
1067 cell.set_text(textb);
1086 var new_cell = this.insert_cell_above(cell.cell_type);
1068 var new_cell = this.insert_cell_above(cell.cell_type);
1087 // Unrender the new cell so we can call set_text.
1069 // Unrender the new cell so we can call set_text.
1088 new_cell.unrender();
1070 new_cell.unrender();
1089 new_cell.set_text(texta);
1071 new_cell.set_text(texta);
1090 }
1072 }
1091 };
1073 }
1092 };
1074 };
1093
1075
1094 /**
1076 /**
1095 * Combine the selected cell into the cell above it.
1077 * Combine the selected cell into the cell above it.
1096 *
1078 *
1097 * @method merge_cell_above
1079 * @method merge_cell_above
1098 */
1080 */
1099 Notebook.prototype.merge_cell_above = function () {
1081 Notebook.prototype.merge_cell_above = function () {
1100 var mdc = IPython.MarkdownCell;
1082 var mdc = IPython.MarkdownCell;
1101 var rc = IPython.RawCell;
1083 var rc = IPython.RawCell;
1102 var index = this.get_selected_index();
1084 var index = this.get_selected_index();
1103 var cell = this.get_cell(index);
1085 var cell = this.get_cell(index);
1104 var render = cell.rendered;
1086 var render = cell.rendered;
1105 if (!cell.is_mergeable()) {
1087 if (!cell.is_mergeable()) {
1106 return;
1088 return;
1107 }
1089 }
1108 if (index > 0) {
1090 if (index > 0) {
1109 var upper_cell = this.get_cell(index-1);
1091 var upper_cell = this.get_cell(index-1);
1110 if (!upper_cell.is_mergeable()) {
1092 if (!upper_cell.is_mergeable()) {
1111 return;
1093 return;
1112 }
1094 }
1113 var upper_text = upper_cell.get_text();
1095 var upper_text = upper_cell.get_text();
1114 var text = cell.get_text();
1096 var text = cell.get_text();
1115 if (cell instanceof IPython.CodeCell) {
1097 if (cell instanceof IPython.CodeCell) {
1116 cell.set_text(upper_text+'\n'+text);
1098 cell.set_text(upper_text+'\n'+text);
1117 } else if ((cell instanceof mdc) || (cell instanceof rc)) {
1099 } else if ((cell instanceof mdc) || (cell instanceof rc)) {
1118 cell.unrender(); // Must unrender before we set_text.
1100 cell.unrender(); // Must unrender before we set_text.
1119 cell.set_text(upper_text+'\n\n'+text);
1101 cell.set_text(upper_text+'\n\n'+text);
1120 if (render) {
1102 if (render) {
1121 // The rendered state of the final cell should match
1103 // The rendered state of the final cell should match
1122 // that of the original selected cell;
1104 // that of the original selected cell;
1123 cell.render();
1105 cell.render();
1124 }
1106 }
1125 };
1107 }
1126 this.delete_cell(index-1);
1108 this.delete_cell(index-1);
1127 this.select(this.find_cell_index(cell));
1109 this.select(this.find_cell_index(cell));
1128 };
1110 }
1129 };
1111 };
1130
1112
1131 /**
1113 /**
1132 * Combine the selected cell into the cell below it.
1114 * Combine the selected cell into the cell below it.
1133 *
1115 *
1134 * @method merge_cell_below
1116 * @method merge_cell_below
1135 */
1117 */
1136 Notebook.prototype.merge_cell_below = function () {
1118 Notebook.prototype.merge_cell_below = function () {
1137 var mdc = IPython.MarkdownCell;
1119 var mdc = IPython.MarkdownCell;
1138 var rc = IPython.RawCell;
1120 var rc = IPython.RawCell;
1139 var index = this.get_selected_index();
1121 var index = this.get_selected_index();
1140 var cell = this.get_cell(index);
1122 var cell = this.get_cell(index);
1141 var render = cell.rendered;
1123 var render = cell.rendered;
1142 if (!cell.is_mergeable()) {
1124 if (!cell.is_mergeable()) {
1143 return;
1125 return;
1144 }
1126 }
1145 if (index < this.ncells()-1) {
1127 if (index < this.ncells()-1) {
1146 var lower_cell = this.get_cell(index+1);
1128 var lower_cell = this.get_cell(index+1);
1147 if (!lower_cell.is_mergeable()) {
1129 if (!lower_cell.is_mergeable()) {
1148 return;
1130 return;
1149 }
1131 }
1150 var lower_text = lower_cell.get_text();
1132 var lower_text = lower_cell.get_text();
1151 var text = cell.get_text();
1133 var text = cell.get_text();
1152 if (cell instanceof IPython.CodeCell) {
1134 if (cell instanceof IPython.CodeCell) {
1153 cell.set_text(text+'\n'+lower_text);
1135 cell.set_text(text+'\n'+lower_text);
1154 } else if ((cell instanceof mdc) || (cell instanceof rc)) {
1136 } else if ((cell instanceof mdc) || (cell instanceof rc)) {
1155 cell.unrender(); // Must unrender before we set_text.
1137 cell.unrender(); // Must unrender before we set_text.
1156 cell.set_text(text+'\n\n'+lower_text);
1138 cell.set_text(text+'\n\n'+lower_text);
1157 if (render) {
1139 if (render) {
1158 // The rendered state of the final cell should match
1140 // The rendered state of the final cell should match
1159 // that of the original selected cell;
1141 // that of the original selected cell;
1160 cell.render();
1142 cell.render();
1161 }
1143 }
1162 };
1144 }
1163 this.delete_cell(index+1);
1145 this.delete_cell(index+1);
1164 this.select(this.find_cell_index(cell));
1146 this.select(this.find_cell_index(cell));
1165 };
1147 }
1166 };
1148 };
1167
1149
1168
1150
1169 // Cell collapsing and output clearing
1151 // Cell collapsing and output clearing
1170
1152
1171 /**
1153 /**
1172 * Hide a cell's output.
1154 * Hide a cell's output.
1173 *
1155 *
1174 * @method collapse_output
1156 * @method collapse_output
1175 * @param {Number} index A cell's numeric index
1157 * @param {Number} index A cell's numeric index
1176 */
1158 */
1177 Notebook.prototype.collapse_output = function (index) {
1159 Notebook.prototype.collapse_output = function (index) {
1178 var i = this.index_or_selected(index);
1160 var i = this.index_or_selected(index);
1179 var cell = this.get_cell(i);
1161 var cell = this.get_cell(i);
1180 if (cell !== null && (cell instanceof IPython.CodeCell)) {
1162 if (cell !== null && (cell instanceof IPython.CodeCell)) {
1181 cell.collapse_output();
1163 cell.collapse_output();
1182 this.set_dirty(true);
1164 this.set_dirty(true);
1183 }
1165 }
1184 };
1166 };
1185
1167
1186 /**
1168 /**
1187 * Hide each code cell's output area.
1169 * Hide each code cell's output area.
1188 *
1170 *
1189 * @method collapse_all_output
1171 * @method collapse_all_output
1190 */
1172 */
1191 Notebook.prototype.collapse_all_output = function () {
1173 Notebook.prototype.collapse_all_output = function () {
1192 $.map(this.get_cells(), function (cell, i) {
1174 $.map(this.get_cells(), function (cell, i) {
1193 if (cell instanceof IPython.CodeCell) {
1175 if (cell instanceof IPython.CodeCell) {
1194 cell.collapse_output();
1176 cell.collapse_output();
1195 }
1177 }
1196 });
1178 });
1197 // this should not be set if the `collapse` key is removed from nbformat
1179 // this should not be set if the `collapse` key is removed from nbformat
1198 this.set_dirty(true);
1180 this.set_dirty(true);
1199 };
1181 };
1200
1182
1201 /**
1183 /**
1202 * Show a cell's output.
1184 * Show a cell's output.
1203 *
1185 *
1204 * @method expand_output
1186 * @method expand_output
1205 * @param {Number} index A cell's numeric index
1187 * @param {Number} index A cell's numeric index
1206 */
1188 */
1207 Notebook.prototype.expand_output = function (index) {
1189 Notebook.prototype.expand_output = function (index) {
1208 var i = this.index_or_selected(index);
1190 var i = this.index_or_selected(index);
1209 var cell = this.get_cell(i);
1191 var cell = this.get_cell(i);
1210 if (cell !== null && (cell instanceof IPython.CodeCell)) {
1192 if (cell !== null && (cell instanceof IPython.CodeCell)) {
1211 cell.expand_output();
1193 cell.expand_output();
1212 this.set_dirty(true);
1194 this.set_dirty(true);
1213 }
1195 }
1214 };
1196 };
1215
1197
1216 /**
1198 /**
1217 * Expand each code cell's output area, and remove scrollbars.
1199 * Expand each code cell's output area, and remove scrollbars.
1218 *
1200 *
1219 * @method expand_all_output
1201 * @method expand_all_output
1220 */
1202 */
1221 Notebook.prototype.expand_all_output = function () {
1203 Notebook.prototype.expand_all_output = function () {
1222 $.map(this.get_cells(), function (cell, i) {
1204 $.map(this.get_cells(), function (cell, i) {
1223 if (cell instanceof IPython.CodeCell) {
1205 if (cell instanceof IPython.CodeCell) {
1224 cell.expand_output();
1206 cell.expand_output();
1225 }
1207 }
1226 });
1208 });
1227 // this should not be set if the `collapse` key is removed from nbformat
1209 // this should not be set if the `collapse` key is removed from nbformat
1228 this.set_dirty(true);
1210 this.set_dirty(true);
1229 };
1211 };
1230
1212
1231 /**
1213 /**
1232 * Clear the selected CodeCell's output area.
1214 * Clear the selected CodeCell's output area.
1233 *
1215 *
1234 * @method clear_output
1216 * @method clear_output
1235 * @param {Number} index A cell's numeric index
1217 * @param {Number} index A cell's numeric index
1236 */
1218 */
1237 Notebook.prototype.clear_output = function (index) {
1219 Notebook.prototype.clear_output = function (index) {
1238 var i = this.index_or_selected(index);
1220 var i = this.index_or_selected(index);
1239 var cell = this.get_cell(i);
1221 var cell = this.get_cell(i);
1240 if (cell !== null && (cell instanceof IPython.CodeCell)) {
1222 if (cell !== null && (cell instanceof IPython.CodeCell)) {
1241 cell.clear_output();
1223 cell.clear_output();
1242 this.set_dirty(true);
1224 this.set_dirty(true);
1243 }
1225 }
1244 };
1226 };
1245
1227
1246 /**
1228 /**
1247 * Clear each code cell's output area.
1229 * Clear each code cell's output area.
1248 *
1230 *
1249 * @method clear_all_output
1231 * @method clear_all_output
1250 */
1232 */
1251 Notebook.prototype.clear_all_output = function () {
1233 Notebook.prototype.clear_all_output = function () {
1252 $.map(this.get_cells(), function (cell, i) {
1234 $.map(this.get_cells(), function (cell, i) {
1253 if (cell instanceof IPython.CodeCell) {
1235 if (cell instanceof IPython.CodeCell) {
1254 cell.clear_output();
1236 cell.clear_output();
1255 }
1237 }
1256 });
1238 });
1257 this.set_dirty(true);
1239 this.set_dirty(true);
1258 };
1240 };
1259
1241
1260 /**
1242 /**
1261 * Scroll the selected CodeCell's output area.
1243 * Scroll the selected CodeCell's output area.
1262 *
1244 *
1263 * @method scroll_output
1245 * @method scroll_output
1264 * @param {Number} index A cell's numeric index
1246 * @param {Number} index A cell's numeric index
1265 */
1247 */
1266 Notebook.prototype.scroll_output = function (index) {
1248 Notebook.prototype.scroll_output = function (index) {
1267 var i = this.index_or_selected(index);
1249 var i = this.index_or_selected(index);
1268 var cell = this.get_cell(i);
1250 var cell = this.get_cell(i);
1269 if (cell !== null && (cell instanceof IPython.CodeCell)) {
1251 if (cell !== null && (cell instanceof IPython.CodeCell)) {
1270 cell.scroll_output();
1252 cell.scroll_output();
1271 this.set_dirty(true);
1253 this.set_dirty(true);
1272 }
1254 }
1273 };
1255 };
1274
1256
1275 /**
1257 /**
1276 * Expand each code cell's output area, and add a scrollbar for long output.
1258 * Expand each code cell's output area, and add a scrollbar for long output.
1277 *
1259 *
1278 * @method scroll_all_output
1260 * @method scroll_all_output
1279 */
1261 */
1280 Notebook.prototype.scroll_all_output = function () {
1262 Notebook.prototype.scroll_all_output = function () {
1281 $.map(this.get_cells(), function (cell, i) {
1263 $.map(this.get_cells(), function (cell, i) {
1282 if (cell instanceof IPython.CodeCell) {
1264 if (cell instanceof IPython.CodeCell) {
1283 cell.scroll_output();
1265 cell.scroll_output();
1284 }
1266 }
1285 });
1267 });
1286 // this should not be set if the `collapse` key is removed from nbformat
1268 // this should not be set if the `collapse` key is removed from nbformat
1287 this.set_dirty(true);
1269 this.set_dirty(true);
1288 };
1270 };
1289
1271
1290 /** Toggle whether a cell's output is collapsed or expanded.
1272 /** Toggle whether a cell's output is collapsed or expanded.
1291 *
1273 *
1292 * @method toggle_output
1274 * @method toggle_output
1293 * @param {Number} index A cell's numeric index
1275 * @param {Number} index A cell's numeric index
1294 */
1276 */
1295 Notebook.prototype.toggle_output = function (index) {
1277 Notebook.prototype.toggle_output = function (index) {
1296 var i = this.index_or_selected(index);
1278 var i = this.index_or_selected(index);
1297 var cell = this.get_cell(i);
1279 var cell = this.get_cell(i);
1298 if (cell !== null && (cell instanceof IPython.CodeCell)) {
1280 if (cell !== null && (cell instanceof IPython.CodeCell)) {
1299 cell.toggle_output();
1281 cell.toggle_output();
1300 this.set_dirty(true);
1282 this.set_dirty(true);
1301 }
1283 }
1302 };
1284 };
1303
1285
1304 /**
1286 /**
1305 * Hide/show the output of all cells.
1287 * Hide/show the output of all cells.
1306 *
1288 *
1307 * @method toggle_all_output
1289 * @method toggle_all_output
1308 */
1290 */
1309 Notebook.prototype.toggle_all_output = function () {
1291 Notebook.prototype.toggle_all_output = function () {
1310 $.map(this.get_cells(), function (cell, i) {
1292 $.map(this.get_cells(), function (cell, i) {
1311 if (cell instanceof IPython.CodeCell) {
1293 if (cell instanceof IPython.CodeCell) {
1312 cell.toggle_output();
1294 cell.toggle_output();
1313 }
1295 }
1314 });
1296 });
1315 // this should not be set if the `collapse` key is removed from nbformat
1297 // this should not be set if the `collapse` key is removed from nbformat
1316 this.set_dirty(true);
1298 this.set_dirty(true);
1317 };
1299 };
1318
1300
1319 /**
1301 /**
1320 * Toggle a scrollbar for long cell outputs.
1302 * Toggle a scrollbar for long cell outputs.
1321 *
1303 *
1322 * @method toggle_output_scroll
1304 * @method toggle_output_scroll
1323 * @param {Number} index A cell's numeric index
1305 * @param {Number} index A cell's numeric index
1324 */
1306 */
1325 Notebook.prototype.toggle_output_scroll = function (index) {
1307 Notebook.prototype.toggle_output_scroll = function (index) {
1326 var i = this.index_or_selected(index);
1308 var i = this.index_or_selected(index);
1327 var cell = this.get_cell(i);
1309 var cell = this.get_cell(i);
1328 if (cell !== null && (cell instanceof IPython.CodeCell)) {
1310 if (cell !== null && (cell instanceof IPython.CodeCell)) {
1329 cell.toggle_output_scroll();
1311 cell.toggle_output_scroll();
1330 this.set_dirty(true);
1312 this.set_dirty(true);
1331 }
1313 }
1332 };
1314 };
1333
1315
1334 /**
1316 /**
1335 * Toggle the scrolling of long output on all cells.
1317 * Toggle the scrolling of long output on all cells.
1336 *
1318 *
1337 * @method toggle_all_output_scrolling
1319 * @method toggle_all_output_scrolling
1338 */
1320 */
1339 Notebook.prototype.toggle_all_output_scroll = function () {
1321 Notebook.prototype.toggle_all_output_scroll = function () {
1340 $.map(this.get_cells(), function (cell, i) {
1322 $.map(this.get_cells(), function (cell, i) {
1341 if (cell instanceof IPython.CodeCell) {
1323 if (cell instanceof IPython.CodeCell) {
1342 cell.toggle_output_scroll();
1324 cell.toggle_output_scroll();
1343 }
1325 }
1344 });
1326 });
1345 // this should not be set if the `collapse` key is removed from nbformat
1327 // this should not be set if the `collapse` key is removed from nbformat
1346 this.set_dirty(true);
1328 this.set_dirty(true);
1347 };
1329 };
1348
1330
1349 // Other cell functions: line numbers, ...
1331 // Other cell functions: line numbers, ...
1350
1332
1351 /**
1333 /**
1352 * Toggle line numbers in the selected cell's input area.
1334 * Toggle line numbers in the selected cell's input area.
1353 *
1335 *
1354 * @method cell_toggle_line_numbers
1336 * @method cell_toggle_line_numbers
1355 */
1337 */
1356 Notebook.prototype.cell_toggle_line_numbers = function() {
1338 Notebook.prototype.cell_toggle_line_numbers = function() {
1357 this.get_selected_cell().toggle_line_numbers();
1339 this.get_selected_cell().toggle_line_numbers();
1358 };
1340 };
1359
1341
1360 // Session related things
1342 // Session related things
1361
1343
1362 /**
1344 /**
1363 * Start a new session and set it on each code cell.
1345 * Start a new session and set it on each code cell.
1364 *
1346 *
1365 * @method start_session
1347 * @method start_session
1366 */
1348 */
1367 Notebook.prototype.start_session = function () {
1349 Notebook.prototype.start_session = function () {
1368 this.session = new IPython.Session(this.notebook_name, this.notebook_path, this);
1350 this.session = new IPython.Session(this, this.options);
1369 this.session.start($.proxy(this._session_started, this));
1351 this.session.start($.proxy(this._session_started, this));
1370 };
1352 };
1371
1353
1372
1354
1373 /**
1355 /**
1374 * Once a session is started, link the code cells to the kernel and pass the
1356 * Once a session is started, link the code cells to the kernel and pass the
1375 * comm manager to the widget manager
1357 * comm manager to the widget manager
1376 *
1358 *
1377 */
1359 */
1378 Notebook.prototype._session_started = function(){
1360 Notebook.prototype._session_started = function(){
1379 this.kernel = this.session.kernel;
1361 this.kernel = this.session.kernel;
1380 var ncells = this.ncells();
1362 var ncells = this.ncells();
1381 for (var i=0; i<ncells; i++) {
1363 for (var i=0; i<ncells; i++) {
1382 var cell = this.get_cell(i);
1364 var cell = this.get_cell(i);
1383 if (cell instanceof IPython.CodeCell) {
1365 if (cell instanceof IPython.CodeCell) {
1384 cell.set_kernel(this.session.kernel);
1366 cell.set_kernel(this.session.kernel);
1385 };
1367 }
1386 };
1368 }
1387 };
1369 };
1388
1370
1389 /**
1371 /**
1390 * Prompt the user to restart the IPython kernel.
1372 * Prompt the user to restart the IPython kernel.
1391 *
1373 *
1392 * @method restart_kernel
1374 * @method restart_kernel
1393 */
1375 */
1394 Notebook.prototype.restart_kernel = function () {
1376 Notebook.prototype.restart_kernel = function () {
1395 var that = this;
1377 var that = this;
1396 IPython.dialog.modal({
1378 IPython.dialog.modal({
1397 title : "Restart kernel or continue running?",
1379 title : "Restart kernel or continue running?",
1398 body : $("<p/>").text(
1380 body : $("<p/>").text(
1399 'Do you want to restart the current kernel? You will lose all variables defined in it.'
1381 'Do you want to restart the current kernel? You will lose all variables defined in it.'
1400 ),
1382 ),
1401 buttons : {
1383 buttons : {
1402 "Continue running" : {},
1384 "Continue running" : {},
1403 "Restart" : {
1385 "Restart" : {
1404 "class" : "btn-danger",
1386 "class" : "btn-danger",
1405 "click" : function() {
1387 "click" : function() {
1406 that.session.restart_kernel();
1388 that.session.restart_kernel();
1407 }
1389 }
1408 }
1390 }
1409 }
1391 }
1410 });
1392 });
1411 };
1393 };
1412
1394
1413 /**
1395 /**
1414 * Execute or render cell outputs and go into command mode.
1396 * Execute or render cell outputs and go into command mode.
1415 *
1397 *
1416 * @method execute_cell
1398 * @method execute_cell
1417 */
1399 */
1418 Notebook.prototype.execute_cell = function () {
1400 Notebook.prototype.execute_cell = function () {
1419 // mode = shift, ctrl, alt
1401 // mode = shift, ctrl, alt
1420 var cell = this.get_selected_cell();
1402 var cell = this.get_selected_cell();
1421 var cell_index = this.find_cell_index(cell);
1403 var cell_index = this.find_cell_index(cell);
1422
1404
1423 cell.execute();
1405 cell.execute();
1424 this.command_mode();
1406 this.command_mode();
1425 cell.focus_cell();
1407 cell.focus_cell();
1426 this.set_dirty(true);
1408 this.set_dirty(true);
1427 }
1409 };
1428
1410
1429 /**
1411 /**
1430 * Execute or render cell outputs and insert a new cell below.
1412 * Execute or render cell outputs and insert a new cell below.
1431 *
1413 *
1432 * @method execute_cell_and_insert_below
1414 * @method execute_cell_and_insert_below
1433 */
1415 */
1434 Notebook.prototype.execute_cell_and_insert_below = function () {
1416 Notebook.prototype.execute_cell_and_insert_below = function () {
1435 var cell = this.get_selected_cell();
1417 var cell = this.get_selected_cell();
1436 var cell_index = this.find_cell_index(cell);
1418 var cell_index = this.find_cell_index(cell);
1437
1419
1438 cell.execute();
1420 cell.execute();
1439
1421
1440 // If we are at the end always insert a new cell and return
1422 // If we are at the end always insert a new cell and return
1441 if (cell_index === (this.ncells()-1)) {
1423 if (cell_index === (this.ncells()-1)) {
1442 this.insert_cell_below('code');
1424 this.insert_cell_below('code');
1443 this.select(cell_index+1);
1425 this.select(cell_index+1);
1444 this.edit_mode();
1426 this.edit_mode();
1445 this.scroll_to_bottom();
1427 this.scroll_to_bottom();
1446 this.set_dirty(true);
1428 this.set_dirty(true);
1447 return;
1429 return;
1448 }
1430 }
1449
1431
1450 this.insert_cell_below('code');
1432 this.insert_cell_below('code');
1451 this.select(cell_index+1);
1433 this.select(cell_index+1);
1452 this.edit_mode();
1434 this.edit_mode();
1453 this.set_dirty(true);
1435 this.set_dirty(true);
1454 };
1436 };
1455
1437
1456 /**
1438 /**
1457 * Execute or render cell outputs and select the next cell.
1439 * Execute or render cell outputs and select the next cell.
1458 *
1440 *
1459 * @method execute_cell_and_select_below
1441 * @method execute_cell_and_select_below
1460 */
1442 */
1461 Notebook.prototype.execute_cell_and_select_below = function () {
1443 Notebook.prototype.execute_cell_and_select_below = function () {
1462
1444
1463 var cell = this.get_selected_cell();
1445 var cell = this.get_selected_cell();
1464 var cell_index = this.find_cell_index(cell);
1446 var cell_index = this.find_cell_index(cell);
1465
1447
1466 cell.execute();
1448 cell.execute();
1467
1449
1468 // If we are at the end always insert a new cell and return
1450 // If we are at the end always insert a new cell and return
1469 if (cell_index === (this.ncells()-1)) {
1451 if (cell_index === (this.ncells()-1)) {
1470 this.insert_cell_below('code');
1452 this.insert_cell_below('code');
1471 this.select(cell_index+1);
1453 this.select(cell_index+1);
1472 this.edit_mode();
1454 this.edit_mode();
1473 this.scroll_to_bottom();
1455 this.scroll_to_bottom();
1474 this.set_dirty(true);
1456 this.set_dirty(true);
1475 return;
1457 return;
1476 }
1458 }
1477
1459
1478 this.select(cell_index+1);
1460 this.select(cell_index+1);
1479 this.get_cell(cell_index+1).focus_cell();
1461 this.get_cell(cell_index+1).focus_cell();
1480 this.set_dirty(true);
1462 this.set_dirty(true);
1481 };
1463 };
1482
1464
1483 /**
1465 /**
1484 * Execute all cells below the selected cell.
1466 * Execute all cells below the selected cell.
1485 *
1467 *
1486 * @method execute_cells_below
1468 * @method execute_cells_below
1487 */
1469 */
1488 Notebook.prototype.execute_cells_below = function () {
1470 Notebook.prototype.execute_cells_below = function () {
1489 this.execute_cell_range(this.get_selected_index(), this.ncells());
1471 this.execute_cell_range(this.get_selected_index(), this.ncells());
1490 this.scroll_to_bottom();
1472 this.scroll_to_bottom();
1491 };
1473 };
1492
1474
1493 /**
1475 /**
1494 * Execute all cells above the selected cell.
1476 * Execute all cells above the selected cell.
1495 *
1477 *
1496 * @method execute_cells_above
1478 * @method execute_cells_above
1497 */
1479 */
1498 Notebook.prototype.execute_cells_above = function () {
1480 Notebook.prototype.execute_cells_above = function () {
1499 this.execute_cell_range(0, this.get_selected_index());
1481 this.execute_cell_range(0, this.get_selected_index());
1500 };
1482 };
1501
1483
1502 /**
1484 /**
1503 * Execute all cells.
1485 * Execute all cells.
1504 *
1486 *
1505 * @method execute_all_cells
1487 * @method execute_all_cells
1506 */
1488 */
1507 Notebook.prototype.execute_all_cells = function () {
1489 Notebook.prototype.execute_all_cells = function () {
1508 this.execute_cell_range(0, this.ncells());
1490 this.execute_cell_range(0, this.ncells());
1509 this.scroll_to_bottom();
1491 this.scroll_to_bottom();
1510 };
1492 };
1511
1493
1512 /**
1494 /**
1513 * Execute a contiguous range of cells.
1495 * Execute a contiguous range of cells.
1514 *
1496 *
1515 * @method execute_cell_range
1497 * @method execute_cell_range
1516 * @param {Number} start Index of the first cell to execute (inclusive)
1498 * @param {Number} start Index of the first cell to execute (inclusive)
1517 * @param {Number} end Index of the last cell to execute (exclusive)
1499 * @param {Number} end Index of the last cell to execute (exclusive)
1518 */
1500 */
1519 Notebook.prototype.execute_cell_range = function (start, end) {
1501 Notebook.prototype.execute_cell_range = function (start, end) {
1520 for (var i=start; i<end; i++) {
1502 for (var i=start; i<end; i++) {
1521 this.select(i);
1503 this.select(i);
1522 this.execute_cell();
1504 this.execute_cell();
1523 };
1505 }
1524 };
1506 };
1525
1507
1526 // Persistance and loading
1508 // Persistance and loading
1527
1509
1528 /**
1510 /**
1529 * Getter method for this notebook's name.
1511 * Getter method for this notebook's name.
1530 *
1512 *
1531 * @method get_notebook_name
1513 * @method get_notebook_name
1532 * @return {String} This notebook's name
1514 * @return {String} This notebook's name (excluding file extension)
1533 */
1515 */
1534 Notebook.prototype.get_notebook_name = function () {
1516 Notebook.prototype.get_notebook_name = function () {
1535 var nbname = this.notebook_name.substring(0,this.notebook_name.length-6);
1517 var nbname = this.notebook_name.substring(0,this.notebook_name.length-6);
1536 return nbname;
1518 return nbname;
1537 };
1519 };
1538
1520
1539 /**
1521 /**
1540 * Setter method for this notebook's name.
1522 * Setter method for this notebook's name.
1541 *
1523 *
1542 * @method set_notebook_name
1524 * @method set_notebook_name
1543 * @param {String} name A new name for this notebook
1525 * @param {String} name A new name for this notebook
1544 */
1526 */
1545 Notebook.prototype.set_notebook_name = function (name) {
1527 Notebook.prototype.set_notebook_name = function (name) {
1546 this.notebook_name = name;
1528 this.notebook_name = name;
1547 };
1529 };
1548
1530
1549 /**
1531 /**
1550 * Check that a notebook's name is valid.
1532 * Check that a notebook's name is valid.
1551 *
1533 *
1552 * @method test_notebook_name
1534 * @method test_notebook_name
1553 * @param {String} nbname A name for this notebook
1535 * @param {String} nbname A name for this notebook
1554 * @return {Boolean} True if the name is valid, false if invalid
1536 * @return {Boolean} True if the name is valid, false if invalid
1555 */
1537 */
1556 Notebook.prototype.test_notebook_name = function (nbname) {
1538 Notebook.prototype.test_notebook_name = function (nbname) {
1557 nbname = nbname || '';
1539 nbname = nbname || '';
1558 if (this.notebook_name_blacklist_re.test(nbname) == false && nbname.length>0) {
1540 if (nbname.length>0 && !this.notebook_name_blacklist_re.test(nbname)) {
1559 return true;
1541 return true;
1560 } else {
1542 } else {
1561 return false;
1543 return false;
1562 };
1544 }
1563 };
1545 };
1564
1546
1565 /**
1547 /**
1566 * Load a notebook from JSON (.ipynb).
1548 * Load a notebook from JSON (.ipynb).
1567 *
1549 *
1568 * This currently handles one worksheet: others are deleted.
1550 * This currently handles one worksheet: others are deleted.
1569 *
1551 *
1570 * @method fromJSON
1552 * @method fromJSON
1571 * @param {Object} data JSON representation of a notebook
1553 * @param {Object} data JSON representation of a notebook
1572 */
1554 */
1573 Notebook.prototype.fromJSON = function (data) {
1555 Notebook.prototype.fromJSON = function (data) {
1574 var content = data.content;
1556 var content = data.content;
1575 var ncells = this.ncells();
1557 var ncells = this.ncells();
1576 var i;
1558 var i;
1577 for (i=0; i<ncells; i++) {
1559 for (i=0; i<ncells; i++) {
1578 // Always delete cell 0 as they get renumbered as they are deleted.
1560 // Always delete cell 0 as they get renumbered as they are deleted.
1579 this.delete_cell(0);
1561 this.delete_cell(0);
1580 };
1562 }
1581 // Save the metadata and name.
1563 // Save the metadata and name.
1582 this.metadata = content.metadata;
1564 this.metadata = content.metadata;
1583 this.notebook_name = data.name;
1565 this.notebook_name = data.name;
1584 // Only handle 1 worksheet for now.
1566 // Only handle 1 worksheet for now.
1585 var worksheet = content.worksheets[0];
1567 var worksheet = content.worksheets[0];
1586 if (worksheet !== undefined) {
1568 if (worksheet !== undefined) {
1587 if (worksheet.metadata) {
1569 if (worksheet.metadata) {
1588 this.worksheet_metadata = worksheet.metadata;
1570 this.worksheet_metadata = worksheet.metadata;
1589 }
1571 }
1590 var new_cells = worksheet.cells;
1572 var new_cells = worksheet.cells;
1591 ncells = new_cells.length;
1573 ncells = new_cells.length;
1592 var cell_data = null;
1574 var cell_data = null;
1593 var new_cell = null;
1575 var new_cell = null;
1594 for (i=0; i<ncells; i++) {
1576 for (i=0; i<ncells; i++) {
1595 cell_data = new_cells[i];
1577 cell_data = new_cells[i];
1596 // VERSIONHACK: plaintext -> raw
1578 // VERSIONHACK: plaintext -> raw
1597 // handle never-released plaintext name for raw cells
1579 // handle never-released plaintext name for raw cells
1598 if (cell_data.cell_type === 'plaintext'){
1580 if (cell_data.cell_type === 'plaintext'){
1599 cell_data.cell_type = 'raw';
1581 cell_data.cell_type = 'raw';
1600 }
1582 }
1601
1583
1602 new_cell = this.insert_cell_at_index(cell_data.cell_type, i);
1584 new_cell = this.insert_cell_at_index(cell_data.cell_type, i);
1603 new_cell.fromJSON(cell_data);
1585 new_cell.fromJSON(cell_data);
1604 };
1586 }
1605 };
1587 }
1606 if (content.worksheets.length > 1) {
1588 if (content.worksheets.length > 1) {
1607 IPython.dialog.modal({
1589 IPython.dialog.modal({
1608 title : "Multiple worksheets",
1590 title : "Multiple worksheets",
1609 body : "This notebook has " + data.worksheets.length + " worksheets, " +
1591 body : "This notebook has " + data.worksheets.length + " worksheets, " +
1610 "but this version of IPython can only handle the first. " +
1592 "but this version of IPython can only handle the first. " +
1611 "If you save this notebook, worksheets after the first will be lost.",
1593 "If you save this notebook, worksheets after the first will be lost.",
1612 buttons : {
1594 buttons : {
1613 OK : {
1595 OK : {
1614 class : "btn-danger"
1596 class : "btn-danger"
1615 }
1597 }
1616 }
1598 }
1617 });
1599 });
1618 }
1600 }
1619 };
1601 };
1620
1602
1621 /**
1603 /**
1622 * Dump this notebook into a JSON-friendly object.
1604 * Dump this notebook into a JSON-friendly object.
1623 *
1605 *
1624 * @method toJSON
1606 * @method toJSON
1625 * @return {Object} A JSON-friendly representation of this notebook.
1607 * @return {Object} A JSON-friendly representation of this notebook.
1626 */
1608 */
1627 Notebook.prototype.toJSON = function () {
1609 Notebook.prototype.toJSON = function () {
1628 var cells = this.get_cells();
1610 var cells = this.get_cells();
1629 var ncells = cells.length;
1611 var ncells = cells.length;
1630 var cell_array = new Array(ncells);
1612 var cell_array = new Array(ncells);
1631 for (var i=0; i<ncells; i++) {
1613 for (var i=0; i<ncells; i++) {
1632 cell_array[i] = cells[i].toJSON();
1614 cell_array[i] = cells[i].toJSON();
1633 };
1615 }
1634 var data = {
1616 var data = {
1635 // Only handle 1 worksheet for now.
1617 // Only handle 1 worksheet for now.
1636 worksheets : [{
1618 worksheets : [{
1637 cells: cell_array,
1619 cells: cell_array,
1638 metadata: this.worksheet_metadata
1620 metadata: this.worksheet_metadata
1639 }],
1621 }],
1640 metadata : this.metadata
1622 metadata : this.metadata
1641 };
1623 };
1642 return data;
1624 return data;
1643 };
1625 };
1644
1626
1645 /**
1627 /**
1646 * Start an autosave timer, for periodically saving the notebook.
1628 * Start an autosave timer, for periodically saving the notebook.
1647 *
1629 *
1648 * @method set_autosave_interval
1630 * @method set_autosave_interval
1649 * @param {Integer} interval the autosave interval in milliseconds
1631 * @param {Integer} interval the autosave interval in milliseconds
1650 */
1632 */
1651 Notebook.prototype.set_autosave_interval = function (interval) {
1633 Notebook.prototype.set_autosave_interval = function (interval) {
1652 var that = this;
1634 var that = this;
1653 // clear previous interval, so we don't get simultaneous timers
1635 // clear previous interval, so we don't get simultaneous timers
1654 if (this.autosave_timer) {
1636 if (this.autosave_timer) {
1655 clearInterval(this.autosave_timer);
1637 clearInterval(this.autosave_timer);
1656 }
1638 }
1657
1639
1658 this.autosave_interval = this.minimum_autosave_interval = interval;
1640 this.autosave_interval = this.minimum_autosave_interval = interval;
1659 if (interval) {
1641 if (interval) {
1660 this.autosave_timer = setInterval(function() {
1642 this.autosave_timer = setInterval(function() {
1661 if (that.dirty) {
1643 if (that.dirty) {
1662 that.save_notebook();
1644 that.save_notebook();
1663 }
1645 }
1664 }, interval);
1646 }, interval);
1665 $([IPython.events]).trigger("autosave_enabled.Notebook", interval);
1647 $([IPython.events]).trigger("autosave_enabled.Notebook", interval);
1666 } else {
1648 } else {
1667 this.autosave_timer = null;
1649 this.autosave_timer = null;
1668 $([IPython.events]).trigger("autosave_disabled.Notebook");
1650 $([IPython.events]).trigger("autosave_disabled.Notebook");
1669 };
1651 }
1670 };
1652 };
1671
1653
1672 /**
1654 /**
1673 * Save this notebook on the server.
1655 * Save this notebook on the server.
1674 *
1656 *
1675 * @method save_notebook
1657 * @method save_notebook
1676 */
1658 */
1677 Notebook.prototype.save_notebook = function (extra_settings) {
1659 Notebook.prototype.save_notebook = function (extra_settings) {
1678 // Create a JSON model to be sent to the server.
1660 // Create a JSON model to be sent to the server.
1679 var model = {};
1661 var model = {};
1680 model.name = this.notebook_name;
1662 model.name = this.notebook_name;
1681 model.path = this.notebook_path;
1663 model.path = this.notebook_path;
1682 model.content = this.toJSON();
1664 model.content = this.toJSON();
1683 model.content.nbformat = this.nbformat;
1665 model.content.nbformat = this.nbformat;
1684 model.content.nbformat_minor = this.nbformat_minor;
1666 model.content.nbformat_minor = this.nbformat_minor;
1685 // time the ajax call for autosave tuning purposes.
1667 // time the ajax call for autosave tuning purposes.
1686 var start = new Date().getTime();
1668 var start = new Date().getTime();
1687 // We do the call with settings so we can set cache to false.
1669 // We do the call with settings so we can set cache to false.
1688 var settings = {
1670 var settings = {
1689 processData : false,
1671 processData : false,
1690 cache : false,
1672 cache : false,
1691 type : "PUT",
1673 type : "PUT",
1692 data : JSON.stringify(model),
1674 data : JSON.stringify(model),
1693 headers : {'Content-Type': 'application/json'},
1675 headers : {'Content-Type': 'application/json'},
1694 success : $.proxy(this.save_notebook_success, this, start),
1676 success : $.proxy(this.save_notebook_success, this, start),
1695 error : $.proxy(this.save_notebook_error, this)
1677 error : $.proxy(this.save_notebook_error, this)
1696 };
1678 };
1697 if (extra_settings) {
1679 if (extra_settings) {
1698 for (var key in extra_settings) {
1680 for (var key in extra_settings) {
1699 settings[key] = extra_settings[key];
1681 settings[key] = extra_settings[key];
1700 }
1682 }
1701 }
1683 }
1702 $([IPython.events]).trigger('notebook_saving.Notebook');
1684 $([IPython.events]).trigger('notebook_saving.Notebook');
1703 var url = utils.url_join_encode(
1685 var url = utils.url_join_encode(
1704 this._baseProjectUrl,
1686 this.base_url,
1705 'api/notebooks',
1687 'api/notebooks',
1706 this.notebook_path,
1688 this.notebook_path,
1707 this.notebook_name
1689 this.notebook_name
1708 );
1690 );
1709 $.ajax(url, settings);
1691 $.ajax(url, settings);
1710 };
1692 };
1711
1693
1712 /**
1694 /**
1713 * Success callback for saving a notebook.
1695 * Success callback for saving a notebook.
1714 *
1696 *
1715 * @method save_notebook_success
1697 * @method save_notebook_success
1716 * @param {Integer} start the time when the save request started
1698 * @param {Integer} start the time when the save request started
1717 * @param {Object} data JSON representation of a notebook
1699 * @param {Object} data JSON representation of a notebook
1718 * @param {String} status Description of response status
1700 * @param {String} status Description of response status
1719 * @param {jqXHR} xhr jQuery Ajax object
1701 * @param {jqXHR} xhr jQuery Ajax object
1720 */
1702 */
1721 Notebook.prototype.save_notebook_success = function (start, data, status, xhr) {
1703 Notebook.prototype.save_notebook_success = function (start, data, status, xhr) {
1722 this.set_dirty(false);
1704 this.set_dirty(false);
1723 $([IPython.events]).trigger('notebook_saved.Notebook');
1705 $([IPython.events]).trigger('notebook_saved.Notebook');
1724 this._update_autosave_interval(start);
1706 this._update_autosave_interval(start);
1725 if (this._checkpoint_after_save) {
1707 if (this._checkpoint_after_save) {
1726 this.create_checkpoint();
1708 this.create_checkpoint();
1727 this._checkpoint_after_save = false;
1709 this._checkpoint_after_save = false;
1728 };
1710 }
1729 };
1711 };
1730
1712
1731 /**
1713 /**
1732 * update the autosave interval based on how long the last save took
1714 * update the autosave interval based on how long the last save took
1733 *
1715 *
1734 * @method _update_autosave_interval
1716 * @method _update_autosave_interval
1735 * @param {Integer} timestamp when the save request started
1717 * @param {Integer} timestamp when the save request started
1736 */
1718 */
1737 Notebook.prototype._update_autosave_interval = function (start) {
1719 Notebook.prototype._update_autosave_interval = function (start) {
1738 var duration = (new Date().getTime() - start);
1720 var duration = (new Date().getTime() - start);
1739 if (this.autosave_interval) {
1721 if (this.autosave_interval) {
1740 // new save interval: higher of 10x save duration or parameter (default 30 seconds)
1722 // new save interval: higher of 10x save duration or parameter (default 30 seconds)
1741 var interval = Math.max(10 * duration, this.minimum_autosave_interval);
1723 var interval = Math.max(10 * duration, this.minimum_autosave_interval);
1742 // round to 10 seconds, otherwise we will be setting a new interval too often
1724 // round to 10 seconds, otherwise we will be setting a new interval too often
1743 interval = 10000 * Math.round(interval / 10000);
1725 interval = 10000 * Math.round(interval / 10000);
1744 // set new interval, if it's changed
1726 // set new interval, if it's changed
1745 if (interval != this.autosave_interval) {
1727 if (interval != this.autosave_interval) {
1746 this.set_autosave_interval(interval);
1728 this.set_autosave_interval(interval);
1747 }
1729 }
1748 }
1730 }
1749 };
1731 };
1750
1732
1751 /**
1733 /**
1752 * Failure callback for saving a notebook.
1734 * Failure callback for saving a notebook.
1753 *
1735 *
1754 * @method save_notebook_error
1736 * @method save_notebook_error
1755 * @param {jqXHR} xhr jQuery Ajax object
1737 * @param {jqXHR} xhr jQuery Ajax object
1756 * @param {String} status Description of response status
1738 * @param {String} status Description of response status
1757 * @param {String} error HTTP error message
1739 * @param {String} error HTTP error message
1758 */
1740 */
1759 Notebook.prototype.save_notebook_error = function (xhr, status, error) {
1741 Notebook.prototype.save_notebook_error = function (xhr, status, error) {
1760 $([IPython.events]).trigger('notebook_save_failed.Notebook', [xhr, status, error]);
1742 $([IPython.events]).trigger('notebook_save_failed.Notebook', [xhr, status, error]);
1761 };
1743 };
1762
1744
1763 Notebook.prototype.new_notebook = function(){
1745 Notebook.prototype.new_notebook = function(){
1764 var path = this.notebook_path;
1746 var path = this.notebook_path;
1765 var base_project_url = this._baseProjectUrl;
1747 var base_url = this.base_url;
1766 var settings = {
1748 var settings = {
1767 processData : false,
1749 processData : false,
1768 cache : false,
1750 cache : false,
1769 type : "POST",
1751 type : "POST",
1770 dataType : "json",
1752 dataType : "json",
1771 async : false,
1753 async : false,
1772 success : function (data, status, xhr){
1754 success : function (data, status, xhr){
1773 var notebook_name = data.name;
1755 var notebook_name = data.name;
1774 window.open(
1756 window.open(
1775 utils.url_join_encode(
1757 utils.url_join_encode(
1776 base_project_url,
1758 base_url,
1777 'notebooks',
1759 'notebooks',
1778 path,
1760 path,
1779 notebook_name
1761 notebook_name
1780 ),
1762 ),
1781 '_blank'
1763 '_blank'
1782 );
1764 );
1783 }
1765 }
1784 };
1766 };
1785 var url = utils.url_join_encode(
1767 var url = utils.url_join_encode(
1786 base_project_url,
1768 base_url,
1787 'api/notebooks',
1769 'api/notebooks',
1788 path
1770 path
1789 );
1771 );
1790 $.ajax(url,settings);
1772 $.ajax(url,settings);
1791 };
1773 };
1792
1774
1793
1775
1794 Notebook.prototype.copy_notebook = function(){
1776 Notebook.prototype.copy_notebook = function(){
1795 var path = this.notebook_path;
1777 var path = this.notebook_path;
1796 var base_project_url = this._baseProjectUrl;
1778 var base_url = this.base_url;
1797 var settings = {
1779 var settings = {
1798 processData : false,
1780 processData : false,
1799 cache : false,
1781 cache : false,
1800 type : "POST",
1782 type : "POST",
1801 dataType : "json",
1783 dataType : "json",
1802 data : JSON.stringify({copy_from : this.notebook_name}),
1784 data : JSON.stringify({copy_from : this.notebook_name}),
1803 async : false,
1785 async : false,
1804 success : function (data, status, xhr) {
1786 success : function (data, status, xhr) {
1805 window.open(utils.url_join_encode(
1787 window.open(utils.url_join_encode(
1806 base_project_url,
1788 base_url,
1807 'notebooks',
1789 'notebooks',
1808 data.path,
1790 data.path,
1809 data.name
1791 data.name
1810 ), '_blank');
1792 ), '_blank');
1811 }
1793 }
1812 };
1794 };
1813 var url = utils.url_join_encode(
1795 var url = utils.url_join_encode(
1814 base_project_url,
1796 base_url,
1815 'api/notebooks',
1797 'api/notebooks',
1816 path
1798 path
1817 );
1799 );
1818 $.ajax(url,settings);
1800 $.ajax(url,settings);
1819 };
1801 };
1820
1802
1821 Notebook.prototype.rename = function (nbname) {
1803 Notebook.prototype.rename = function (nbname) {
1822 var that = this;
1804 var that = this;
1823 var data = {name: nbname + '.ipynb'};
1805 if (!nbname.match(/\.ipynb$/)) {
1806 nbname = nbname + ".ipynb";
1807 }
1808 var data = {name: nbname};
1824 var settings = {
1809 var settings = {
1825 processData : false,
1810 processData : false,
1826 cache : false,
1811 cache : false,
1827 type : "PATCH",
1812 type : "PATCH",
1828 data : JSON.stringify(data),
1813 data : JSON.stringify(data),
1829 dataType: "json",
1814 dataType: "json",
1830 headers : {'Content-Type': 'application/json'},
1815 headers : {'Content-Type': 'application/json'},
1831 success : $.proxy(that.rename_success, this),
1816 success : $.proxy(that.rename_success, this),
1832 error : $.proxy(that.rename_error, this)
1817 error : $.proxy(that.rename_error, this)
1833 };
1818 };
1834 $([IPython.events]).trigger('rename_notebook.Notebook', data);
1819 $([IPython.events]).trigger('rename_notebook.Notebook', data);
1835 var url = utils.url_join_encode(
1820 var url = utils.url_join_encode(
1836 this._baseProjectUrl,
1821 this.base_url,
1837 'api/notebooks',
1822 'api/notebooks',
1838 this.notebook_path,
1823 this.notebook_path,
1839 this.notebook_name
1824 this.notebook_name
1840 );
1825 );
1841 $.ajax(url, settings);
1826 $.ajax(url, settings);
1842 };
1827 };
1843
1828
1844 Notebook.prototype.delete = function () {
1829 Notebook.prototype.delete = function () {
1845 var that = this;
1830 var that = this;
1846 var settings = {
1831 var settings = {
1847 processData : false,
1832 processData : false,
1848 cache : false,
1833 cache : false,
1849 type : "DELETE",
1834 type : "DELETE",
1850 dataType: "json",
1835 dataType: "json",
1851 };
1836 };
1852 var url = utils.url_join_encode(
1837 var url = utils.url_join_encode(
1853 this._baseProjectUrl,
1838 this.base_url,
1854 'api/notebooks',
1839 'api/notebooks',
1855 this.notebook_path,
1840 this.notebook_path,
1856 this.notebook_name
1841 this.notebook_name
1857 );
1842 );
1858 $.ajax(url, settings);
1843 $.ajax(url, settings);
1859 };
1844 };
1860
1845
1861
1846
1862 Notebook.prototype.rename_success = function (json, status, xhr) {
1847 Notebook.prototype.rename_success = function (json, status, xhr) {
1863 this.notebook_name = json.name;
1848 var name = this.notebook_name = json.name;
1864 var name = this.notebook_name;
1865 var path = json.path;
1849 var path = json.path;
1866 this.session.rename_notebook(name, path);
1850 this.session.rename_notebook(name, path);
1867 $([IPython.events]).trigger('notebook_renamed.Notebook', json);
1851 $([IPython.events]).trigger('notebook_renamed.Notebook', json);
1868 }
1852 };
1869
1853
1870 Notebook.prototype.rename_error = function (xhr, status, error) {
1854 Notebook.prototype.rename_error = function (xhr, status, error) {
1871 var that = this;
1855 var that = this;
1872 var dialog = $('<div/>').append(
1856 var dialog = $('<div/>').append(
1873 $("<p/>").addClass("rename-message")
1857 $("<p/>").addClass("rename-message")
1874 .text('This notebook name already exists.')
1858 .text('This notebook name already exists.')
1875 )
1859 );
1876 $([IPython.events]).trigger('notebook_rename_failed.Notebook', [xhr, status, error]);
1860 $([IPython.events]).trigger('notebook_rename_failed.Notebook', [xhr, status, error]);
1877 IPython.dialog.modal({
1861 IPython.dialog.modal({
1878 title: "Notebook Rename Error!",
1862 title: "Notebook Rename Error!",
1879 body: dialog,
1863 body: dialog,
1880 buttons : {
1864 buttons : {
1881 "Cancel": {},
1865 "Cancel": {},
1882 "OK": {
1866 "OK": {
1883 class: "btn-primary",
1867 class: "btn-primary",
1884 click: function () {
1868 click: function () {
1885 IPython.save_widget.rename_notebook();
1869 IPython.save_widget.rename_notebook();
1886 }}
1870 }}
1887 },
1871 },
1888 open : function (event, ui) {
1872 open : function (event, ui) {
1889 var that = $(this);
1873 var that = $(this);
1890 // Upon ENTER, click the OK button.
1874 // Upon ENTER, click the OK button.
1891 that.find('input[type="text"]').keydown(function (event, ui) {
1875 that.find('input[type="text"]').keydown(function (event, ui) {
1892 if (event.which === utils.keycodes.ENTER) {
1876 if (event.which === utils.keycodes.ENTER) {
1893 that.find('.btn-primary').first().click();
1877 that.find('.btn-primary').first().click();
1894 }
1878 }
1895 });
1879 });
1896 that.find('input[type="text"]').focus();
1880 that.find('input[type="text"]').focus();
1897 }
1881 }
1898 });
1882 });
1899 }
1883 };
1900
1884
1901 /**
1885 /**
1902 * Request a notebook's data from the server.
1886 * Request a notebook's data from the server.
1903 *
1887 *
1904 * @method load_notebook
1888 * @method load_notebook
1905 * @param {String} notebook_name and path A notebook to load
1889 * @param {String} notebook_name and path A notebook to load
1906 */
1890 */
1907 Notebook.prototype.load_notebook = function (notebook_name, notebook_path) {
1891 Notebook.prototype.load_notebook = function (notebook_name, notebook_path) {
1908 var that = this;
1892 var that = this;
1909 this.notebook_name = notebook_name;
1893 this.notebook_name = notebook_name;
1910 this.notebook_path = notebook_path;
1894 this.notebook_path = notebook_path;
1911 // We do the call with settings so we can set cache to false.
1895 // We do the call with settings so we can set cache to false.
1912 var settings = {
1896 var settings = {
1913 processData : false,
1897 processData : false,
1914 cache : false,
1898 cache : false,
1915 type : "GET",
1899 type : "GET",
1916 dataType : "json",
1900 dataType : "json",
1917 success : $.proxy(this.load_notebook_success,this),
1901 success : $.proxy(this.load_notebook_success,this),
1918 error : $.proxy(this.load_notebook_error,this),
1902 error : $.proxy(this.load_notebook_error,this),
1919 };
1903 };
1920 $([IPython.events]).trigger('notebook_loading.Notebook');
1904 $([IPython.events]).trigger('notebook_loading.Notebook');
1921 var url = utils.url_join_encode(
1905 var url = utils.url_join_encode(
1922 this._baseProjectUrl,
1906 this.base_url,
1923 'api/notebooks',
1907 'api/notebooks',
1924 this.notebook_path,
1908 this.notebook_path,
1925 this.notebook_name
1909 this.notebook_name
1926 );
1910 );
1927 $.ajax(url, settings);
1911 $.ajax(url, settings);
1928 };
1912 };
1929
1913
1930 /**
1914 /**
1931 * Success callback for loading a notebook from the server.
1915 * Success callback for loading a notebook from the server.
1932 *
1916 *
1933 * Load notebook data from the JSON response.
1917 * Load notebook data from the JSON response.
1934 *
1918 *
1935 * @method load_notebook_success
1919 * @method load_notebook_success
1936 * @param {Object} data JSON representation of a notebook
1920 * @param {Object} data JSON representation of a notebook
1937 * @param {String} status Description of response status
1921 * @param {String} status Description of response status
1938 * @param {jqXHR} xhr jQuery Ajax object
1922 * @param {jqXHR} xhr jQuery Ajax object
1939 */
1923 */
1940 Notebook.prototype.load_notebook_success = function (data, status, xhr) {
1924 Notebook.prototype.load_notebook_success = function (data, status, xhr) {
1941 this.fromJSON(data);
1925 this.fromJSON(data);
1942 if (this.ncells() === 0) {
1926 if (this.ncells() === 0) {
1943 this.insert_cell_below('code');
1927 this.insert_cell_below('code');
1944 this.select(0);
1928 this.select(0);
1945 this.edit_mode();
1929 this.edit_mode();
1946 } else {
1930 } else {
1947 this.select(0);
1931 this.select(0);
1948 this.command_mode();
1932 this.command_mode();
1949 };
1933 }
1950 this.set_dirty(false);
1934 this.set_dirty(false);
1951 this.scroll_to_top();
1935 this.scroll_to_top();
1952 if (data.orig_nbformat !== undefined && data.nbformat !== data.orig_nbformat) {
1936 if (data.orig_nbformat !== undefined && data.nbformat !== data.orig_nbformat) {
1953 var msg = "This notebook has been converted from an older " +
1937 var msg = "This notebook has been converted from an older " +
1954 "notebook format (v"+data.orig_nbformat+") to the current notebook " +
1938 "notebook format (v"+data.orig_nbformat+") to the current notebook " +
1955 "format (v"+data.nbformat+"). The next time you save this notebook, the " +
1939 "format (v"+data.nbformat+"). The next time you save this notebook, the " +
1956 "newer notebook format will be used and older versions of IPython " +
1940 "newer notebook format will be used and older versions of IPython " +
1957 "may not be able to read it. To keep the older version, close the " +
1941 "may not be able to read it. To keep the older version, close the " +
1958 "notebook without saving it.";
1942 "notebook without saving it.";
1959 IPython.dialog.modal({
1943 IPython.dialog.modal({
1960 title : "Notebook converted",
1944 title : "Notebook converted",
1961 body : msg,
1945 body : msg,
1962 buttons : {
1946 buttons : {
1963 OK : {
1947 OK : {
1964 class : "btn-primary"
1948 class : "btn-primary"
1965 }
1949 }
1966 }
1950 }
1967 });
1951 });
1968 } else if (data.orig_nbformat_minor !== undefined && data.nbformat_minor !== data.orig_nbformat_minor) {
1952 } else if (data.orig_nbformat_minor !== undefined && data.nbformat_minor !== data.orig_nbformat_minor) {
1969 var that = this;
1953 var that = this;
1970 var orig_vs = 'v' + data.nbformat + '.' + data.orig_nbformat_minor;
1954 var orig_vs = 'v' + data.nbformat + '.' + data.orig_nbformat_minor;
1971 var this_vs = 'v' + data.nbformat + '.' + this.nbformat_minor;
1955 var this_vs = 'v' + data.nbformat + '.' + this.nbformat_minor;
1972 var msg = "This notebook is version " + orig_vs + ", but we only fully support up to " +
1956 var msg = "This notebook is version " + orig_vs + ", but we only fully support up to " +
1973 this_vs + ". You can still work with this notebook, but some features " +
1957 this_vs + ". You can still work with this notebook, but some features " +
1974 "introduced in later notebook versions may not be available."
1958 "introduced in later notebook versions may not be available.";
1975
1959
1976 IPython.dialog.modal({
1960 IPython.dialog.modal({
1977 title : "Newer Notebook",
1961 title : "Newer Notebook",
1978 body : msg,
1962 body : msg,
1979 buttons : {
1963 buttons : {
1980 OK : {
1964 OK : {
1981 class : "btn-danger"
1965 class : "btn-danger"
1982 }
1966 }
1983 }
1967 }
1984 });
1968 });
1985
1969
1986 }
1970 }
1987
1971
1988 // Create the session after the notebook is completely loaded to prevent
1972 // Create the session after the notebook is completely loaded to prevent
1989 // code execution upon loading, which is a security risk.
1973 // code execution upon loading, which is a security risk.
1990 if (this.session == null) {
1974 if (this.session === null) {
1991 this.start_session();
1975 this.start_session();
1992 }
1976 }
1993 // load our checkpoint list
1977 // load our checkpoint list
1994 this.list_checkpoints();
1978 this.list_checkpoints();
1995
1979
1996 // load toolbar state
1980 // load toolbar state
1997 if (this.metadata.celltoolbar) {
1981 if (this.metadata.celltoolbar) {
1998 IPython.CellToolbar.global_show();
1982 IPython.CellToolbar.global_show();
1999 IPython.CellToolbar.activate_preset(this.metadata.celltoolbar);
1983 IPython.CellToolbar.activate_preset(this.metadata.celltoolbar);
2000 }
1984 }
2001
1985
2002 $([IPython.events]).trigger('notebook_loaded.Notebook');
1986 $([IPython.events]).trigger('notebook_loaded.Notebook');
2003 };
1987 };
2004
1988
2005 /**
1989 /**
2006 * Failure callback for loading a notebook from the server.
1990 * Failure callback for loading a notebook from the server.
2007 *
1991 *
2008 * @method load_notebook_error
1992 * @method load_notebook_error
2009 * @param {jqXHR} xhr jQuery Ajax object
1993 * @param {jqXHR} xhr jQuery Ajax object
2010 * @param {String} status Description of response status
1994 * @param {String} status Description of response status
2011 * @param {String} error HTTP error message
1995 * @param {String} error HTTP error message
2012 */
1996 */
2013 Notebook.prototype.load_notebook_error = function (xhr, status, error) {
1997 Notebook.prototype.load_notebook_error = function (xhr, status, error) {
2014 $([IPython.events]).trigger('notebook_load_failed.Notebook', [xhr, status, error]);
1998 $([IPython.events]).trigger('notebook_load_failed.Notebook', [xhr, status, error]);
1999 var msg;
2015 if (xhr.status === 400) {
2000 if (xhr.status === 400) {
2016 var msg = error;
2001 msg = error;
2017 } else if (xhr.status === 500) {
2002 } else if (xhr.status === 500) {
2018 var msg = "An unknown error occurred while loading this notebook. " +
2003 msg = "An unknown error occurred while loading this notebook. " +
2019 "This version can load notebook formats " +
2004 "This version can load notebook formats " +
2020 "v" + this.nbformat + " or earlier.";
2005 "v" + this.nbformat + " or earlier.";
2021 }
2006 }
2022 IPython.dialog.modal({
2007 IPython.dialog.modal({
2023 title: "Error loading notebook",
2008 title: "Error loading notebook",
2024 body : msg,
2009 body : msg,
2025 buttons : {
2010 buttons : {
2026 "OK": {}
2011 "OK": {}
2027 }
2012 }
2028 });
2013 });
2029 }
2014 };
2030
2015
2031 /********************* checkpoint-related *********************/
2016 /********************* checkpoint-related *********************/
2032
2017
2033 /**
2018 /**
2034 * Save the notebook then immediately create a checkpoint.
2019 * Save the notebook then immediately create a checkpoint.
2035 *
2020 *
2036 * @method save_checkpoint
2021 * @method save_checkpoint
2037 */
2022 */
2038 Notebook.prototype.save_checkpoint = function () {
2023 Notebook.prototype.save_checkpoint = function () {
2039 this._checkpoint_after_save = true;
2024 this._checkpoint_after_save = true;
2040 this.save_notebook();
2025 this.save_notebook();
2041 };
2026 };
2042
2027
2043 /**
2028 /**
2044 * Add a checkpoint for this notebook.
2029 * Add a checkpoint for this notebook.
2045 * for use as a callback from checkpoint creation.
2030 * for use as a callback from checkpoint creation.
2046 *
2031 *
2047 * @method add_checkpoint
2032 * @method add_checkpoint
2048 */
2033 */
2049 Notebook.prototype.add_checkpoint = function (checkpoint) {
2034 Notebook.prototype.add_checkpoint = function (checkpoint) {
2050 var found = false;
2035 var found = false;
2051 for (var i = 0; i < this.checkpoints.length; i++) {
2036 for (var i = 0; i < this.checkpoints.length; i++) {
2052 var existing = this.checkpoints[i];
2037 var existing = this.checkpoints[i];
2053 if (existing.id == checkpoint.id) {
2038 if (existing.id == checkpoint.id) {
2054 found = true;
2039 found = true;
2055 this.checkpoints[i] = checkpoint;
2040 this.checkpoints[i] = checkpoint;
2056 break;
2041 break;
2057 }
2042 }
2058 }
2043 }
2059 if (!found) {
2044 if (!found) {
2060 this.checkpoints.push(checkpoint);
2045 this.checkpoints.push(checkpoint);
2061 }
2046 }
2062 this.last_checkpoint = this.checkpoints[this.checkpoints.length - 1];
2047 this.last_checkpoint = this.checkpoints[this.checkpoints.length - 1];
2063 };
2048 };
2064
2049
2065 /**
2050 /**
2066 * List checkpoints for this notebook.
2051 * List checkpoints for this notebook.
2067 *
2052 *
2068 * @method list_checkpoints
2053 * @method list_checkpoints
2069 */
2054 */
2070 Notebook.prototype.list_checkpoints = function () {
2055 Notebook.prototype.list_checkpoints = function () {
2071 var url = utils.url_join_encode(
2056 var url = utils.url_join_encode(
2072 this._baseProjectUrl,
2057 this.base_url,
2073 'api/notebooks',
2058 'api/notebooks',
2074 this.notebook_path,
2059 this.notebook_path,
2075 this.notebook_name,
2060 this.notebook_name,
2076 'checkpoints'
2061 'checkpoints'
2077 );
2062 );
2078 $.get(url).done(
2063 $.get(url).done(
2079 $.proxy(this.list_checkpoints_success, this)
2064 $.proxy(this.list_checkpoints_success, this)
2080 ).fail(
2065 ).fail(
2081 $.proxy(this.list_checkpoints_error, this)
2066 $.proxy(this.list_checkpoints_error, this)
2082 );
2067 );
2083 };
2068 };
2084
2069
2085 /**
2070 /**
2086 * Success callback for listing checkpoints.
2071 * Success callback for listing checkpoints.
2087 *
2072 *
2088 * @method list_checkpoint_success
2073 * @method list_checkpoint_success
2089 * @param {Object} data JSON representation of a checkpoint
2074 * @param {Object} data JSON representation of a checkpoint
2090 * @param {String} status Description of response status
2075 * @param {String} status Description of response status
2091 * @param {jqXHR} xhr jQuery Ajax object
2076 * @param {jqXHR} xhr jQuery Ajax object
2092 */
2077 */
2093 Notebook.prototype.list_checkpoints_success = function (data, status, xhr) {
2078 Notebook.prototype.list_checkpoints_success = function (data, status, xhr) {
2094 var data = $.parseJSON(data);
2079 data = $.parseJSON(data);
2095 this.checkpoints = data;
2080 this.checkpoints = data;
2096 if (data.length) {
2081 if (data.length) {
2097 this.last_checkpoint = data[data.length - 1];
2082 this.last_checkpoint = data[data.length - 1];
2098 } else {
2083 } else {
2099 this.last_checkpoint = null;
2084 this.last_checkpoint = null;
2100 }
2085 }
2101 $([IPython.events]).trigger('checkpoints_listed.Notebook', [data]);
2086 $([IPython.events]).trigger('checkpoints_listed.Notebook', [data]);
2102 };
2087 };
2103
2088
2104 /**
2089 /**
2105 * Failure callback for listing a checkpoint.
2090 * Failure callback for listing a checkpoint.
2106 *
2091 *
2107 * @method list_checkpoint_error
2092 * @method list_checkpoint_error
2108 * @param {jqXHR} xhr jQuery Ajax object
2093 * @param {jqXHR} xhr jQuery Ajax object
2109 * @param {String} status Description of response status
2094 * @param {String} status Description of response status
2110 * @param {String} error_msg HTTP error message
2095 * @param {String} error_msg HTTP error message
2111 */
2096 */
2112 Notebook.prototype.list_checkpoints_error = function (xhr, status, error_msg) {
2097 Notebook.prototype.list_checkpoints_error = function (xhr, status, error_msg) {
2113 $([IPython.events]).trigger('list_checkpoints_failed.Notebook');
2098 $([IPython.events]).trigger('list_checkpoints_failed.Notebook');
2114 };
2099 };
2115
2100
2116 /**
2101 /**
2117 * Create a checkpoint of this notebook on the server from the most recent save.
2102 * Create a checkpoint of this notebook on the server from the most recent save.
2118 *
2103 *
2119 * @method create_checkpoint
2104 * @method create_checkpoint
2120 */
2105 */
2121 Notebook.prototype.create_checkpoint = function () {
2106 Notebook.prototype.create_checkpoint = function () {
2122 var url = utils.url_join_encode(
2107 var url = utils.url_join_encode(
2123 this._baseProjectUrl,
2108 this.base_url,
2124 'api/notebooks',
2109 'api/notebooks',
2125 this.notebookPath(),
2110 this.notebook_path,
2126 this.notebook_name,
2111 this.notebook_name,
2127 'checkpoints'
2112 'checkpoints'
2128 );
2113 );
2129 $.post(url).done(
2114 $.post(url).done(
2130 $.proxy(this.create_checkpoint_success, this)
2115 $.proxy(this.create_checkpoint_success, this)
2131 ).fail(
2116 ).fail(
2132 $.proxy(this.create_checkpoint_error, this)
2117 $.proxy(this.create_checkpoint_error, this)
2133 );
2118 );
2134 };
2119 };
2135
2120
2136 /**
2121 /**
2137 * Success callback for creating a checkpoint.
2122 * Success callback for creating a checkpoint.
2138 *
2123 *
2139 * @method create_checkpoint_success
2124 * @method create_checkpoint_success
2140 * @param {Object} data JSON representation of a checkpoint
2125 * @param {Object} data JSON representation of a checkpoint
2141 * @param {String} status Description of response status
2126 * @param {String} status Description of response status
2142 * @param {jqXHR} xhr jQuery Ajax object
2127 * @param {jqXHR} xhr jQuery Ajax object
2143 */
2128 */
2144 Notebook.prototype.create_checkpoint_success = function (data, status, xhr) {
2129 Notebook.prototype.create_checkpoint_success = function (data, status, xhr) {
2145 var data = $.parseJSON(data);
2130 data = $.parseJSON(data);
2146 this.add_checkpoint(data);
2131 this.add_checkpoint(data);
2147 $([IPython.events]).trigger('checkpoint_created.Notebook', data);
2132 $([IPython.events]).trigger('checkpoint_created.Notebook', data);
2148 };
2133 };
2149
2134
2150 /**
2135 /**
2151 * Failure callback for creating a checkpoint.
2136 * Failure callback for creating a checkpoint.
2152 *
2137 *
2153 * @method create_checkpoint_error
2138 * @method create_checkpoint_error
2154 * @param {jqXHR} xhr jQuery Ajax object
2139 * @param {jqXHR} xhr jQuery Ajax object
2155 * @param {String} status Description of response status
2140 * @param {String} status Description of response status
2156 * @param {String} error_msg HTTP error message
2141 * @param {String} error_msg HTTP error message
2157 */
2142 */
2158 Notebook.prototype.create_checkpoint_error = function (xhr, status, error_msg) {
2143 Notebook.prototype.create_checkpoint_error = function (xhr, status, error_msg) {
2159 $([IPython.events]).trigger('checkpoint_failed.Notebook');
2144 $([IPython.events]).trigger('checkpoint_failed.Notebook');
2160 };
2145 };
2161
2146
2162 Notebook.prototype.restore_checkpoint_dialog = function (checkpoint) {
2147 Notebook.prototype.restore_checkpoint_dialog = function (checkpoint) {
2163 var that = this;
2148 var that = this;
2164 var checkpoint = checkpoint || this.last_checkpoint;
2149 checkpoint = checkpoint || this.last_checkpoint;
2165 if ( ! checkpoint ) {
2150 if ( ! checkpoint ) {
2166 console.log("restore dialog, but no checkpoint to restore to!");
2151 console.log("restore dialog, but no checkpoint to restore to!");
2167 return;
2152 return;
2168 }
2153 }
2169 var body = $('<div/>').append(
2154 var body = $('<div/>').append(
2170 $('<p/>').addClass("p-space").text(
2155 $('<p/>').addClass("p-space").text(
2171 "Are you sure you want to revert the notebook to " +
2156 "Are you sure you want to revert the notebook to " +
2172 "the latest checkpoint?"
2157 "the latest checkpoint?"
2173 ).append(
2158 ).append(
2174 $("<strong/>").text(
2159 $("<strong/>").text(
2175 " This cannot be undone."
2160 " This cannot be undone."
2176 )
2161 )
2177 )
2162 )
2178 ).append(
2163 ).append(
2179 $('<p/>').addClass("p-space").text("The checkpoint was last updated at:")
2164 $('<p/>').addClass("p-space").text("The checkpoint was last updated at:")
2180 ).append(
2165 ).append(
2181 $('<p/>').addClass("p-space").text(
2166 $('<p/>').addClass("p-space").text(
2182 Date(checkpoint.last_modified)
2167 Date(checkpoint.last_modified)
2183 ).css("text-align", "center")
2168 ).css("text-align", "center")
2184 );
2169 );
2185
2170
2186 IPython.dialog.modal({
2171 IPython.dialog.modal({
2187 title : "Revert notebook to checkpoint",
2172 title : "Revert notebook to checkpoint",
2188 body : body,
2173 body : body,
2189 buttons : {
2174 buttons : {
2190 Revert : {
2175 Revert : {
2191 class : "btn-danger",
2176 class : "btn-danger",
2192 click : function () {
2177 click : function () {
2193 that.restore_checkpoint(checkpoint.id);
2178 that.restore_checkpoint(checkpoint.id);
2194 }
2179 }
2195 },
2180 },
2196 Cancel : {}
2181 Cancel : {}
2197 }
2182 }
2198 });
2183 });
2199 }
2184 };
2200
2185
2201 /**
2186 /**
2202 * Restore the notebook to a checkpoint state.
2187 * Restore the notebook to a checkpoint state.
2203 *
2188 *
2204 * @method restore_checkpoint
2189 * @method restore_checkpoint
2205 * @param {String} checkpoint ID
2190 * @param {String} checkpoint ID
2206 */
2191 */
2207 Notebook.prototype.restore_checkpoint = function (checkpoint) {
2192 Notebook.prototype.restore_checkpoint = function (checkpoint) {
2208 $([IPython.events]).trigger('notebook_restoring.Notebook', checkpoint);
2193 $([IPython.events]).trigger('notebook_restoring.Notebook', checkpoint);
2209 var url = utils.url_join_encode(
2194 var url = utils.url_join_encode(
2210 this._baseProjectUrl,
2195 this.base_url,
2211 'api/notebooks',
2196 'api/notebooks',
2212 this.notebookPath(),
2197 this.notebook_path,
2213 this.notebook_name,
2198 this.notebook_name,
2214 'checkpoints',
2199 'checkpoints',
2215 checkpoint
2200 checkpoint
2216 );
2201 );
2217 $.post(url).done(
2202 $.post(url).done(
2218 $.proxy(this.restore_checkpoint_success, this)
2203 $.proxy(this.restore_checkpoint_success, this)
2219 ).fail(
2204 ).fail(
2220 $.proxy(this.restore_checkpoint_error, this)
2205 $.proxy(this.restore_checkpoint_error, this)
2221 );
2206 );
2222 };
2207 };
2223
2208
2224 /**
2209 /**
2225 * Success callback for restoring a notebook to a checkpoint.
2210 * Success callback for restoring a notebook to a checkpoint.
2226 *
2211 *
2227 * @method restore_checkpoint_success
2212 * @method restore_checkpoint_success
2228 * @param {Object} data (ignored, should be empty)
2213 * @param {Object} data (ignored, should be empty)
2229 * @param {String} status Description of response status
2214 * @param {String} status Description of response status
2230 * @param {jqXHR} xhr jQuery Ajax object
2215 * @param {jqXHR} xhr jQuery Ajax object
2231 */
2216 */
2232 Notebook.prototype.restore_checkpoint_success = function (data, status, xhr) {
2217 Notebook.prototype.restore_checkpoint_success = function (data, status, xhr) {
2233 $([IPython.events]).trigger('checkpoint_restored.Notebook');
2218 $([IPython.events]).trigger('checkpoint_restored.Notebook');
2234 this.load_notebook(this.notebook_name, this.notebook_path);
2219 this.load_notebook(this.notebook_name, this.notebook_path);
2235 };
2220 };
2236
2221
2237 /**
2222 /**
2238 * Failure callback for restoring a notebook to a checkpoint.
2223 * Failure callback for restoring a notebook to a checkpoint.
2239 *
2224 *
2240 * @method restore_checkpoint_error
2225 * @method restore_checkpoint_error
2241 * @param {jqXHR} xhr jQuery Ajax object
2226 * @param {jqXHR} xhr jQuery Ajax object
2242 * @param {String} status Description of response status
2227 * @param {String} status Description of response status
2243 * @param {String} error_msg HTTP error message
2228 * @param {String} error_msg HTTP error message
2244 */
2229 */
2245 Notebook.prototype.restore_checkpoint_error = function (xhr, status, error_msg) {
2230 Notebook.prototype.restore_checkpoint_error = function (xhr, status, error_msg) {
2246 $([IPython.events]).trigger('checkpoint_restore_failed.Notebook');
2231 $([IPython.events]).trigger('checkpoint_restore_failed.Notebook');
2247 };
2232 };
2248
2233
2249 /**
2234 /**
2250 * Delete a notebook checkpoint.
2235 * Delete a notebook checkpoint.
2251 *
2236 *
2252 * @method delete_checkpoint
2237 * @method delete_checkpoint
2253 * @param {String} checkpoint ID
2238 * @param {String} checkpoint ID
2254 */
2239 */
2255 Notebook.prototype.delete_checkpoint = function (checkpoint) {
2240 Notebook.prototype.delete_checkpoint = function (checkpoint) {
2256 $([IPython.events]).trigger('notebook_restoring.Notebook', checkpoint);
2241 $([IPython.events]).trigger('notebook_restoring.Notebook', checkpoint);
2257 var url = utils.url_join_encode(
2242 var url = utils.url_join_encode(
2258 this._baseProjectUrl,
2243 this.base_url,
2259 'api/notebooks',
2244 'api/notebooks',
2260 this.notebookPath(),
2245 this.notebook_path,
2261 this.notebook_name,
2246 this.notebook_name,
2262 'checkpoints',
2247 'checkpoints',
2263 checkpoint
2248 checkpoint
2264 );
2249 );
2265 $.ajax(url, {
2250 $.ajax(url, {
2266 type: 'DELETE',
2251 type: 'DELETE',
2267 success: $.proxy(this.delete_checkpoint_success, this),
2252 success: $.proxy(this.delete_checkpoint_success, this),
2268 error: $.proxy(this.delete_notebook_error,this)
2253 error: $.proxy(this.delete_notebook_error,this)
2269 });
2254 });
2270 };
2255 };
2271
2256
2272 /**
2257 /**
2273 * Success callback for deleting a notebook checkpoint
2258 * Success callback for deleting a notebook checkpoint
2274 *
2259 *
2275 * @method delete_checkpoint_success
2260 * @method delete_checkpoint_success
2276 * @param {Object} data (ignored, should be empty)
2261 * @param {Object} data (ignored, should be empty)
2277 * @param {String} status Description of response status
2262 * @param {String} status Description of response status
2278 * @param {jqXHR} xhr jQuery Ajax object
2263 * @param {jqXHR} xhr jQuery Ajax object
2279 */
2264 */
2280 Notebook.prototype.delete_checkpoint_success = function (data, status, xhr) {
2265 Notebook.prototype.delete_checkpoint_success = function (data, status, xhr) {
2281 $([IPython.events]).trigger('checkpoint_deleted.Notebook', data);
2266 $([IPython.events]).trigger('checkpoint_deleted.Notebook', data);
2282 this.load_notebook(this.notebook_name, this.notebook_path);
2267 this.load_notebook(this.notebook_name, this.notebook_path);
2283 };
2268 };
2284
2269
2285 /**
2270 /**
2286 * Failure callback for deleting a notebook checkpoint.
2271 * Failure callback for deleting a notebook checkpoint.
2287 *
2272 *
2288 * @method delete_checkpoint_error
2273 * @method delete_checkpoint_error
2289 * @param {jqXHR} xhr jQuery Ajax object
2274 * @param {jqXHR} xhr jQuery Ajax object
2290 * @param {String} status Description of response status
2275 * @param {String} status Description of response status
2291 * @param {String} error_msg HTTP error message
2276 * @param {String} error_msg HTTP error message
2292 */
2277 */
2293 Notebook.prototype.delete_checkpoint_error = function (xhr, status, error_msg) {
2278 Notebook.prototype.delete_checkpoint_error = function (xhr, status, error_msg) {
2294 $([IPython.events]).trigger('checkpoint_delete_failed.Notebook');
2279 $([IPython.events]).trigger('checkpoint_delete_failed.Notebook');
2295 };
2280 };
2296
2281
2297
2282
2298 IPython.Notebook = Notebook;
2283 IPython.Notebook = Notebook;
2299
2284
2300
2285
2301 return IPython;
2286 return IPython;
2302
2287
2303 }(IPython));
2288 }(IPython));
@@ -1,173 +1,173 b''
1 //----------------------------------------------------------------------------
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2008-2011 The IPython Development Team
2 // Copyright (C) 2008-2011 The IPython Development Team
3 //
3 //
4 // Distributed under the terms of the BSD License. The full license is in
4 // Distributed under the terms of the BSD License. The full license is in
5 // the file COPYING, distributed as part of this software.
5 // the file COPYING, distributed as part of this software.
6 //----------------------------------------------------------------------------
6 //----------------------------------------------------------------------------
7
7
8 //============================================================================
8 //============================================================================
9 // SaveWidget
9 // SaveWidget
10 //============================================================================
10 //============================================================================
11
11
12 var IPython = (function (IPython) {
12 var IPython = (function (IPython) {
13 "use strict";
13 "use strict";
14
14
15 var utils = IPython.utils;
15 var utils = IPython.utils;
16
16
17 var SaveWidget = function (selector) {
17 var SaveWidget = function (selector) {
18 this.selector = selector;
18 this.selector = selector;
19 if (this.selector !== undefined) {
19 if (this.selector !== undefined) {
20 this.element = $(selector);
20 this.element = $(selector);
21 this.style();
21 this.style();
22 this.bind_events();
22 this.bind_events();
23 }
23 }
24 };
24 };
25
25
26
26
27 SaveWidget.prototype.style = function () {
27 SaveWidget.prototype.style = function () {
28 };
28 };
29
29
30
30
31 SaveWidget.prototype.bind_events = function () {
31 SaveWidget.prototype.bind_events = function () {
32 var that = this;
32 var that = this;
33 this.element.find('span#notebook_name').click(function () {
33 this.element.find('span#notebook_name').click(function () {
34 that.rename_notebook();
34 that.rename_notebook();
35 });
35 });
36 this.element.find('span#notebook_name').hover(function () {
36 this.element.find('span#notebook_name').hover(function () {
37 $(this).addClass("ui-state-hover");
37 $(this).addClass("ui-state-hover");
38 }, function () {
38 }, function () {
39 $(this).removeClass("ui-state-hover");
39 $(this).removeClass("ui-state-hover");
40 });
40 });
41 $([IPython.events]).on('notebook_loaded.Notebook', function () {
41 $([IPython.events]).on('notebook_loaded.Notebook', function () {
42 that.update_notebook_name();
42 that.update_notebook_name();
43 that.update_document_title();
43 that.update_document_title();
44 });
44 });
45 $([IPython.events]).on('notebook_saved.Notebook', function () {
45 $([IPython.events]).on('notebook_saved.Notebook', function () {
46 that.update_notebook_name();
46 that.update_notebook_name();
47 that.update_document_title();
47 that.update_document_title();
48 });
48 });
49 $([IPython.events]).on('notebook_renamed.Notebook', function () {
49 $([IPython.events]).on('notebook_renamed.Notebook', function () {
50 that.update_notebook_name();
50 that.update_notebook_name();
51 that.update_document_title();
51 that.update_document_title();
52 that.update_address_bar();
52 that.update_address_bar();
53 });
53 });
54 $([IPython.events]).on('notebook_save_failed.Notebook', function () {
54 $([IPython.events]).on('notebook_save_failed.Notebook', function () {
55 that.set_save_status('Autosave Failed!');
55 that.set_save_status('Autosave Failed!');
56 });
56 });
57 $([IPython.events]).on('checkpoints_listed.Notebook', function (event, data) {
57 $([IPython.events]).on('checkpoints_listed.Notebook', function (event, data) {
58 that.set_last_checkpoint(data[0]);
58 that.set_last_checkpoint(data[0]);
59 });
59 });
60
60
61 $([IPython.events]).on('checkpoint_created.Notebook', function (event, data) {
61 $([IPython.events]).on('checkpoint_created.Notebook', function (event, data) {
62 that.set_last_checkpoint(data);
62 that.set_last_checkpoint(data);
63 });
63 });
64 $([IPython.events]).on('set_dirty.Notebook', function (event, data) {
64 $([IPython.events]).on('set_dirty.Notebook', function (event, data) {
65 that.set_autosaved(data.value);
65 that.set_autosaved(data.value);
66 });
66 });
67 };
67 };
68
68
69
69
70 SaveWidget.prototype.rename_notebook = function () {
70 SaveWidget.prototype.rename_notebook = function () {
71 var that = this;
71 var that = this;
72 var dialog = $('<div/>').append(
72 var dialog = $('<div/>').append(
73 $("<p/>").addClass("rename-message")
73 $("<p/>").addClass("rename-message")
74 .text('Enter a new notebook name:')
74 .text('Enter a new notebook name:')
75 ).append(
75 ).append(
76 $("<br/>")
76 $("<br/>")
77 ).append(
77 ).append(
78 $('<input/>').attr('type','text').attr('size','25')
78 $('<input/>').attr('type','text').attr('size','25')
79 .val(IPython.notebook.get_notebook_name())
79 .val(IPython.notebook.get_notebook_name())
80 );
80 );
81 IPython.dialog.modal({
81 IPython.dialog.modal({
82 title: "Rename Notebook",
82 title: "Rename Notebook",
83 body: dialog,
83 body: dialog,
84 buttons : {
84 buttons : {
85 "Cancel": {},
85 "Cancel": {},
86 "OK": {
86 "OK": {
87 class: "btn-primary",
87 class: "btn-primary",
88 click: function () {
88 click: function () {
89 var new_name = $(this).find('input').val();
89 var new_name = $(this).find('input').val();
90 if (!IPython.notebook.test_notebook_name(new_name)) {
90 if (!IPython.notebook.test_notebook_name(new_name)) {
91 $(this).find('.rename-message').text(
91 $(this).find('.rename-message').text(
92 "Invalid notebook name. Notebook names must "+
92 "Invalid notebook name. Notebook names must "+
93 "have 1 or more characters and can contain any characters " +
93 "have 1 or more characters and can contain any characters " +
94 "except :/\\. Please enter a new notebook name:"
94 "except :/\\. Please enter a new notebook name:"
95 );
95 );
96 return false;
96 return false;
97 } else {
97 } else {
98 IPython.notebook.rename(new_name);
98 IPython.notebook.rename(new_name);
99 }
99 }
100 }}
100 }}
101 },
101 },
102 open : function (event, ui) {
102 open : function (event, ui) {
103 var that = $(this);
103 var that = $(this);
104 // Upon ENTER, click the OK button.
104 // Upon ENTER, click the OK button.
105 that.find('input[type="text"]').keydown(function (event, ui) {
105 that.find('input[type="text"]').keydown(function (event, ui) {
106 if (event.which === utils.keycodes.ENTER) {
106 if (event.which === utils.keycodes.ENTER) {
107 that.find('.btn-primary').first().click();
107 that.find('.btn-primary').first().click();
108 return false;
108 return false;
109 }
109 }
110 });
110 });
111 that.find('input[type="text"]').focus().select();
111 that.find('input[type="text"]').focus().select();
112 }
112 }
113 });
113 });
114 }
114 }
115
115
116
116
117 SaveWidget.prototype.update_notebook_name = function () {
117 SaveWidget.prototype.update_notebook_name = function () {
118 var nbname = IPython.notebook.get_notebook_name();
118 var nbname = IPython.notebook.get_notebook_name();
119 this.element.find('span#notebook_name').text(nbname);
119 this.element.find('span#notebook_name').text(nbname);
120 };
120 };
121
121
122
122
123 SaveWidget.prototype.update_document_title = function () {
123 SaveWidget.prototype.update_document_title = function () {
124 var nbname = IPython.notebook.get_notebook_name();
124 var nbname = IPython.notebook.get_notebook_name();
125 document.title = nbname;
125 document.title = nbname;
126 };
126 };
127
127
128 SaveWidget.prototype.update_address_bar = function(){
128 SaveWidget.prototype.update_address_bar = function(){
129 var nbname = IPython.notebook.notebook_name;
129 var nbname = IPython.notebook.notebook_name;
130 var path = IPython.notebook.notebookPath();
130 var path = IPython.notebook.notebook_path;
131 var state = {path : utils.url_join_encode(path, nbname)};
131 var state = {path : utils.url_join_encode(path, nbname)};
132 window.history.replaceState(state, "", utils.url_join_encode(
132 window.history.replaceState(state, "", utils.url_join_encode(
133 "/notebooks",
133 "/notebooks",
134 path,
134 path,
135 nbname)
135 nbname)
136 );
136 );
137 }
137 }
138
138
139
139
140 SaveWidget.prototype.set_save_status = function (msg) {
140 SaveWidget.prototype.set_save_status = function (msg) {
141 this.element.find('span#autosave_status').text(msg);
141 this.element.find('span#autosave_status').text(msg);
142 }
142 }
143
143
144 SaveWidget.prototype.set_checkpoint_status = function (msg) {
144 SaveWidget.prototype.set_checkpoint_status = function (msg) {
145 this.element.find('span#checkpoint_status').text(msg);
145 this.element.find('span#checkpoint_status').text(msg);
146 }
146 }
147
147
148 SaveWidget.prototype.set_last_checkpoint = function (checkpoint) {
148 SaveWidget.prototype.set_last_checkpoint = function (checkpoint) {
149 if (!checkpoint) {
149 if (!checkpoint) {
150 this.set_checkpoint_status("");
150 this.set_checkpoint_status("");
151 return;
151 return;
152 }
152 }
153 var d = new Date(checkpoint.last_modified);
153 var d = new Date(checkpoint.last_modified);
154 this.set_checkpoint_status(
154 this.set_checkpoint_status(
155 "Last Checkpoint: " + d.format('mmm dd HH:MM')
155 "Last Checkpoint: " + d.format('mmm dd HH:MM')
156 );
156 );
157 }
157 }
158
158
159 SaveWidget.prototype.set_autosaved = function (dirty) {
159 SaveWidget.prototype.set_autosaved = function (dirty) {
160 if (dirty) {
160 if (dirty) {
161 this.set_save_status("(unsaved changes)");
161 this.set_save_status("(unsaved changes)");
162 } else {
162 } else {
163 this.set_save_status("(autosaved)");
163 this.set_save_status("(autosaved)");
164 }
164 }
165 };
165 };
166
166
167
167
168 IPython.SaveWidget = SaveWidget;
168 IPython.SaveWidget = SaveWidget;
169
169
170 return IPython;
170 return IPython;
171
171
172 }(IPython));
172 }(IPython));
173
173
@@ -1,603 +1,609 b''
1 //----------------------------------------------------------------------------
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2008-2011 The IPython Development Team
2 // Copyright (C) 2008-2011 The IPython Development Team
3 //
3 //
4 // Distributed under the terms of the BSD License. The full license is in
4 // Distributed under the terms of the BSD License. The full license is in
5 // the file COPYING, distributed as part of this software.
5 // the file COPYING, distributed as part of this software.
6 //----------------------------------------------------------------------------
6 //----------------------------------------------------------------------------
7
7
8 //============================================================================
8 //============================================================================
9 // Kernel
9 // Kernel
10 //============================================================================
10 //============================================================================
11
11
12 /**
12 /**
13 * @module IPython
13 * @module IPython
14 * @namespace IPython
14 * @namespace IPython
15 * @submodule Kernel
15 * @submodule Kernel
16 */
16 */
17
17
18 var IPython = (function (IPython) {
18 var IPython = (function (IPython) {
19 "use strict";
19 "use strict";
20
20
21 var utils = IPython.utils;
21 var utils = IPython.utils;
22
22
23 // Initialization and connection.
23 // Initialization and connection.
24 /**
24 /**
25 * A Kernel Class to communicate with the Python kernel
25 * A Kernel Class to communicate with the Python kernel
26 * @Class Kernel
26 * @Class Kernel
27 */
27 */
28 var Kernel = function (base_url) {
28 var Kernel = function (kernel_service_url) {
29 this.kernel_id = null;
29 this.kernel_id = null;
30 this.shell_channel = null;
30 this.shell_channel = null;
31 this.iopub_channel = null;
31 this.iopub_channel = null;
32 this.stdin_channel = null;
32 this.stdin_channel = null;
33 this.base_url = base_url;
33 this.kernel_service_url = kernel_service_url;
34 this.running = false;
34 this.running = false;
35 this.username = "username";
35 this.username = "username";
36 this.session_id = utils.uuid();
36 this.session_id = utils.uuid();
37 this._msg_callbacks = {};
37 this._msg_callbacks = {};
38
38
39 if (typeof(WebSocket) !== 'undefined') {
39 if (typeof(WebSocket) !== 'undefined') {
40 this.WebSocket = WebSocket;
40 this.WebSocket = WebSocket;
41 } else if (typeof(MozWebSocket) !== 'undefined') {
41 } else if (typeof(MozWebSocket) !== 'undefined') {
42 this.WebSocket = MozWebSocket;
42 this.WebSocket = MozWebSocket;
43 } else {
43 } else {
44 alert('Your browser does not have WebSocket support, please try Chrome, Safari or Firefox β‰₯ 6. Firefox 4 and 5 are also supported by you have to enable WebSockets in about:config.');
44 alert('Your browser does not have WebSocket support, please try Chrome, Safari or Firefox β‰₯ 6. Firefox 4 and 5 are also supported by you have to enable WebSockets in about:config.');
45 }
45 }
46
46
47 this.bind_events();
47 this.bind_events();
48 this.init_iopub_handlers();
48 this.init_iopub_handlers();
49 this.comm_manager = new IPython.CommManager(this);
49 this.comm_manager = new IPython.CommManager(this);
50 this.widget_manager = new IPython.WidgetManager(this.comm_manager);
50 this.widget_manager = new IPython.WidgetManager(this.comm_manager);
51 };
51 };
52
52
53
53
54 Kernel.prototype._get_msg = function (msg_type, content, metadata) {
54 Kernel.prototype._get_msg = function (msg_type, content, metadata) {
55 var msg = {
55 var msg = {
56 header : {
56 header : {
57 msg_id : utils.uuid(),
57 msg_id : utils.uuid(),
58 username : this.username,
58 username : this.username,
59 session : this.session_id,
59 session : this.session_id,
60 msg_type : msg_type
60 msg_type : msg_type
61 },
61 },
62 metadata : metadata || {},
62 metadata : metadata || {},
63 content : content,
63 content : content,
64 parent_header : {}
64 parent_header : {}
65 };
65 };
66 return msg;
66 return msg;
67 };
67 };
68
68
69 Kernel.prototype.bind_events = function () {
69 Kernel.prototype.bind_events = function () {
70 var that = this;
70 var that = this;
71 $([IPython.events]).on('send_input_reply.Kernel', function(evt, data) {
71 $([IPython.events]).on('send_input_reply.Kernel', function(evt, data) {
72 that.send_input_reply(data);
72 that.send_input_reply(data);
73 });
73 });
74 };
74 };
75
75
76 // Initialize the iopub handlers
76 // Initialize the iopub handlers
77
77
78 Kernel.prototype.init_iopub_handlers = function () {
78 Kernel.prototype.init_iopub_handlers = function () {
79 var output_types = ['stream', 'display_data', 'pyout', 'pyerr'];
79 var output_types = ['stream', 'display_data', 'pyout', 'pyerr'];
80 this._iopub_handlers = {};
80 this._iopub_handlers = {};
81 this.register_iopub_handler('status', $.proxy(this._handle_status_message, this));
81 this.register_iopub_handler('status', $.proxy(this._handle_status_message, this));
82 this.register_iopub_handler('clear_output', $.proxy(this._handle_clear_output, this));
82 this.register_iopub_handler('clear_output', $.proxy(this._handle_clear_output, this));
83
83
84 for (var i=0; i < output_types.length; i++) {
84 for (var i=0; i < output_types.length; i++) {
85 this.register_iopub_handler(output_types[i], $.proxy(this._handle_output_message, this));
85 this.register_iopub_handler(output_types[i], $.proxy(this._handle_output_message, this));
86 }
86 }
87 };
87 };
88
88
89 /**
89 /**
90 * Start the Python kernel
90 * Start the Python kernel
91 * @method start
91 * @method start
92 */
92 */
93 Kernel.prototype.start = function (params) {
93 Kernel.prototype.start = function (params) {
94 params = params || {};
94 params = params || {};
95 if (!this.running) {
95 if (!this.running) {
96 var qs = $.param(params);
96 var qs = $.param(params);
97 var url = this.base_url + '?' + qs;
97 $.post(utils.url_join_encode(this.kernel_service_url) + '?' + qs,
98 $.post(url,
99 $.proxy(this._kernel_started, this),
98 $.proxy(this._kernel_started, this),
100 'json'
99 'json'
101 );
100 );
102 }
101 }
103 };
102 };
104
103
105 /**
104 /**
106 * Restart the python kernel.
105 * Restart the python kernel.
107 *
106 *
108 * Emit a 'status_restarting.Kernel' event with
107 * Emit a 'status_restarting.Kernel' event with
109 * the current object as parameter
108 * the current object as parameter
110 *
109 *
111 * @method restart
110 * @method restart
112 */
111 */
113 Kernel.prototype.restart = function () {
112 Kernel.prototype.restart = function () {
114 $([IPython.events]).trigger('status_restarting.Kernel', {kernel: this});
113 $([IPython.events]).trigger('status_restarting.Kernel', {kernel: this});
115 if (this.running) {
114 if (this.running) {
116 this.stop_channels();
115 this.stop_channels();
117 var url = utils.url_join_encode(this.kernel_url, "restart");
116 $.post(utils.url_join_encode(this.kernel_url, "restart"),
118 $.post(url,
119 $.proxy(this._kernel_started, this),
117 $.proxy(this._kernel_started, this),
120 'json'
118 'json'
121 );
119 );
122 }
120 }
123 };
121 };
124
122
125
123
126 Kernel.prototype._kernel_started = function (json) {
124 Kernel.prototype._kernel_started = function (json) {
127 console.log("Kernel started: ", json.id);
125 console.log("Kernel started: ", json.id);
128 this.running = true;
126 this.running = true;
129 this.kernel_id = json.id;
127 this.kernel_id = json.id;
130 var ws_url = json.ws_url;
128 var ws_url = json.ws_url;
131 if (ws_url.match(/wss?:\/\//) === null) {
129 if (ws_url.match(/wss?:\/\//) === null) {
132 // trailing 's' in https will become wss for secure web sockets
130 // trailing 's' in https will become wss for secure web sockets
133 var prot = location.protocol.replace('http', 'ws') + "//";
131 var prot = location.protocol.replace('http', 'ws') + "//";
134 ws_url = prot + location.host + ws_url;
132 ws_url = prot + location.host + ws_url;
135 }
133 }
136 this.ws_url = ws_url;
134 var parsed = utils.parse_url(ws_url);
137 this.kernel_url = utils.url_join_encode(this.base_url, this.kernel_id);
135 this.ws_host = parsed.protocol + "//" + parsed.host;
136 this.kernel_url = utils.url_path_join(this.kernel_service_url, this.kernel_id);
137 this.ws_url = utils.url_path_join(parsed.pathname, this.kernel_url);
138 this.start_channels();
138 this.start_channels();
139 };
139 };
140
140
141
141
142 Kernel.prototype._websocket_closed = function(ws_url, early) {
142 Kernel.prototype._websocket_closed = function(ws_url, early) {
143 this.stop_channels();
143 this.stop_channels();
144 $([IPython.events]).trigger('websocket_closed.Kernel',
144 $([IPython.events]).trigger('websocket_closed.Kernel',
145 {ws_url: ws_url, kernel: this, early: early}
145 {ws_url: ws_url, kernel: this, early: early}
146 );
146 );
147 };
147 };
148
148
149 /**
149 /**
150 * Start the `shell`and `iopub` channels.
150 * Start the `shell`and `iopub` channels.
151 * Will stop and restart them if they already exist.
151 * Will stop and restart them if they already exist.
152 *
152 *
153 * @method start_channels
153 * @method start_channels
154 */
154 */
155 Kernel.prototype.start_channels = function () {
155 Kernel.prototype.start_channels = function () {
156 var that = this;
156 var that = this;
157 this.stop_channels();
157 this.stop_channels();
158 var ws_url = this.ws_url + this.kernel_url;
158 console.log("Starting WebSockets:", this.ws_host + this.ws_url);
159 console.log("Starting WebSockets:", ws_url);
159 this.shell_channel = new this.WebSocket(
160 this.shell_channel = new this.WebSocket(ws_url + "/shell");
160 this.ws_host + utils.url_join_encode(this.ws_url, "shell")
161 this.stdin_channel = new this.WebSocket(ws_url + "/stdin");
161 );
162 this.iopub_channel = new this.WebSocket(ws_url + "/iopub");
162 this.stdin_channel = new this.WebSocket(
163 this.ws_host + utils.url_join_encode(this.ws_url, "stdin")
164 );
165 this.iopub_channel = new this.WebSocket(
166 this.ws_host + utils.url_join_encode(this.ws_url, "iopub")
167 );
163
168
169 var ws_host_url = this.ws_host + this.ws_url;
164 var already_called_onclose = false; // only alert once
170 var already_called_onclose = false; // only alert once
165 var ws_closed_early = function(evt){
171 var ws_closed_early = function(evt){
166 if (already_called_onclose){
172 if (already_called_onclose){
167 return;
173 return;
168 }
174 }
169 already_called_onclose = true;
175 already_called_onclose = true;
170 if ( ! evt.wasClean ){
176 if ( ! evt.wasClean ){
171 that._websocket_closed(ws_url, true);
177 that._websocket_closed(ws_host_url, true);
172 }
178 }
173 };
179 };
174 var ws_closed_late = function(evt){
180 var ws_closed_late = function(evt){
175 if (already_called_onclose){
181 if (already_called_onclose){
176 return;
182 return;
177 }
183 }
178 already_called_onclose = true;
184 already_called_onclose = true;
179 if ( ! evt.wasClean ){
185 if ( ! evt.wasClean ){
180 that._websocket_closed(ws_url, false);
186 that._websocket_closed(ws_host_url, false);
181 }
187 }
182 };
188 };
183 var channels = [this.shell_channel, this.iopub_channel, this.stdin_channel];
189 var channels = [this.shell_channel, this.iopub_channel, this.stdin_channel];
184 for (var i=0; i < channels.length; i++) {
190 for (var i=0; i < channels.length; i++) {
185 channels[i].onopen = $.proxy(this._ws_opened, this);
191 channels[i].onopen = $.proxy(this._ws_opened, this);
186 channels[i].onclose = ws_closed_early;
192 channels[i].onclose = ws_closed_early;
187 }
193 }
188 // switch from early-close to late-close message after 1s
194 // switch from early-close to late-close message after 1s
189 setTimeout(function() {
195 setTimeout(function() {
190 for (var i=0; i < channels.length; i++) {
196 for (var i=0; i < channels.length; i++) {
191 if (channels[i] !== null) {
197 if (channels[i] !== null) {
192 channels[i].onclose = ws_closed_late;
198 channels[i].onclose = ws_closed_late;
193 }
199 }
194 }
200 }
195 }, 1000);
201 }, 1000);
196 this.shell_channel.onmessage = $.proxy(this._handle_shell_reply, this);
202 this.shell_channel.onmessage = $.proxy(this._handle_shell_reply, this);
197 this.iopub_channel.onmessage = $.proxy(this._handle_iopub_message, this);
203 this.iopub_channel.onmessage = $.proxy(this._handle_iopub_message, this);
198 this.stdin_channel.onmessage = $.proxy(this._handle_input_request, this);
204 this.stdin_channel.onmessage = $.proxy(this._handle_input_request, this);
199 };
205 };
200
206
201 /**
207 /**
202 * Handle a websocket entering the open state
208 * Handle a websocket entering the open state
203 * sends session and cookie authentication info as first message.
209 * sends session and cookie authentication info as first message.
204 * Once all sockets are open, signal the Kernel.status_started event.
210 * Once all sockets are open, signal the Kernel.status_started event.
205 * @method _ws_opened
211 * @method _ws_opened
206 */
212 */
207 Kernel.prototype._ws_opened = function (evt) {
213 Kernel.prototype._ws_opened = function (evt) {
208 // send the session id so the Session object Python-side
214 // send the session id so the Session object Python-side
209 // has the same identity
215 // has the same identity
210 evt.target.send(this.session_id + ':' + document.cookie);
216 evt.target.send(this.session_id + ':' + document.cookie);
211
217
212 var channels = [this.shell_channel, this.iopub_channel, this.stdin_channel];
218 var channels = [this.shell_channel, this.iopub_channel, this.stdin_channel];
213 for (var i=0; i < channels.length; i++) {
219 for (var i=0; i < channels.length; i++) {
214 // if any channel is not ready, don't trigger event.
220 // if any channel is not ready, don't trigger event.
215 if ( !channels[i].readyState ) return;
221 if ( !channels[i].readyState ) return;
216 }
222 }
217 // all events ready, trigger started event.
223 // all events ready, trigger started event.
218 $([IPython.events]).trigger('status_started.Kernel', {kernel: this});
224 $([IPython.events]).trigger('status_started.Kernel', {kernel: this});
219 };
225 };
220
226
221 /**
227 /**
222 * Stop the websocket channels.
228 * Stop the websocket channels.
223 * @method stop_channels
229 * @method stop_channels
224 */
230 */
225 Kernel.prototype.stop_channels = function () {
231 Kernel.prototype.stop_channels = function () {
226 var channels = [this.shell_channel, this.iopub_channel, this.stdin_channel];
232 var channels = [this.shell_channel, this.iopub_channel, this.stdin_channel];
227 for (var i=0; i < channels.length; i++) {
233 for (var i=0; i < channels.length; i++) {
228 if ( channels[i] !== null ) {
234 if ( channels[i] !== null ) {
229 channels[i].onclose = null;
235 channels[i].onclose = null;
230 channels[i].close();
236 channels[i].close();
231 }
237 }
232 }
238 }
233 this.shell_channel = this.iopub_channel = this.stdin_channel = null;
239 this.shell_channel = this.iopub_channel = this.stdin_channel = null;
234 };
240 };
235
241
236 // Main public methods.
242 // Main public methods.
237
243
238 // send a message on the Kernel's shell channel
244 // send a message on the Kernel's shell channel
239 Kernel.prototype.send_shell_message = function (msg_type, content, callbacks, metadata) {
245 Kernel.prototype.send_shell_message = function (msg_type, content, callbacks, metadata) {
240 var msg = this._get_msg(msg_type, content, metadata);
246 var msg = this._get_msg(msg_type, content, metadata);
241 this.shell_channel.send(JSON.stringify(msg));
247 this.shell_channel.send(JSON.stringify(msg));
242 this.set_callbacks_for_msg(msg.header.msg_id, callbacks);
248 this.set_callbacks_for_msg(msg.header.msg_id, callbacks);
243 return msg.header.msg_id;
249 return msg.header.msg_id;
244 };
250 };
245
251
246 /**
252 /**
247 * Get kernel info
253 * Get kernel info
248 *
254 *
249 * @param callback {function}
255 * @param callback {function}
250 * @method object_info
256 * @method object_info
251 *
257 *
252 * When calling this method, pass a callback function that expects one argument.
258 * When calling this method, pass a callback function that expects one argument.
253 * The callback will be passed the complete `kernel_info_reply` message documented
259 * The callback will be passed the complete `kernel_info_reply` message documented
254 * [here](http://ipython.org/ipython-doc/dev/development/messaging.html#kernel-info)
260 * [here](http://ipython.org/ipython-doc/dev/development/messaging.html#kernel-info)
255 */
261 */
256 Kernel.prototype.kernel_info = function (callback) {
262 Kernel.prototype.kernel_info = function (callback) {
257 var callbacks;
263 var callbacks;
258 if (callback) {
264 if (callback) {
259 callbacks = { shell : { reply : callback } };
265 callbacks = { shell : { reply : callback } };
260 }
266 }
261 return this.send_shell_message("kernel_info_request", {}, callbacks);
267 return this.send_shell_message("kernel_info_request", {}, callbacks);
262 };
268 };
263
269
264 /**
270 /**
265 * Get info on an object
271 * Get info on an object
266 *
272 *
267 * @param objname {string}
273 * @param objname {string}
268 * @param callback {function}
274 * @param callback {function}
269 * @method object_info
275 * @method object_info
270 *
276 *
271 * When calling this method, pass a callback function that expects one argument.
277 * When calling this method, pass a callback function that expects one argument.
272 * The callback will be passed the complete `object_info_reply` message documented
278 * The callback will be passed the complete `object_info_reply` message documented
273 * [here](http://ipython.org/ipython-doc/dev/development/messaging.html#object-information)
279 * [here](http://ipython.org/ipython-doc/dev/development/messaging.html#object-information)
274 */
280 */
275 Kernel.prototype.object_info = function (objname, callback) {
281 Kernel.prototype.object_info = function (objname, callback) {
276 var callbacks;
282 var callbacks;
277 if (callback) {
283 if (callback) {
278 callbacks = { shell : { reply : callback } };
284 callbacks = { shell : { reply : callback } };
279 }
285 }
280
286
281 if (typeof(objname) !== null && objname !== null) {
287 if (typeof(objname) !== null && objname !== null) {
282 var content = {
288 var content = {
283 oname : objname.toString(),
289 oname : objname.toString(),
284 detail_level : 0,
290 detail_level : 0,
285 };
291 };
286 return this.send_shell_message("object_info_request", content, callbacks);
292 return this.send_shell_message("object_info_request", content, callbacks);
287 }
293 }
288 return;
294 return;
289 };
295 };
290
296
291 /**
297 /**
292 * Execute given code into kernel, and pass result to callback.
298 * Execute given code into kernel, and pass result to callback.
293 *
299 *
294 * @async
300 * @async
295 * @method execute
301 * @method execute
296 * @param {string} code
302 * @param {string} code
297 * @param [callbacks] {Object} With the following keys (all optional)
303 * @param [callbacks] {Object} With the following keys (all optional)
298 * @param callbacks.shell.reply {function}
304 * @param callbacks.shell.reply {function}
299 * @param callbacks.shell.payload.[payload_name] {function}
305 * @param callbacks.shell.payload.[payload_name] {function}
300 * @param callbacks.iopub.output {function}
306 * @param callbacks.iopub.output {function}
301 * @param callbacks.iopub.clear_output {function}
307 * @param callbacks.iopub.clear_output {function}
302 * @param callbacks.input {function}
308 * @param callbacks.input {function}
303 * @param {object} [options]
309 * @param {object} [options]
304 * @param [options.silent=false] {Boolean}
310 * @param [options.silent=false] {Boolean}
305 * @param [options.user_expressions=empty_dict] {Dict}
311 * @param [options.user_expressions=empty_dict] {Dict}
306 * @param [options.user_variables=empty_list] {List od Strings}
312 * @param [options.user_variables=empty_list] {List od Strings}
307 * @param [options.allow_stdin=false] {Boolean} true|false
313 * @param [options.allow_stdin=false] {Boolean} true|false
308 *
314 *
309 * @example
315 * @example
310 *
316 *
311 * The options object should contain the options for the execute call. Its default
317 * The options object should contain the options for the execute call. Its default
312 * values are:
318 * values are:
313 *
319 *
314 * options = {
320 * options = {
315 * silent : true,
321 * silent : true,
316 * user_variables : [],
322 * user_variables : [],
317 * user_expressions : {},
323 * user_expressions : {},
318 * allow_stdin : false
324 * allow_stdin : false
319 * }
325 * }
320 *
326 *
321 * When calling this method pass a callbacks structure of the form:
327 * When calling this method pass a callbacks structure of the form:
322 *
328 *
323 * callbacks = {
329 * callbacks = {
324 * shell : {
330 * shell : {
325 * reply : execute_reply_callback,
331 * reply : execute_reply_callback,
326 * payload : {
332 * payload : {
327 * set_next_input : set_next_input_callback,
333 * set_next_input : set_next_input_callback,
328 * }
334 * }
329 * },
335 * },
330 * iopub : {
336 * iopub : {
331 * output : output_callback,
337 * output : output_callback,
332 * clear_output : clear_output_callback,
338 * clear_output : clear_output_callback,
333 * },
339 * },
334 * input : raw_input_callback
340 * input : raw_input_callback
335 * }
341 * }
336 *
342 *
337 * Each callback will be passed the entire message as a single arugment.
343 * Each callback will be passed the entire message as a single arugment.
338 * Payload handlers will be passed the corresponding payload and the execute_reply message.
344 * Payload handlers will be passed the corresponding payload and the execute_reply message.
339 */
345 */
340 Kernel.prototype.execute = function (code, callbacks, options) {
346 Kernel.prototype.execute = function (code, callbacks, options) {
341
347
342 var content = {
348 var content = {
343 code : code,
349 code : code,
344 silent : true,
350 silent : true,
345 store_history : false,
351 store_history : false,
346 user_variables : [],
352 user_variables : [],
347 user_expressions : {},
353 user_expressions : {},
348 allow_stdin : false
354 allow_stdin : false
349 };
355 };
350 callbacks = callbacks || {};
356 callbacks = callbacks || {};
351 if (callbacks.input !== undefined) {
357 if (callbacks.input !== undefined) {
352 content.allow_stdin = true;
358 content.allow_stdin = true;
353 }
359 }
354 $.extend(true, content, options);
360 $.extend(true, content, options);
355 $([IPython.events]).trigger('execution_request.Kernel', {kernel: this, content:content});
361 $([IPython.events]).trigger('execution_request.Kernel', {kernel: this, content:content});
356 return this.send_shell_message("execute_request", content, callbacks);
362 return this.send_shell_message("execute_request", content, callbacks);
357 };
363 };
358
364
359 /**
365 /**
360 * When calling this method, pass a function to be called with the `complete_reply` message
366 * When calling this method, pass a function to be called with the `complete_reply` message
361 * as its only argument when it arrives.
367 * as its only argument when it arrives.
362 *
368 *
363 * `complete_reply` is documented
369 * `complete_reply` is documented
364 * [here](http://ipython.org/ipython-doc/dev/development/messaging.html#complete)
370 * [here](http://ipython.org/ipython-doc/dev/development/messaging.html#complete)
365 *
371 *
366 * @method complete
372 * @method complete
367 * @param line {integer}
373 * @param line {integer}
368 * @param cursor_pos {integer}
374 * @param cursor_pos {integer}
369 * @param callback {function}
375 * @param callback {function}
370 *
376 *
371 */
377 */
372 Kernel.prototype.complete = function (line, cursor_pos, callback) {
378 Kernel.prototype.complete = function (line, cursor_pos, callback) {
373 var callbacks;
379 var callbacks;
374 if (callback) {
380 if (callback) {
375 callbacks = { shell : { reply : callback } };
381 callbacks = { shell : { reply : callback } };
376 }
382 }
377 var content = {
383 var content = {
378 text : '',
384 text : '',
379 line : line,
385 line : line,
380 block : null,
386 block : null,
381 cursor_pos : cursor_pos
387 cursor_pos : cursor_pos
382 };
388 };
383 return this.send_shell_message("complete_request", content, callbacks);
389 return this.send_shell_message("complete_request", content, callbacks);
384 };
390 };
385
391
386
392
387 Kernel.prototype.interrupt = function () {
393 Kernel.prototype.interrupt = function () {
388 if (this.running) {
394 if (this.running) {
389 $([IPython.events]).trigger('status_interrupting.Kernel', {kernel: this});
395 $([IPython.events]).trigger('status_interrupting.Kernel', {kernel: this});
390 $.post(this.kernel_url + "/interrupt");
396 $.post(utils.url_join_encode(this.kernel_url, "interrupt"));
391 }
397 }
392 };
398 };
393
399
394
400
395 Kernel.prototype.kill = function () {
401 Kernel.prototype.kill = function () {
396 if (this.running) {
402 if (this.running) {
397 this.running = false;
403 this.running = false;
398 var settings = {
404 var settings = {
399 cache : false,
405 cache : false,
400 type : "DELETE"
406 type : "DELETE"
401 };
407 };
402 $.ajax(this.kernel_url, settings);
408 $.ajax(utils.url_join_encode(this.kernel_url), settings);
403 }
409 }
404 };
410 };
405
411
406 Kernel.prototype.send_input_reply = function (input) {
412 Kernel.prototype.send_input_reply = function (input) {
407 var content = {
413 var content = {
408 value : input,
414 value : input,
409 };
415 };
410 $([IPython.events]).trigger('input_reply.Kernel', {kernel: this, content:content});
416 $([IPython.events]).trigger('input_reply.Kernel', {kernel: this, content:content});
411 var msg = this._get_msg("input_reply", content);
417 var msg = this._get_msg("input_reply", content);
412 this.stdin_channel.send(JSON.stringify(msg));
418 this.stdin_channel.send(JSON.stringify(msg));
413 return msg.header.msg_id;
419 return msg.header.msg_id;
414 };
420 };
415
421
416
422
417 // Reply handlers
423 // Reply handlers
418
424
419 Kernel.prototype.register_iopub_handler = function (msg_type, callback) {
425 Kernel.prototype.register_iopub_handler = function (msg_type, callback) {
420 this._iopub_handlers[msg_type] = callback;
426 this._iopub_handlers[msg_type] = callback;
421 };
427 };
422
428
423 Kernel.prototype.get_iopub_handler = function (msg_type) {
429 Kernel.prototype.get_iopub_handler = function (msg_type) {
424 // get iopub handler for a specific message type
430 // get iopub handler for a specific message type
425 return this._iopub_handlers[msg_type];
431 return this._iopub_handlers[msg_type];
426 };
432 };
427
433
428
434
429 Kernel.prototype.get_callbacks_for_msg = function (msg_id) {
435 Kernel.prototype.get_callbacks_for_msg = function (msg_id) {
430 // get callbacks for a specific message
436 // get callbacks for a specific message
431 return this._msg_callbacks[msg_id];
437 return this._msg_callbacks[msg_id];
432 };
438 };
433
439
434
440
435 Kernel.prototype.clear_callbacks_for_msg = function (msg_id) {
441 Kernel.prototype.clear_callbacks_for_msg = function (msg_id) {
436 if (this._msg_callbacks[msg_id] !== undefined ) {
442 if (this._msg_callbacks[msg_id] !== undefined ) {
437 delete this._msg_callbacks[msg_id];
443 delete this._msg_callbacks[msg_id];
438 }
444 }
439 };
445 };
440
446
441 /* Set callbacks for a particular message.
447 /* Set callbacks for a particular message.
442 * Callbacks should be a struct of the following form:
448 * Callbacks should be a struct of the following form:
443 * shell : {
449 * shell : {
444 *
450 *
445 * }
451 * }
446
452
447 */
453 */
448 Kernel.prototype.set_callbacks_for_msg = function (msg_id, callbacks) {
454 Kernel.prototype.set_callbacks_for_msg = function (msg_id, callbacks) {
449 if (callbacks) {
455 if (callbacks) {
450 // shallow-copy mapping, because we will modify it at the top level
456 // shallow-copy mapping, because we will modify it at the top level
451 var cbcopy = this._msg_callbacks[msg_id] = {};
457 var cbcopy = this._msg_callbacks[msg_id] = {};
452 cbcopy.shell = callbacks.shell;
458 cbcopy.shell = callbacks.shell;
453 cbcopy.iopub = callbacks.iopub;
459 cbcopy.iopub = callbacks.iopub;
454 cbcopy.input = callbacks.input;
460 cbcopy.input = callbacks.input;
455 this._msg_callbacks[msg_id] = cbcopy;
461 this._msg_callbacks[msg_id] = cbcopy;
456 }
462 }
457 };
463 };
458
464
459
465
460 Kernel.prototype._handle_shell_reply = function (e) {
466 Kernel.prototype._handle_shell_reply = function (e) {
461 var reply = $.parseJSON(e.data);
467 var reply = $.parseJSON(e.data);
462 $([IPython.events]).trigger('shell_reply.Kernel', {kernel: this, reply:reply});
468 $([IPython.events]).trigger('shell_reply.Kernel', {kernel: this, reply:reply});
463 var content = reply.content;
469 var content = reply.content;
464 var metadata = reply.metadata;
470 var metadata = reply.metadata;
465 var parent_id = reply.parent_header.msg_id;
471 var parent_id = reply.parent_header.msg_id;
466 var callbacks = this.get_callbacks_for_msg(parent_id);
472 var callbacks = this.get_callbacks_for_msg(parent_id);
467 if (!callbacks || !callbacks.shell) {
473 if (!callbacks || !callbacks.shell) {
468 return;
474 return;
469 }
475 }
470 var shell_callbacks = callbacks.shell;
476 var shell_callbacks = callbacks.shell;
471
477
472 // clear callbacks on shell
478 // clear callbacks on shell
473 delete callbacks.shell;
479 delete callbacks.shell;
474 delete callbacks.input;
480 delete callbacks.input;
475 if (!callbacks.iopub) {
481 if (!callbacks.iopub) {
476 this.clear_callbacks_for_msg(parent_id);
482 this.clear_callbacks_for_msg(parent_id);
477 }
483 }
478
484
479 if (shell_callbacks.reply !== undefined) {
485 if (shell_callbacks.reply !== undefined) {
480 shell_callbacks.reply(reply);
486 shell_callbacks.reply(reply);
481 }
487 }
482 if (content.payload && shell_callbacks.payload) {
488 if (content.payload && shell_callbacks.payload) {
483 this._handle_payloads(content.payload, shell_callbacks.payload, reply);
489 this._handle_payloads(content.payload, shell_callbacks.payload, reply);
484 }
490 }
485 };
491 };
486
492
487
493
488 Kernel.prototype._handle_payloads = function (payloads, payload_callbacks, msg) {
494 Kernel.prototype._handle_payloads = function (payloads, payload_callbacks, msg) {
489 var l = payloads.length;
495 var l = payloads.length;
490 // Payloads are handled by triggering events because we don't want the Kernel
496 // Payloads are handled by triggering events because we don't want the Kernel
491 // to depend on the Notebook or Pager classes.
497 // to depend on the Notebook or Pager classes.
492 for (var i=0; i<l; i++) {
498 for (var i=0; i<l; i++) {
493 var payload = payloads[i];
499 var payload = payloads[i];
494 var callback = payload_callbacks[payload.source];
500 var callback = payload_callbacks[payload.source];
495 if (callback) {
501 if (callback) {
496 callback(payload, msg);
502 callback(payload, msg);
497 }
503 }
498 }
504 }
499 };
505 };
500
506
501 Kernel.prototype._handle_status_message = function (msg) {
507 Kernel.prototype._handle_status_message = function (msg) {
502 var execution_state = msg.content.execution_state;
508 var execution_state = msg.content.execution_state;
503 var parent_id = msg.parent_header.msg_id;
509 var parent_id = msg.parent_header.msg_id;
504
510
505 // dispatch status msg callbacks, if any
511 // dispatch status msg callbacks, if any
506 var callbacks = this.get_callbacks_for_msg(parent_id);
512 var callbacks = this.get_callbacks_for_msg(parent_id);
507 if (callbacks && callbacks.iopub && callbacks.iopub.status) {
513 if (callbacks && callbacks.iopub && callbacks.iopub.status) {
508 try {
514 try {
509 callbacks.iopub.status(msg);
515 callbacks.iopub.status(msg);
510 } catch (e) {
516 } catch (e) {
511 console.log("Exception in status msg handler", e, e.stack);
517 console.log("Exception in status msg handler", e, e.stack);
512 }
518 }
513 }
519 }
514
520
515 if (execution_state === 'busy') {
521 if (execution_state === 'busy') {
516 $([IPython.events]).trigger('status_busy.Kernel', {kernel: this});
522 $([IPython.events]).trigger('status_busy.Kernel', {kernel: this});
517 } else if (execution_state === 'idle') {
523 } else if (execution_state === 'idle') {
518 // clear callbacks on idle, there can be no more
524 // clear callbacks on idle, there can be no more
519 if (callbacks !== undefined) {
525 if (callbacks !== undefined) {
520 delete callbacks.iopub;
526 delete callbacks.iopub;
521 delete callbacks.input;
527 delete callbacks.input;
522 if (!callbacks.shell) {
528 if (!callbacks.shell) {
523 this.clear_callbacks_for_msg(parent_id);
529 this.clear_callbacks_for_msg(parent_id);
524 }
530 }
525 }
531 }
526 // trigger status_idle event
532 // trigger status_idle event
527 $([IPython.events]).trigger('status_idle.Kernel', {kernel: this});
533 $([IPython.events]).trigger('status_idle.Kernel', {kernel: this});
528 } else if (execution_state === 'restarting') {
534 } else if (execution_state === 'restarting') {
529 // autorestarting is distinct from restarting,
535 // autorestarting is distinct from restarting,
530 // in that it means the kernel died and the server is restarting it.
536 // in that it means the kernel died and the server is restarting it.
531 // status_restarting sets the notification widget,
537 // status_restarting sets the notification widget,
532 // autorestart shows the more prominent dialog.
538 // autorestart shows the more prominent dialog.
533 $([IPython.events]).trigger('status_autorestarting.Kernel', {kernel: this});
539 $([IPython.events]).trigger('status_autorestarting.Kernel', {kernel: this});
534 $([IPython.events]).trigger('status_restarting.Kernel', {kernel: this});
540 $([IPython.events]).trigger('status_restarting.Kernel', {kernel: this});
535 } else if (execution_state === 'dead') {
541 } else if (execution_state === 'dead') {
536 this.stop_channels();
542 this.stop_channels();
537 $([IPython.events]).trigger('status_dead.Kernel', {kernel: this});
543 $([IPython.events]).trigger('status_dead.Kernel', {kernel: this});
538 }
544 }
539 };
545 };
540
546
541
547
542 // handle clear_output message
548 // handle clear_output message
543 Kernel.prototype._handle_clear_output = function (msg) {
549 Kernel.prototype._handle_clear_output = function (msg) {
544 var callbacks = this.get_callbacks_for_msg(msg.parent_header.msg_id);
550 var callbacks = this.get_callbacks_for_msg(msg.parent_header.msg_id);
545 if (!callbacks || !callbacks.iopub) {
551 if (!callbacks || !callbacks.iopub) {
546 return;
552 return;
547 }
553 }
548 var callback = callbacks.iopub.clear_output;
554 var callback = callbacks.iopub.clear_output;
549 if (callback) {
555 if (callback) {
550 callback(msg);
556 callback(msg);
551 }
557 }
552 };
558 };
553
559
554
560
555 // handle an output message (pyout, display_data, etc.)
561 // handle an output message (pyout, display_data, etc.)
556 Kernel.prototype._handle_output_message = function (msg) {
562 Kernel.prototype._handle_output_message = function (msg) {
557 var callbacks = this.get_callbacks_for_msg(msg.parent_header.msg_id);
563 var callbacks = this.get_callbacks_for_msg(msg.parent_header.msg_id);
558 if (!callbacks || !callbacks.iopub) {
564 if (!callbacks || !callbacks.iopub) {
559 return;
565 return;
560 }
566 }
561 var callback = callbacks.iopub.output;
567 var callback = callbacks.iopub.output;
562 if (callback) {
568 if (callback) {
563 callback(msg);
569 callback(msg);
564 }
570 }
565 };
571 };
566
572
567 // dispatch IOPub messages to respective handlers.
573 // dispatch IOPub messages to respective handlers.
568 // each message type should have a handler.
574 // each message type should have a handler.
569 Kernel.prototype._handle_iopub_message = function (e) {
575 Kernel.prototype._handle_iopub_message = function (e) {
570 var msg = $.parseJSON(e.data);
576 var msg = $.parseJSON(e.data);
571
577
572 var handler = this.get_iopub_handler(msg.header.msg_type);
578 var handler = this.get_iopub_handler(msg.header.msg_type);
573 if (handler !== undefined) {
579 if (handler !== undefined) {
574 handler(msg);
580 handler(msg);
575 }
581 }
576 };
582 };
577
583
578
584
579 Kernel.prototype._handle_input_request = function (e) {
585 Kernel.prototype._handle_input_request = function (e) {
580 var request = $.parseJSON(e.data);
586 var request = $.parseJSON(e.data);
581 var header = request.header;
587 var header = request.header;
582 var content = request.content;
588 var content = request.content;
583 var metadata = request.metadata;
589 var metadata = request.metadata;
584 var msg_type = header.msg_type;
590 var msg_type = header.msg_type;
585 if (msg_type !== 'input_request') {
591 if (msg_type !== 'input_request') {
586 console.log("Invalid input request!", request);
592 console.log("Invalid input request!", request);
587 return;
593 return;
588 }
594 }
589 var callbacks = this.get_callbacks_for_msg(request.parent_header.msg_id);
595 var callbacks = this.get_callbacks_for_msg(request.parent_header.msg_id);
590 if (callbacks) {
596 if (callbacks) {
591 if (callbacks.input) {
597 if (callbacks.input) {
592 callbacks.input(request);
598 callbacks.input(request);
593 }
599 }
594 }
600 }
595 };
601 };
596
602
597
603
598 IPython.Kernel = Kernel;
604 IPython.Kernel = Kernel;
599
605
600 return IPython;
606 return IPython;
601
607
602 }(IPython));
608 }(IPython));
603
609
@@ -1,118 +1,119 b''
1 //----------------------------------------------------------------------------
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2013 The IPython Development Team
2 // Copyright (C) 2013 The IPython Development Team
3 //
3 //
4 // Distributed under the terms of the BSD License. The full license is in
4 // Distributed under the terms of the BSD License. The full license is in
5 // the file COPYING, distributed as part of this software.
5 // the file COPYING, distributed as part of this software.
6 //----------------------------------------------------------------------------
6 //----------------------------------------------------------------------------
7
7
8 //============================================================================
8 //============================================================================
9 // Notebook
9 // Notebook
10 //============================================================================
10 //============================================================================
11
11
12 var IPython = (function (IPython) {
12 var IPython = (function (IPython) {
13 "use strict";
13 "use strict";
14
14
15 var utils = IPython.utils;
15 var utils = IPython.utils;
16
16
17 var Session = function(notebook_name, notebook_path, notebook){
17 var Session = function(notebook, options){
18 this.kernel = null;
18 this.kernel = null;
19 this.id = null;
19 this.id = null;
20 this.name = notebook_name;
21 this.path = notebook_path;
22 this.notebook = notebook;
20 this.notebook = notebook;
23 this._baseProjectUrl = notebook.baseProjectUrl();
21 this.name = notebook.notebook_name;
22 this.path = notebook.notebook_path;
23 this.base_url = notebook.base_url;
24 this.base_kernel_url = options.base_kernel_url || utils.get_body_data("baseKernelUrl");
24 };
25 };
25
26
26 Session.prototype.start = function(callback) {
27 Session.prototype.start = function(callback) {
27 var that = this;
28 var that = this;
28 var model = {
29 var model = {
29 notebook : {
30 notebook : {
30 name : this.name,
31 name : this.name,
31 path : this.path
32 path : this.path
32 }
33 }
33 };
34 };
34 var settings = {
35 var settings = {
35 processData : false,
36 processData : false,
36 cache : false,
37 cache : false,
37 type : "POST",
38 type : "POST",
38 data: JSON.stringify(model),
39 data: JSON.stringify(model),
39 dataType : "json",
40 dataType : "json",
40 success : function (data, status, xhr) {
41 success : function (data, status, xhr) {
41 that._handle_start_success(data);
42 that._handle_start_success(data);
42 if (callback) {
43 if (callback) {
43 callback(data, status, xhr);
44 callback(data, status, xhr);
44 }
45 }
45 },
46 },
46 };
47 };
47 var url = utils.url_join_encode(this._baseProjectUrl, 'api/sessions');
48 var url = utils.url_join_encode(this.base_url, 'api/sessions');
48 $.ajax(url, settings);
49 $.ajax(url, settings);
49 };
50 };
50
51
51 Session.prototype.rename_notebook = function (name, path) {
52 Session.prototype.rename_notebook = function (name, path) {
52 this.name = name;
53 this.name = name;
53 this.path = path;
54 this.path = path;
54 var model = {
55 var model = {
55 notebook : {
56 notebook : {
56 name : this.name,
57 name : this.name,
57 path : this.path
58 path : this.path
58 }
59 }
59 };
60 };
60 var settings = {
61 var settings = {
61 processData : false,
62 processData : false,
62 cache : false,
63 cache : false,
63 type : "PATCH",
64 type : "PATCH",
64 data: JSON.stringify(model),
65 data: JSON.stringify(model),
65 dataType : "json",
66 dataType : "json",
66 };
67 };
67 var url = utils.url_join_encode(this._baseProjectUrl, 'api/sessions', this.id);
68 var url = utils.url_join_encode(this.base_url, 'api/sessions', this.id);
68 $.ajax(url, settings);
69 $.ajax(url, settings);
69 };
70 };
70
71
71 Session.prototype.delete = function() {
72 Session.prototype.delete = function() {
72 var settings = {
73 var settings = {
73 processData : false,
74 processData : false,
74 cache : false,
75 cache : false,
75 type : "DELETE",
76 type : "DELETE",
76 dataType : "json",
77 dataType : "json",
77 };
78 };
78 this.kernel.running = false;
79 this.kernel.running = false;
79 var url = utils.url_join_encode(this._baseProjectUrl, 'api/sessions', this.id);
80 var url = utils.url_join_encode(this.base_url, 'api/sessions', this.id);
80 $.ajax(url, settings);
81 $.ajax(url, settings);
81 };
82 };
82
83
83 // Kernel related things
84 // Kernel related things
84 /**
85 /**
85 * Create the Kernel object associated with this Session.
86 * Create the Kernel object associated with this Session.
86 *
87 *
87 * @method _handle_start_success
88 * @method _handle_start_success
88 */
89 */
89 Session.prototype._handle_start_success = function (data, status, xhr) {
90 Session.prototype._handle_start_success = function (data, status, xhr) {
90 this.id = data.id;
91 this.id = data.id;
91 var base_url = utils.url_path_join($('body').data('baseKernelUrl'), "api/kernels");
92 var kernel_service_url = utils.url_path_join(this.base_kernel_url, "api/kernels");
92 this.kernel = new IPython.Kernel(base_url);
93 this.kernel = new IPython.Kernel(kernel_service_url);
93 this.kernel._kernel_started(data.kernel);
94 this.kernel._kernel_started(data.kernel);
94 };
95 };
95
96
96 /**
97 /**
97 * Prompt the user to restart the IPython kernel.
98 * Prompt the user to restart the IPython kernel.
98 *
99 *
99 * @method restart_kernel
100 * @method restart_kernel
100 */
101 */
101 Session.prototype.restart_kernel = function () {
102 Session.prototype.restart_kernel = function () {
102 this.kernel.restart();
103 this.kernel.restart();
103 };
104 };
104
105
105 Session.prototype.interrupt_kernel = function() {
106 Session.prototype.interrupt_kernel = function() {
106 this.kernel.interrupt();
107 this.kernel.interrupt();
107 };
108 };
108
109
109
110
110 Session.prototype.kill_kernel = function() {
111 Session.prototype.kill_kernel = function() {
111 this.kernel.kill();
112 this.kernel.kill();
112 };
113 };
113
114
114 IPython.Session = Session;
115 IPython.Session = Session;
115
116
116 return IPython;
117 return IPython;
117
118
118 }(IPython));
119 }(IPython));
@@ -1,199 +1,196 b''
1 //----------------------------------------------------------------------------
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2011 The IPython Development Team
2 // Copyright (C) 2011 The IPython Development Team
3 //
3 //
4 // Distributed under the terms of the BSD License. The full license is in
4 // Distributed under the terms of the BSD License. The full license is in
5 // the file COPYING, distributed as part of this software.
5 // the file COPYING, distributed as part of this software.
6 //----------------------------------------------------------------------------
6 //----------------------------------------------------------------------------
7
7
8 //============================================================================
8 //============================================================================
9 // NotebookList
9 // NotebookList
10 //============================================================================
10 //============================================================================
11
11
12 var IPython = (function (IPython) {
12 var IPython = (function (IPython) {
13 "use strict";
13 "use strict";
14
14
15 var utils = IPython.utils;
15 var utils = IPython.utils;
16
16
17 var ClusterList = function (selector) {
17 var ClusterList = function (selector, options) {
18 this.selector = selector;
18 this.selector = selector;
19 if (this.selector !== undefined) {
19 if (this.selector !== undefined) {
20 this.element = $(selector);
20 this.element = $(selector);
21 this.style();
21 this.style();
22 this.bind_events();
22 this.bind_events();
23 }
23 }
24 };
24 options = options || {};
25
25 this.options = options;
26 ClusterList.prototype.baseProjectUrl = function(){
26 this.base_url = options.base_url || utils.get_body_data("baseUrl");
27 return this._baseProjectUrl || $('body').data('baseProjectUrl');
27 this.notebook_path = options.notebook_path || utils.get_body_data("notebookPath");
28 };
28 };
29
29
30 ClusterList.prototype.style = function () {
30 ClusterList.prototype.style = function () {
31 $('#cluster_list').addClass('list_container');
31 $('#cluster_list').addClass('list_container');
32 $('#cluster_toolbar').addClass('list_toolbar');
32 $('#cluster_toolbar').addClass('list_toolbar');
33 $('#cluster_list_info').addClass('toolbar_info');
33 $('#cluster_list_info').addClass('toolbar_info');
34 $('#cluster_buttons').addClass('toolbar_buttons');
34 $('#cluster_buttons').addClass('toolbar_buttons');
35 };
35 };
36
36
37
37
38 ClusterList.prototype.bind_events = function () {
38 ClusterList.prototype.bind_events = function () {
39 var that = this;
39 var that = this;
40 $('#refresh_cluster_list').click(function () {
40 $('#refresh_cluster_list').click(function () {
41 that.load_list();
41 that.load_list();
42 });
42 });
43 };
43 };
44
44
45
45
46 ClusterList.prototype.load_list = function () {
46 ClusterList.prototype.load_list = function () {
47 var settings = {
47 var settings = {
48 processData : false,
48 processData : false,
49 cache : false,
49 cache : false,
50 type : "GET",
50 type : "GET",
51 dataType : "json",
51 dataType : "json",
52 success : $.proxy(this.load_list_success, this)
52 success : $.proxy(this.load_list_success, this)
53 };
53 };
54 var url = utils.url_join_encode(this.baseProjectUrl(), 'clusters');
54 var url = utils.url_join_encode(this.base_url, 'clusters');
55 $.ajax(url, settings);
55 $.ajax(url, settings);
56 };
56 };
57
57
58
58
59 ClusterList.prototype.clear_list = function () {
59 ClusterList.prototype.clear_list = function () {
60 this.element.children('.list_item').remove();
60 this.element.children('.list_item').remove();
61 };
61 };
62
62
63 ClusterList.prototype.load_list_success = function (data, status, xhr) {
63 ClusterList.prototype.load_list_success = function (data, status, xhr) {
64 this.clear_list();
64 this.clear_list();
65 var len = data.length;
65 var len = data.length;
66 for (var i=0; i<len; i++) {
66 for (var i=0; i<len; i++) {
67 var element = $('<div/>');
67 var element = $('<div/>');
68 var item = new ClusterItem(element);
68 var item = new ClusterItem(element, this.options);
69 item.update_state(data[i]);
69 item.update_state(data[i]);
70 element.data('item', item);
70 element.data('item', item);
71 this.element.append(element);
71 this.element.append(element);
72 }
72 }
73 };
73 };
74
74
75
75
76 var ClusterItem = function (element) {
76 var ClusterItem = function (element, options) {
77 this.element = $(element);
77 this.element = $(element);
78 this.base_url = options.base_url || utils.get_body_data("baseUrl");
79 this.notebook_path = options.notebook_path || utils.get_body_data("notebookPath");
78 this.data = null;
80 this.data = null;
79 this.style();
81 this.style();
80 };
82 };
81
83
82 ClusterItem.prototype.baseProjectUrl = function(){
83 return this._baseProjectUrl || $('body').data('baseProjectUrl');
84 };
85
86
87 ClusterItem.prototype.style = function () {
84 ClusterItem.prototype.style = function () {
88 this.element.addClass('list_item').addClass("row-fluid");
85 this.element.addClass('list_item').addClass("row-fluid");
89 };
86 };
90
87
91 ClusterItem.prototype.update_state = function (data) {
88 ClusterItem.prototype.update_state = function (data) {
92 this.data = data;
89 this.data = data;
93 if (data.status === 'running') {
90 if (data.status === 'running') {
94 this.state_running();
91 this.state_running();
95 } else if (data.status === 'stopped') {
92 } else if (data.status === 'stopped') {
96 this.state_stopped();
93 this.state_stopped();
97 }
94 }
98 };
95 };
99
96
100
97
101 ClusterItem.prototype.state_stopped = function () {
98 ClusterItem.prototype.state_stopped = function () {
102 var that = this;
99 var that = this;
103 var profile_col = $('<div/>').addClass('profile_col span4').text(this.data.profile);
100 var profile_col = $('<div/>').addClass('profile_col span4').text(this.data.profile);
104 var status_col = $('<div/>').addClass('status_col span3').text('stopped');
101 var status_col = $('<div/>').addClass('status_col span3').text('stopped');
105 var engines_col = $('<div/>').addClass('engine_col span3');
102 var engines_col = $('<div/>').addClass('engine_col span3');
106 var input = $('<input/>').attr('type','number')
103 var input = $('<input/>').attr('type','number')
107 .attr('min',1)
104 .attr('min',1)
108 .attr('size',3)
105 .attr('size',3)
109 .addClass('engine_num_input');
106 .addClass('engine_num_input');
110 engines_col.append(input);
107 engines_col.append(input);
111 var start_button = $('<button/>').addClass("btn btn-mini").text("Start");
108 var start_button = $('<button/>').addClass("btn btn-mini").text("Start");
112 var action_col = $('<div/>').addClass('action_col span2').append(
109 var action_col = $('<div/>').addClass('action_col span2').append(
113 $("<span/>").addClass("item_buttons btn-group").append(
110 $("<span/>").addClass("item_buttons btn-group").append(
114 start_button
111 start_button
115 )
112 )
116 );
113 );
117 this.element.empty()
114 this.element.empty()
118 .append(profile_col)
115 .append(profile_col)
119 .append(status_col)
116 .append(status_col)
120 .append(engines_col)
117 .append(engines_col)
121 .append(action_col);
118 .append(action_col);
122 start_button.click(function (e) {
119 start_button.click(function (e) {
123 var n = that.element.find('.engine_num_input').val();
120 var n = that.element.find('.engine_num_input').val();
124 if (!/^\d+$/.test(n) && n.length>0) {
121 if (!/^\d+$/.test(n) && n.length>0) {
125 status_col.text('invalid engine #');
122 status_col.text('invalid engine #');
126 } else {
123 } else {
127 var settings = {
124 var settings = {
128 cache : false,
125 cache : false,
129 data : {n:n},
126 data : {n:n},
130 type : "POST",
127 type : "POST",
131 dataType : "json",
128 dataType : "json",
132 success : function (data, status, xhr) {
129 success : function (data, status, xhr) {
133 that.update_state(data);
130 that.update_state(data);
134 },
131 },
135 error : function (data, status, xhr) {
132 error : function (data, status, xhr) {
136 status_col.text("error starting cluster");
133 status_col.text("error starting cluster");
137 }
134 }
138 };
135 };
139 status_col.text('starting');
136 status_col.text('starting');
140 var url = utils.url_join_encode(
137 var url = utils.url_join_encode(
141 that.baseProjectUrl(),
138 that.base_url,
142 'clusters',
139 'clusters',
143 that.data.profile,
140 that.data.profile,
144 'start'
141 'start'
145 );
142 );
146 $.ajax(url, settings);
143 $.ajax(url, settings);
147 }
144 }
148 });
145 });
149 };
146 };
150
147
151
148
152 ClusterItem.prototype.state_running = function () {
149 ClusterItem.prototype.state_running = function () {
153 var that = this;
150 var that = this;
154 var profile_col = $('<div/>').addClass('profile_col span4').text(this.data.profile);
151 var profile_col = $('<div/>').addClass('profile_col span4').text(this.data.profile);
155 var status_col = $('<div/>').addClass('status_col span3').text('running');
152 var status_col = $('<div/>').addClass('status_col span3').text('running');
156 var engines_col = $('<div/>').addClass('engines_col span3').text(this.data.n);
153 var engines_col = $('<div/>').addClass('engines_col span3').text(this.data.n);
157 var stop_button = $('<button/>').addClass("btn btn-mini").text("Stop");
154 var stop_button = $('<button/>').addClass("btn btn-mini").text("Stop");
158 var action_col = $('<div/>').addClass('action_col span2').append(
155 var action_col = $('<div/>').addClass('action_col span2').append(
159 $("<span/>").addClass("item_buttons btn-group").append(
156 $("<span/>").addClass("item_buttons btn-group").append(
160 stop_button
157 stop_button
161 )
158 )
162 );
159 );
163 this.element.empty()
160 this.element.empty()
164 .append(profile_col)
161 .append(profile_col)
165 .append(status_col)
162 .append(status_col)
166 .append(engines_col)
163 .append(engines_col)
167 .append(action_col);
164 .append(action_col);
168 stop_button.click(function (e) {
165 stop_button.click(function (e) {
169 var settings = {
166 var settings = {
170 cache : false,
167 cache : false,
171 type : "POST",
168 type : "POST",
172 dataType : "json",
169 dataType : "json",
173 success : function (data, status, xhr) {
170 success : function (data, status, xhr) {
174 that.update_state(data);
171 that.update_state(data);
175 },
172 },
176 error : function (data, status, xhr) {
173 error : function (data, status, xhr) {
177 console.log('error',data);
174 console.log('error',data);
178 status_col.text("error stopping cluster");
175 status_col.text("error stopping cluster");
179 }
176 }
180 };
177 };
181 status_col.text('stopping');
178 status_col.text('stopping');
182 var url = utils.url_join_encode(
179 var url = utils.url_join_encode(
183 that.baseProjectUrl(),
180 that.base_url,
184 'clusters',
181 'clusters',
185 that.data.profile,
182 that.data.profile,
186 'stop'
183 'stop'
187 );
184 );
188 $.ajax(url, settings);
185 $.ajax(url, settings);
189 });
186 });
190 };
187 };
191
188
192
189
193 IPython.ClusterList = ClusterList;
190 IPython.ClusterList = ClusterList;
194 IPython.ClusterItem = ClusterItem;
191 IPython.ClusterItem = ClusterItem;
195
192
196 return IPython;
193 return IPython;
197
194
198 }(IPython));
195 }(IPython));
199
196
@@ -1,85 +1,89 b''
1 //----------------------------------------------------------------------------
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2008-2011 The IPython Development Team
2 // Copyright (C) 2008-2011 The IPython Development Team
3 //
3 //
4 // Distributed under the terms of the BSD License. The full license is in
4 // Distributed under the terms of the BSD License. The full license is in
5 // the file COPYING, distributed as part of this software.
5 // the file COPYING, distributed as part of this software.
6 //----------------------------------------------------------------------------
6 //----------------------------------------------------------------------------
7
7
8 //============================================================================
8 //============================================================================
9 // On document ready
9 // On document ready
10 //============================================================================
10 //============================================================================
11
11
12
12
13 $(document).ready(function () {
13 $(document).ready(function () {
14
14
15 IPython.page = new IPython.Page();
15 IPython.page = new IPython.Page();
16
16
17 $('#new_notebook').button().click(function (e) {
17 $('#new_notebook').button().click(function (e) {
18 IPython.notebook_list.new_notebook($('body').data('baseProjectUrl'))
18 IPython.notebook_list.new_notebook()
19 });
19 });
20
20
21 IPython.notebook_list = new IPython.NotebookList('#notebook_list');
21 var opts = {
22 IPython.cluster_list = new IPython.ClusterList('#cluster_list');
22 base_url : IPython.utils.get_body_data("baseUrl"),
23 IPython.login_widget = new IPython.LoginWidget('#login_widget');
23 notebook_path : IPython.utils.get_body_data("notebookPath"),
24 };
25 IPython.notebook_list = new IPython.NotebookList('#notebook_list', opts);
26 IPython.cluster_list = new IPython.ClusterList('#cluster_list', opts);
27 IPython.login_widget = new IPython.LoginWidget('#login_widget', opts);
24
28
25 var interval_id=0;
29 var interval_id=0;
26 // auto refresh every xx secondes, no need to be fast,
30 // auto refresh every xx secondes, no need to be fast,
27 // update is done at least when page get focus
31 // update is done at least when page get focus
28 var time_refresh = 60; // in sec
32 var time_refresh = 60; // in sec
29
33
30 var enable_autorefresh = function(){
34 var enable_autorefresh = function(){
31 //refresh immediately , then start interval
35 //refresh immediately , then start interval
32 if($('.upload_button').length == 0)
36 if($('.upload_button').length == 0)
33 {
37 {
34 IPython.notebook_list.load_sessions();
38 IPython.notebook_list.load_sessions();
35 IPython.cluster_list.load_list();
39 IPython.cluster_list.load_list();
36 }
40 }
37 if (!interval_id){
41 if (!interval_id){
38 interval_id = setInterval(function(){
42 interval_id = setInterval(function(){
39 if($('.upload_button').length == 0)
43 if($('.upload_button').length == 0)
40 {
44 {
41 IPython.notebook_list.load_sessions();
45 IPython.notebook_list.load_sessions();
42 IPython.cluster_list.load_list();
46 IPython.cluster_list.load_list();
43 }
47 }
44 }, time_refresh*1000);
48 }, time_refresh*1000);
45 }
49 }
46 }
50 }
47
51
48 var disable_autorefresh = function(){
52 var disable_autorefresh = function(){
49 clearInterval(interval_id);
53 clearInterval(interval_id);
50 interval_id = 0;
54 interval_id = 0;
51 }
55 }
52
56
53 // stop autorefresh when page lose focus
57 // stop autorefresh when page lose focus
54 $(window).blur(function() {
58 $(window).blur(function() {
55 disable_autorefresh();
59 disable_autorefresh();
56 })
60 })
57
61
58 //re-enable when page get focus back
62 //re-enable when page get focus back
59 $(window).focus(function() {
63 $(window).focus(function() {
60 enable_autorefresh();
64 enable_autorefresh();
61 });
65 });
62
66
63 // finally start it, it will refresh immediately
67 // finally start it, it will refresh immediately
64 enable_autorefresh();
68 enable_autorefresh();
65
69
66 IPython.page.show();
70 IPython.page.show();
67
71
68 // bound the upload method to the on change of the file select list
72 // bound the upload method to the on change of the file select list
69 $("#alternate_upload").change(function (event){
73 $("#alternate_upload").change(function (event){
70 IPython.notebook_list.handelFilesUpload(event,'form');
74 IPython.notebook_list.handelFilesUpload(event,'form');
71 });
75 });
72
76
73 // set hash on tab click
77 // set hash on tab click
74 $("#tabs").find("a").click(function() {
78 $("#tabs").find("a").click(function() {
75 window.location.hash = $(this).attr("href");
79 window.location.hash = $(this).attr("href");
76 })
80 })
77
81
78 // load tab if url hash
82 // load tab if url hash
79 if (window.location.hash) {
83 if (window.location.hash) {
80 $("#tabs").find("a[href=" + window.location.hash + "]").click();
84 $("#tabs").find("a[href=" + window.location.hash + "]").click();
81 }
85 }
82
86
83
87
84 });
88 });
85
89
@@ -1,437 +1,431 b''
1 //----------------------------------------------------------------------------
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2011 The IPython Development Team
2 // Copyright (C) 2011 The IPython Development Team
3 //
3 //
4 // Distributed under the terms of the BSD License. The full license is in
4 // Distributed under the terms of the BSD License. The full license is in
5 // the file COPYING, distributed as part of this software.
5 // the file COPYING, distributed as part of this software.
6 //----------------------------------------------------------------------------
6 //----------------------------------------------------------------------------
7
7
8 //============================================================================
8 //============================================================================
9 // NotebookList
9 // NotebookList
10 //============================================================================
10 //============================================================================
11
11
12 var IPython = (function (IPython) {
12 var IPython = (function (IPython) {
13 "use strict";
13 "use strict";
14
14
15 var utils = IPython.utils;
15 var utils = IPython.utils;
16
16
17 var NotebookList = function (selector) {
17 var NotebookList = function (selector, options) {
18 this.selector = selector;
18 this.selector = selector;
19 if (this.selector !== undefined) {
19 if (this.selector !== undefined) {
20 this.element = $(selector);
20 this.element = $(selector);
21 this.style();
21 this.style();
22 this.bind_events();
22 this.bind_events();
23 }
23 }
24 this.notebooks_list = [];
24 this.notebooks_list = [];
25 this.sessions = {};
25 this.sessions = {};
26 this.base_url = options.base_url || utils.get_body_data("baseUrl");
27 this.notebook_path = options.notebook_path || utils.get_body_data("notebookPath");
26 };
28 };
27
29
28 NotebookList.prototype.baseProjectUrl = function () {
29 return $('body').data('baseProjectUrl');
30 };
31
32 NotebookList.prototype.notebookPath = function() {
33 return $('body').data('notebookPath');
34 };
35
36 NotebookList.prototype.style = function () {
30 NotebookList.prototype.style = function () {
37 $('#notebook_toolbar').addClass('list_toolbar');
31 $('#notebook_toolbar').addClass('list_toolbar');
38 $('#drag_info').addClass('toolbar_info');
32 $('#drag_info').addClass('toolbar_info');
39 $('#notebook_buttons').addClass('toolbar_buttons');
33 $('#notebook_buttons').addClass('toolbar_buttons');
40 $('#notebook_list_header').addClass('list_header');
34 $('#notebook_list_header').addClass('list_header');
41 this.element.addClass("list_container");
35 this.element.addClass("list_container");
42 };
36 };
43
37
44
38
45 NotebookList.prototype.bind_events = function () {
39 NotebookList.prototype.bind_events = function () {
46 var that = this;
40 var that = this;
47 $('#refresh_notebook_list').click(function () {
41 $('#refresh_notebook_list').click(function () {
48 that.load_list();
42 that.load_list();
49 });
43 });
50 this.element.bind('dragover', function () {
44 this.element.bind('dragover', function () {
51 return false;
45 return false;
52 });
46 });
53 this.element.bind('drop', function(event){
47 this.element.bind('drop', function(event){
54 that.handelFilesUpload(event,'drop');
48 that.handelFilesUpload(event,'drop');
55 return false;
49 return false;
56 });
50 });
57 };
51 };
58
52
59 NotebookList.prototype.handelFilesUpload = function(event, dropOrForm) {
53 NotebookList.prototype.handelFilesUpload = function(event, dropOrForm) {
60 var that = this;
54 var that = this;
61 var files;
55 var files;
62 if(dropOrForm =='drop'){
56 if(dropOrForm =='drop'){
63 files = event.originalEvent.dataTransfer.files;
57 files = event.originalEvent.dataTransfer.files;
64 } else
58 } else
65 {
59 {
66 files = event.originalEvent.target.files;
60 files = event.originalEvent.target.files;
67 }
61 }
68 for (var i = 0; i < files.length; i++) {
62 for (var i = 0; i < files.length; i++) {
69 var f = files[i];
63 var f = files[i];
70 var reader = new FileReader();
64 var reader = new FileReader();
71 reader.readAsText(f);
65 reader.readAsText(f);
72 var name_and_ext = utils.splitext(f.name);
66 var name_and_ext = utils.splitext(f.name);
73 var file_ext = name_and_ext[1];
67 var file_ext = name_and_ext[1];
74 if (file_ext === '.ipynb') {
68 if (file_ext === '.ipynb') {
75 var item = that.new_notebook_item(0);
69 var item = that.new_notebook_item(0);
76 that.add_name_input(f.name, item);
70 that.add_name_input(f.name, item);
77 // Store the notebook item in the reader so we can use it later
71 // Store the notebook item in the reader so we can use it later
78 // to know which item it belongs to.
72 // to know which item it belongs to.
79 $(reader).data('item', item);
73 $(reader).data('item', item);
80 reader.onload = function (event) {
74 reader.onload = function (event) {
81 var nbitem = $(event.target).data('item');
75 var nbitem = $(event.target).data('item');
82 that.add_notebook_data(event.target.result, nbitem);
76 that.add_notebook_data(event.target.result, nbitem);
83 that.add_upload_button(nbitem);
77 that.add_upload_button(nbitem);
84 };
78 };
85 } else {
79 } else {
86 var dialog = 'Uploaded notebooks must be .ipynb files';
80 var dialog = 'Uploaded notebooks must be .ipynb files';
87 IPython.dialog.modal({
81 IPython.dialog.modal({
88 title : 'Invalid file type',
82 title : 'Invalid file type',
89 body : dialog,
83 body : dialog,
90 buttons : {'OK' : {'class' : 'btn-primary'}}
84 buttons : {'OK' : {'class' : 'btn-primary'}}
91 });
85 });
92 }
86 }
93 }
87 }
94 // Replace the file input form wth a clone of itself. This is required to
88 // Replace the file input form wth a clone of itself. This is required to
95 // reset the form. Otherwise, if you upload a file, delete it and try to
89 // reset the form. Otherwise, if you upload a file, delete it and try to
96 // upload it again, the changed event won't fire.
90 // upload it again, the changed event won't fire.
97 var form = $('input.fileinput');
91 var form = $('input.fileinput');
98 form.replaceWith(form.clone(true));
92 form.replaceWith(form.clone(true));
99 return false;
93 return false;
100 };
94 };
101
95
102 NotebookList.prototype.clear_list = function () {
96 NotebookList.prototype.clear_list = function () {
103 this.element.children('.list_item').remove();
97 this.element.children('.list_item').remove();
104 };
98 };
105
99
106 NotebookList.prototype.load_sessions = function(){
100 NotebookList.prototype.load_sessions = function(){
107 var that = this;
101 var that = this;
108 var settings = {
102 var settings = {
109 processData : false,
103 processData : false,
110 cache : false,
104 cache : false,
111 type : "GET",
105 type : "GET",
112 dataType : "json",
106 dataType : "json",
113 success : $.proxy(that.sessions_loaded, this)
107 success : $.proxy(that.sessions_loaded, this)
114 };
108 };
115 var url = this.baseProjectUrl() + 'api/sessions';
109 var url = utils.url_join_encode(this.base_url, 'api/sessions');
116 $.ajax(url,settings);
110 $.ajax(url,settings);
117 };
111 };
118
112
119
113
120 NotebookList.prototype.sessions_loaded = function(data){
114 NotebookList.prototype.sessions_loaded = function(data){
121 this.sessions = {};
115 this.sessions = {};
122 var len = data.length;
116 var len = data.length;
123 if (len > 0) {
117 if (len > 0) {
124 for (var i=0; i<len; i++) {
118 for (var i=0; i<len; i++) {
125 var nb_path;
119 var nb_path;
126 if (!data[i].notebook.path) {
120 if (!data[i].notebook.path) {
127 nb_path = data[i].notebook.name;
121 nb_path = data[i].notebook.name;
128 }
122 }
129 else {
123 else {
130 nb_path = utils.url_path_join(
124 nb_path = utils.url_path_join(
131 data[i].notebook.path,
125 data[i].notebook.path,
132 data[i].notebook.name
126 data[i].notebook.name
133 );
127 );
134 }
128 }
135 this.sessions[nb_path] = data[i].id;
129 this.sessions[nb_path] = data[i].id;
136 }
130 }
137 }
131 }
138 this.load_list();
132 this.load_list();
139 };
133 };
140
134
141 NotebookList.prototype.load_list = function () {
135 NotebookList.prototype.load_list = function () {
142 var that = this;
136 var that = this;
143 var settings = {
137 var settings = {
144 processData : false,
138 processData : false,
145 cache : false,
139 cache : false,
146 type : "GET",
140 type : "GET",
147 dataType : "json",
141 dataType : "json",
148 success : $.proxy(this.list_loaded, this),
142 success : $.proxy(this.list_loaded, this),
149 error : $.proxy( function(){
143 error : $.proxy( function(){
150 that.list_loaded([], null, null, {msg:"Error connecting to server."});
144 that.list_loaded([], null, null, {msg:"Error connecting to server."});
151 },this)
145 },this)
152 };
146 };
153
147
154 var url = utils.url_join_encode(
148 var url = utils.url_join_encode(
155 this.baseProjectUrl(),
149 this.base_url,
156 'api',
150 'api',
157 'notebooks',
151 'notebooks',
158 this.notebookPath()
152 this.notebook_path
159 );
153 );
160 $.ajax(url, settings);
154 $.ajax(url, settings);
161 };
155 };
162
156
163
157
164 NotebookList.prototype.list_loaded = function (data, status, xhr, param) {
158 NotebookList.prototype.list_loaded = function (data, status, xhr, param) {
165 var message = 'Notebook list empty.';
159 var message = 'Notebook list empty.';
166 if (param !== undefined && param.msg) {
160 if (param !== undefined && param.msg) {
167 message = param.msg;
161 message = param.msg;
168 }
162 }
169 var item = null;
163 var item = null;
170 var len = data.length;
164 var len = data.length;
171 this.clear_list();
165 this.clear_list();
172 if (len === 0) {
166 if (len === 0) {
173 item = this.new_notebook_item(0);
167 item = this.new_notebook_item(0);
174 var span12 = item.children().first();
168 var span12 = item.children().first();
175 span12.empty();
169 span12.empty();
176 span12.append($('<div style="margin:auto;text-align:center;color:grey"/>').text(message));
170 span12.append($('<div style="margin:auto;text-align:center;color:grey"/>').text(message));
177 }
171 }
178 var path = this.notebookPath();
172 var path = this.notebook_path;
179 var offset = 0;
173 var offset = 0;
180 if (path !== '') {
174 if (path !== '') {
181 item = this.new_notebook_item(0);
175 item = this.new_notebook_item(0);
182 this.add_dir(path, '..', item);
176 this.add_dir(path, '..', item);
183 offset = 1;
177 offset = 1;
184 }
178 }
185 for (var i=0; i<len; i++) {
179 for (var i=0; i<len; i++) {
186 if (data[i].type === 'directory') {
180 if (data[i].type === 'directory') {
187 var name = data[i].name;
181 var name = data[i].name;
188 item = this.new_notebook_item(i+offset);
182 item = this.new_notebook_item(i+offset);
189 this.add_dir(path, name, item);
183 this.add_dir(path, name, item);
190 } else {
184 } else {
191 var name = data[i].name;
185 var name = data[i].name;
192 item = this.new_notebook_item(i+offset);
186 item = this.new_notebook_item(i+offset);
193 this.add_link(path, name, item);
187 this.add_link(path, name, item);
194 name = utils.url_path_join(path, name);
188 name = utils.url_path_join(path, name);
195 if(this.sessions[name] === undefined){
189 if(this.sessions[name] === undefined){
196 this.add_delete_button(item);
190 this.add_delete_button(item);
197 } else {
191 } else {
198 this.add_shutdown_button(item,this.sessions[name]);
192 this.add_shutdown_button(item,this.sessions[name]);
199 }
193 }
200 }
194 }
201 }
195 }
202 };
196 };
203
197
204
198
205 NotebookList.prototype.new_notebook_item = function (index) {
199 NotebookList.prototype.new_notebook_item = function (index) {
206 var item = $('<div/>').addClass("list_item").addClass("row-fluid");
200 var item = $('<div/>').addClass("list_item").addClass("row-fluid");
207 // item.addClass('list_item ui-widget ui-widget-content ui-helper-clearfix');
201 // item.addClass('list_item ui-widget ui-widget-content ui-helper-clearfix');
208 // item.css('border-top-style','none');
202 // item.css('border-top-style','none');
209 item.append($("<div/>").addClass("span12").append(
203 item.append($("<div/>").addClass("span12").append(
210 $('<i/>').addClass('item_icon')
204 $('<i/>').addClass('item_icon')
211 ).append(
205 ).append(
212 $("<a/>").addClass("item_link").append(
206 $("<a/>").addClass("item_link").append(
213 $("<span/>").addClass("item_name")
207 $("<span/>").addClass("item_name")
214 )
208 )
215 ).append(
209 ).append(
216 $('<div/>').addClass("item_buttons btn-group pull-right")
210 $('<div/>').addClass("item_buttons btn-group pull-right")
217 ));
211 ));
218
212
219 if (index === -1) {
213 if (index === -1) {
220 this.element.append(item);
214 this.element.append(item);
221 } else {
215 } else {
222 this.element.children().eq(index).after(item);
216 this.element.children().eq(index).after(item);
223 }
217 }
224 return item;
218 return item;
225 };
219 };
226
220
227
221
228 NotebookList.prototype.add_dir = function (path, name, item) {
222 NotebookList.prototype.add_dir = function (path, name, item) {
229 item.data('name', name);
223 item.data('name', name);
230 item.data('path', path);
224 item.data('path', path);
231 item.find(".item_name").text(name);
225 item.find(".item_name").text(name);
232 item.find(".item_icon").addClass('icon-folder-open');
226 item.find(".item_icon").addClass('icon-folder-open');
233 item.find("a.item_link")
227 item.find("a.item_link")
234 .attr('href',
228 .attr('href',
235 utils.url_join_encode(
229 utils.url_join_encode(
236 this.baseProjectUrl(),
230 this.base_url,
237 "tree",
231 "tree",
238 path,
232 path,
239 name
233 name
240 )
234 )
241 );
235 );
242 };
236 };
243
237
244
238
245 NotebookList.prototype.add_link = function (path, nbname, item) {
239 NotebookList.prototype.add_link = function (path, nbname, item) {
246 item.data('nbname', nbname);
240 item.data('nbname', nbname);
247 item.data('path', path);
241 item.data('path', path);
248 item.find(".item_name").text(nbname);
242 item.find(".item_name").text(nbname);
249 item.find(".item_icon").addClass('icon-book');
243 item.find(".item_icon").addClass('icon-book');
250 item.find("a.item_link")
244 item.find("a.item_link")
251 .attr('href',
245 .attr('href',
252 utils.url_join_encode(
246 utils.url_join_encode(
253 this.baseProjectUrl(),
247 this.base_url,
254 "notebooks",
248 "notebooks",
255 path,
249 path,
256 nbname
250 nbname
257 )
251 )
258 ).attr('target','_blank');
252 ).attr('target','_blank');
259 };
253 };
260
254
261
255
262 NotebookList.prototype.add_name_input = function (nbname, item) {
256 NotebookList.prototype.add_name_input = function (nbname, item) {
263 item.data('nbname', nbname);
257 item.data('nbname', nbname);
264 item.find(".item_icon").addClass('icon-book');
258 item.find(".item_icon").addClass('icon-book');
265 item.find(".item_name").empty().append(
259 item.find(".item_name").empty().append(
266 $('<input/>')
260 $('<input/>')
267 .addClass("nbname_input")
261 .addClass("nbname_input")
268 .attr('value', utils.splitext(nbname)[0])
262 .attr('value', utils.splitext(nbname)[0])
269 .attr('size', '30')
263 .attr('size', '30')
270 .attr('type', 'text')
264 .attr('type', 'text')
271 );
265 );
272 };
266 };
273
267
274
268
275 NotebookList.prototype.add_notebook_data = function (data, item) {
269 NotebookList.prototype.add_notebook_data = function (data, item) {
276 item.data('nbdata', data);
270 item.data('nbdata', data);
277 };
271 };
278
272
279
273
280 NotebookList.prototype.add_shutdown_button = function (item, session) {
274 NotebookList.prototype.add_shutdown_button = function (item, session) {
281 var that = this;
275 var that = this;
282 var shutdown_button = $("<button/>").text("Shutdown").addClass("btn btn-mini btn-danger").
276 var shutdown_button = $("<button/>").text("Shutdown").addClass("btn btn-mini btn-danger").
283 click(function (e) {
277 click(function (e) {
284 var settings = {
278 var settings = {
285 processData : false,
279 processData : false,
286 cache : false,
280 cache : false,
287 type : "DELETE",
281 type : "DELETE",
288 dataType : "json",
282 dataType : "json",
289 success : function () {
283 success : function () {
290 that.load_sessions();
284 that.load_sessions();
291 }
285 }
292 };
286 };
293 var url = utils.url_join_encode(
287 var url = utils.url_join_encode(
294 that.baseProjectUrl(),
288 that.base_url,
295 'api/sessions',
289 'api/sessions',
296 session
290 session
297 );
291 );
298 $.ajax(url, settings);
292 $.ajax(url, settings);
299 return false;
293 return false;
300 });
294 });
301 // var new_buttons = item.find('a'); // shutdown_button;
295 // var new_buttons = item.find('a'); // shutdown_button;
302 item.find(".item_buttons").text("").append(shutdown_button);
296 item.find(".item_buttons").text("").append(shutdown_button);
303 };
297 };
304
298
305 NotebookList.prototype.add_delete_button = function (item) {
299 NotebookList.prototype.add_delete_button = function (item) {
306 var new_buttons = $('<span/>').addClass("btn-group pull-right");
300 var new_buttons = $('<span/>').addClass("btn-group pull-right");
307 var notebooklist = this;
301 var notebooklist = this;
308 var delete_button = $("<button/>").text("Delete").addClass("btn btn-mini").
302 var delete_button = $("<button/>").text("Delete").addClass("btn btn-mini").
309 click(function (e) {
303 click(function (e) {
310 // $(this) is the button that was clicked.
304 // $(this) is the button that was clicked.
311 var that = $(this);
305 var that = $(this);
312 // We use the nbname and notebook_id from the parent notebook_item element's
306 // We use the nbname and notebook_id from the parent notebook_item element's
313 // data because the outer scopes values change as we iterate through the loop.
307 // data because the outer scopes values change as we iterate through the loop.
314 var parent_item = that.parents('div.list_item');
308 var parent_item = that.parents('div.list_item');
315 var nbname = parent_item.data('nbname');
309 var nbname = parent_item.data('nbname');
316 var message = 'Are you sure you want to permanently delete the notebook: ' + nbname + '?';
310 var message = 'Are you sure you want to permanently delete the notebook: ' + nbname + '?';
317 IPython.dialog.modal({
311 IPython.dialog.modal({
318 title : "Delete notebook",
312 title : "Delete notebook",
319 body : message,
313 body : message,
320 buttons : {
314 buttons : {
321 Delete : {
315 Delete : {
322 class: "btn-danger",
316 class: "btn-danger",
323 click: function() {
317 click: function() {
324 var settings = {
318 var settings = {
325 processData : false,
319 processData : false,
326 cache : false,
320 cache : false,
327 type : "DELETE",
321 type : "DELETE",
328 dataType : "json",
322 dataType : "json",
329 success : function (data, status, xhr) {
323 success : function (data, status, xhr) {
330 parent_item.remove();
324 parent_item.remove();
331 }
325 }
332 };
326 };
333 var url = utils.url_join_encode(
327 var url = utils.url_join_encode(
334 notebooklist.baseProjectUrl(),
328 notebooklist.base_url,
335 'api/notebooks',
329 'api/notebooks',
336 notebooklist.notebookPath(),
330 notebooklist.notebook_path,
337 nbname
331 nbname
338 );
332 );
339 $.ajax(url, settings);
333 $.ajax(url, settings);
340 }
334 }
341 },
335 },
342 Cancel : {}
336 Cancel : {}
343 }
337 }
344 });
338 });
345 return false;
339 return false;
346 });
340 });
347 item.find(".item_buttons").text("").append(delete_button);
341 item.find(".item_buttons").text("").append(delete_button);
348 };
342 };
349
343
350
344
351 NotebookList.prototype.add_upload_button = function (item) {
345 NotebookList.prototype.add_upload_button = function (item) {
352 var that = this;
346 var that = this;
353 var upload_button = $('<button/>').text("Upload")
347 var upload_button = $('<button/>').text("Upload")
354 .addClass('btn btn-primary btn-mini upload_button')
348 .addClass('btn btn-primary btn-mini upload_button')
355 .click(function (e) {
349 .click(function (e) {
356 var nbname = item.find('.item_name > input').val();
350 var nbname = item.find('.item_name > input').val();
357 if (nbname.slice(nbname.length-6, nbname.length) != ".ipynb") {
351 if (nbname.slice(nbname.length-6, nbname.length) != ".ipynb") {
358 nbname = nbname + ".ipynb";
352 nbname = nbname + ".ipynb";
359 }
353 }
360 var path = that.notebookPath();
354 var path = that.notebook_path;
361 var nbdata = item.data('nbdata');
355 var nbdata = item.data('nbdata');
362 var content_type = 'application/json';
356 var content_type = 'application/json';
363 var model = {
357 var model = {
364 content : JSON.parse(nbdata),
358 content : JSON.parse(nbdata),
365 };
359 };
366 var settings = {
360 var settings = {
367 processData : false,
361 processData : false,
368 cache : false,
362 cache : false,
369 type : 'PUT',
363 type : 'PUT',
370 dataType : 'json',
364 dataType : 'json',
371 data : JSON.stringify(model),
365 data : JSON.stringify(model),
372 headers : {'Content-Type': content_type},
366 headers : {'Content-Type': content_type},
373 success : function (data, status, xhr) {
367 success : function (data, status, xhr) {
374 that.add_link(path, nbname, item);
368 that.add_link(path, nbname, item);
375 that.add_delete_button(item);
369 that.add_delete_button(item);
376 },
370 },
377 error : function (data, status, xhr) {
371 error : function (data, status, xhr) {
378 console.log(data, status);
372 console.log(data, status);
379 }
373 }
380 };
374 };
381
375
382 var url = utils.url_join_encode(
376 var url = utils.url_join_encode(
383 that.baseProjectUrl(),
377 that.base_url,
384 'api/notebooks',
378 'api/notebooks',
385 that.notebookPath(),
379 that.notebook_path,
386 nbname
380 nbname
387 );
381 );
388 $.ajax(url, settings);
382 $.ajax(url, settings);
389 return false;
383 return false;
390 });
384 });
391 var cancel_button = $('<button/>').text("Cancel")
385 var cancel_button = $('<button/>').text("Cancel")
392 .addClass("btn btn-mini")
386 .addClass("btn btn-mini")
393 .click(function (e) {
387 .click(function (e) {
394 console.log('cancel click');
388 console.log('cancel click');
395 item.remove();
389 item.remove();
396 return false;
390 return false;
397 });
391 });
398 item.find(".item_buttons").empty()
392 item.find(".item_buttons").empty()
399 .append(upload_button)
393 .append(upload_button)
400 .append(cancel_button);
394 .append(cancel_button);
401 };
395 };
402
396
403
397
404 NotebookList.prototype.new_notebook = function(){
398 NotebookList.prototype.new_notebook = function(){
405 var path = this.notebookPath();
399 var path = this.notebook_path;
406 var base_project_url = this.baseProjectUrl();
400 var base_url = this.base_url;
407 var settings = {
401 var settings = {
408 processData : false,
402 processData : false,
409 cache : false,
403 cache : false,
410 type : "POST",
404 type : "POST",
411 dataType : "json",
405 dataType : "json",
412 async : false,
406 async : false,
413 success : function (data, status, xhr) {
407 success : function (data, status, xhr) {
414 var notebook_name = data.name;
408 var notebook_name = data.name;
415 window.open(
409 window.open(
416 utils.url_join_encode(
410 utils.url_join_encode(
417 base_project_url,
411 base_url,
418 'notebooks',
412 'notebooks',
419 path,
413 path,
420 notebook_name),
414 notebook_name),
421 '_blank'
415 '_blank'
422 );
416 );
423 }
417 }
424 };
418 };
425 var url = utils.url_join_encode(
419 var url = utils.url_join_encode(
426 base_project_url,
420 base_url,
427 'api/notebooks',
421 'api/notebooks',
428 path
422 path
429 );
423 );
430 $.ajax(url, settings);
424 $.ajax(url, settings);
431 };
425 };
432
426
433 IPython.NotebookList = NotebookList;
427 IPython.NotebookList = NotebookList;
434
428
435 return IPython;
429 return IPython;
436
430
437 }(IPython));
431 }(IPython));
@@ -1,52 +1,52 b''
1 {% extends "page.html" %}
1 {% extends "page.html" %}
2
2
3
3
4 {% block stylesheet %}
4 {% block stylesheet %}
5 {{super()}}
5 {{super()}}
6 <link rel="stylesheet" href="{{ static_url("auth/css/override.css") }}" type="text/css" />
6 <link rel="stylesheet" href="{{ static_url("auth/css/override.css") }}" type="text/css" />
7 {% endblock %}
7 {% endblock %}
8
8
9 {% block login_widget %}
9 {% block login_widget %}
10 {% endblock %}
10 {% endblock %}
11
11
12 {% block site %}
12 {% block site %}
13
13
14 <div id="ipython-main-app" class="container">
14 <div id="ipython-main-app" class="container">
15
15
16 {% if login_available %}
16 {% if login_available %}
17 <div class="row">
17 <div class="row">
18 <div class="navbar span8 offset2">
18 <div class="navbar span8 offset2">
19 <div class="navbar-inner">
19 <div class="navbar-inner">
20 <div class="container">
20 <div class="container">
21 <div class="center-nav">
21 <div class="center-nav">
22 <p class="navbar-text nav">Password:</p>
22 <p class="navbar-text nav">Password:</p>
23 <form action="{{base_project_url}}login?next={{next}}" method="post" class="navbar-form pull-left">
23 <form action="{{base_url}}login?next={{next}}" method="post" class="navbar-form pull-left">
24 <input type="password" name="password" id="password_input">
24 <input type="password" name="password" id="password_input">
25 <button type="submit" id="login_submit">Log in</button>
25 <button type="submit" id="login_submit">Log in</button>
26 </form>
26 </form>
27 </div>
27 </div>
28 </div>
28 </div>
29 </div>
29 </div>
30 </div>
30 </div>
31 </div>
31 </div>
32 {% endif %}
32 {% endif %}
33 {% if message %}
33 {% if message %}
34 <div class="row">
34 <div class="row">
35 {% for key in message %}
35 {% for key in message %}
36 <div class="message {{key}}">
36 <div class="message {{key}}">
37 {{message[key]}}
37 {{message[key]}}
38 </div>
38 </div>
39 {% endfor %}
39 {% endfor %}
40 </div>
40 </div>
41 {% endif %}
41 {% endif %}
42
42
43 <div/>
43 <div/>
44
44
45 {% endblock %}
45 {% endblock %}
46
46
47
47
48 {% block script %}
48 {% block script %}
49
49
50 <script src="{{static_url("auth/js/loginmain.js") }}" type="text/javascript" charset="utf-8"></script>
50 <script src="{{static_url("auth/js/loginmain.js") }}" type="text/javascript" charset="utf-8"></script>
51
51
52 {% endblock %}
52 {% endblock %}
@@ -1,38 +1,38 b''
1 {% extends "page.html" %}
1 {% extends "page.html" %}
2
2
3 {% block stylesheet %}
3 {% block stylesheet %}
4 {{super()}}
4 {{super()}}
5 <link rel="stylesheet" href="{{ static_url("auth/css/override.css") }}" type="text/css" />
5 <link rel="stylesheet" href="{{ static_url("auth/css/override.css") }}" type="text/css" />
6 {% endblock %}
6 {% endblock %}
7
7
8 {% block login_widget %}
8 {% block login_widget %}
9 {% endblock %}
9 {% endblock %}
10
10
11 {% block site %}
11 {% block site %}
12
12
13 <div id="ipython-main-app" class="container">
13 <div id="ipython-main-app" class="container">
14
14
15 {% if message %}
15 {% if message %}
16 {% for key in message %}
16 {% for key in message %}
17 <div class="message {{key}}">
17 <div class="message {{key}}">
18 {{message[key]}}
18 {{message[key]}}
19 </div>
19 </div>
20 {% endfor %}
20 {% endfor %}
21 {% endif %}
21 {% endif %}
22
22
23 {% if not login_available %}
23 {% if not login_available %}
24 Proceed to the <a href="{{base_project_url}}">dashboard</a>.
24 Proceed to the <a href="{{base_url}}">dashboard</a>.
25 {% else %}
25 {% else %}
26 Proceed to the <a href="{{base_project_url}}login">login page</a>.
26 Proceed to the <a href="{{base_url}}login">login page</a>.
27 {% endif %}
27 {% endif %}
28
28
29
29
30 <div/>
30 <div/>
31
31
32 {% endblock %}
32 {% endblock %}
33
33
34 {% block script %}
34 {% block script %}
35
35
36 <script src="{{static_url("auth/js/logoutmain.js") }}" type="text/javascript" charset="utf-8"></script>
36 <script src="{{static_url("auth/js/logoutmain.js") }}" type="text/javascript" charset="utf-8"></script>
37
37
38 {% endblock %}
38 {% endblock %}
@@ -1,352 +1,352 b''
1 {% extends "page.html" %}
1 {% extends "page.html" %}
2
2
3 {% block stylesheet %}
3 {% block stylesheet %}
4
4
5 {% if mathjax_url %}
5 {% if mathjax_url %}
6 <script type="text/javascript" src="{{mathjax_url}}?config=TeX-AMS_HTML-full&delayStartupUntil=configured" charset="utf-8"></script>
6 <script type="text/javascript" src="{{mathjax_url}}?config=TeX-AMS_HTML-full&delayStartupUntil=configured" charset="utf-8"></script>
7 {% endif %}
7 {% endif %}
8 <script type="text/javascript">
8 <script type="text/javascript">
9 // MathJax disabled, set as null to distingish from *missing* MathJax,
9 // MathJax disabled, set as null to distingish from *missing* MathJax,
10 // where it will be undefined, and should prompt a dialog later.
10 // where it will be undefined, and should prompt a dialog later.
11 window.mathjax_url = "{{mathjax_url}}";
11 window.mathjax_url = "{{mathjax_url}}";
12 </script>
12 </script>
13
13
14 <link rel="stylesheet" href="{{ static_url("components/codemirror/lib/codemirror.css") }}">
14 <link rel="stylesheet" href="{{ static_url("components/codemirror/lib/codemirror.css") }}">
15
15
16 {{super()}}
16 {{super()}}
17
17
18 <link rel="stylesheet" href="{{ static_url("notebook/css/override.css") }}" type="text/css" />
18 <link rel="stylesheet" href="{{ static_url("notebook/css/override.css") }}" type="text/css" />
19
19
20 {% endblock %}
20 {% endblock %}
21
21
22 {% block params %}
22 {% block params %}
23
23
24 data-project="{{project}}"
24 data-project="{{project}}"
25 data-base-project-url="{{base_project_url}}"
25 data-base-url="{{base_url}}"
26 data-base-kernel-url="{{base_kernel_url}}"
26 data-base-kernel-url="{{base_kernel_url}}"
27 data-notebook-name="{{notebook_name}}"
27 data-notebook-name="{{notebook_name}}"
28 data-notebook-path="{{notebook_path}}"
28 data-notebook-path="{{notebook_path}}"
29 class="notebook_app"
29 class="notebook_app"
30
30
31 {% endblock %}
31 {% endblock %}
32
32
33
33
34 {% block header %}
34 {% block header %}
35
35
36 <span id="save_widget" class="nav pull-left">
36 <span id="save_widget" class="nav pull-left">
37 <span id="notebook_name"></span>
37 <span id="notebook_name"></span>
38 <span id="checkpoint_status"></span>
38 <span id="checkpoint_status"></span>
39 <span id="autosave_status"></span>
39 <span id="autosave_status"></span>
40 </span>
40 </span>
41
41
42 {% endblock %}
42 {% endblock %}
43
43
44
44
45 {% block site %}
45 {% block site %}
46
46
47 <div id="menubar-container" class="container">
47 <div id="menubar-container" class="container">
48 <div id="menubar">
48 <div id="menubar">
49 <div class="navbar">
49 <div class="navbar">
50 <div class="navbar-inner">
50 <div class="navbar-inner">
51 <div class="container">
51 <div class="container">
52 <ul id="menus" class="nav">
52 <ul id="menus" class="nav">
53 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">File</a>
53 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">File</a>
54 <ul id="file_menu" class="dropdown-menu">
54 <ul id="file_menu" class="dropdown-menu">
55 <li id="new_notebook"
55 <li id="new_notebook"
56 title="Make a new notebook (Opens a new window)">
56 title="Make a new notebook (Opens a new window)">
57 <a href="#">New</a></li>
57 <a href="#">New</a></li>
58 <li id="open_notebook"
58 <li id="open_notebook"
59 title="Opens a new window with the Dashboard view">
59 title="Opens a new window with the Dashboard view">
60 <a href="#">Open...</a></li>
60 <a href="#">Open...</a></li>
61 <!-- <hr/> -->
61 <!-- <hr/> -->
62 <li class="divider"></li>
62 <li class="divider"></li>
63 <li id="copy_notebook"
63 <li id="copy_notebook"
64 title="Open a copy of this notebook's contents and start a new kernel">
64 title="Open a copy of this notebook's contents and start a new kernel">
65 <a href="#">Make a Copy...</a></li>
65 <a href="#">Make a Copy...</a></li>
66 <li id="rename_notebook"><a href="#">Rename...</a></li>
66 <li id="rename_notebook"><a href="#">Rename...</a></li>
67 <li id="save_checkpoint"><a href="#">Save and Checkpoint</a></li>
67 <li id="save_checkpoint"><a href="#">Save and Checkpoint</a></li>
68 <!-- <hr/> -->
68 <!-- <hr/> -->
69 <li class="divider"></li>
69 <li class="divider"></li>
70 <li id="restore_checkpoint" class="dropdown-submenu"><a href="#">Revert to Checkpoint</a>
70 <li id="restore_checkpoint" class="dropdown-submenu"><a href="#">Revert to Checkpoint</a>
71 <ul class="dropdown-menu">
71 <ul class="dropdown-menu">
72 <li><a href="#"></a></li>
72 <li><a href="#"></a></li>
73 <li><a href="#"></a></li>
73 <li><a href="#"></a></li>
74 <li><a href="#"></a></li>
74 <li><a href="#"></a></li>
75 <li><a href="#"></a></li>
75 <li><a href="#"></a></li>
76 <li><a href="#"></a></li>
76 <li><a href="#"></a></li>
77 </ul>
77 </ul>
78 </li>
78 </li>
79 <li class="divider"></li>
79 <li class="divider"></li>
80 <li id="print_preview"><a href="#">Print Preview</a></li>
80 <li id="print_preview"><a href="#">Print Preview</a></li>
81 <li class="dropdown-submenu"><a href="#">Download as</a>
81 <li class="dropdown-submenu"><a href="#">Download as</a>
82 <ul class="dropdown-menu">
82 <ul class="dropdown-menu">
83 <li id="download_ipynb"><a href="#">IPython Notebook (.ipynb)</a></li>
83 <li id="download_ipynb"><a href="#">IPython Notebook (.ipynb)</a></li>
84 <li id="download_py"><a href="#">Python (.py)</a></li>
84 <li id="download_py"><a href="#">Python (.py)</a></li>
85 <li id="download_html"><a href="#">HTML (.html)</a></li>
85 <li id="download_html"><a href="#">HTML (.html)</a></li>
86 <li id="download_rst"><a href="#">reST (.rst)</a></li>
86 <li id="download_rst"><a href="#">reST (.rst)</a></li>
87 </ul>
87 </ul>
88 </li>
88 </li>
89 <li class="divider"></li>
89 <li class="divider"></li>
90
90
91 <li id="kill_and_exit"
91 <li id="kill_and_exit"
92 title="Shutdown this notebook's kernel, and close this window">
92 title="Shutdown this notebook's kernel, and close this window">
93 <a href="#" >Close and halt</a></li>
93 <a href="#" >Close and halt</a></li>
94 </ul>
94 </ul>
95 </li>
95 </li>
96 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Edit</a>
96 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Edit</a>
97 <ul id="edit_menu" class="dropdown-menu">
97 <ul id="edit_menu" class="dropdown-menu">
98 <li id="cut_cell"><a href="#">Cut Cell</a></li>
98 <li id="cut_cell"><a href="#">Cut Cell</a></li>
99 <li id="copy_cell"><a href="#">Copy Cell</a></li>
99 <li id="copy_cell"><a href="#">Copy Cell</a></li>
100 <li id="paste_cell_above" class="disabled"><a href="#">Paste Cell Above</a></li>
100 <li id="paste_cell_above" class="disabled"><a href="#">Paste Cell Above</a></li>
101 <li id="paste_cell_below" class="disabled"><a href="#">Paste Cell Below</a></li>
101 <li id="paste_cell_below" class="disabled"><a href="#">Paste Cell Below</a></li>
102 <li id="paste_cell_replace" class="disabled"><a href="#">Paste Cell &amp; Replace</a></li>
102 <li id="paste_cell_replace" class="disabled"><a href="#">Paste Cell &amp; Replace</a></li>
103 <li id="delete_cell"><a href="#">Delete Cell</a></li>
103 <li id="delete_cell"><a href="#">Delete Cell</a></li>
104 <li id="undelete_cell" class="disabled"><a href="#">Undo Delete Cell</a></li>
104 <li id="undelete_cell" class="disabled"><a href="#">Undo Delete Cell</a></li>
105 <li class="divider"></li>
105 <li class="divider"></li>
106 <li id="split_cell"><a href="#">Split Cell</a></li>
106 <li id="split_cell"><a href="#">Split Cell</a></li>
107 <li id="merge_cell_above"><a href="#">Merge Cell Above</a></li>
107 <li id="merge_cell_above"><a href="#">Merge Cell Above</a></li>
108 <li id="merge_cell_below"><a href="#">Merge Cell Below</a></li>
108 <li id="merge_cell_below"><a href="#">Merge Cell Below</a></li>
109 <li class="divider"></li>
109 <li class="divider"></li>
110 <li id="move_cell_up"><a href="#">Move Cell Up</a></li>
110 <li id="move_cell_up"><a href="#">Move Cell Up</a></li>
111 <li id="move_cell_down"><a href="#">Move Cell Down</a></li>
111 <li id="move_cell_down"><a href="#">Move Cell Down</a></li>
112 <li class="divider"></li>
112 <li class="divider"></li>
113 <li id="edit_nb_metadata"><a href="#">Edit Notebook Metadata</a></li>
113 <li id="edit_nb_metadata"><a href="#">Edit Notebook Metadata</a></li>
114 </ul>
114 </ul>
115 </li>
115 </li>
116 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">View</a>
116 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">View</a>
117 <ul id="view_menu" class="dropdown-menu">
117 <ul id="view_menu" class="dropdown-menu">
118 <li id="toggle_header"
118 <li id="toggle_header"
119 title="Show/Hide the IPython Notebook logo and notebook title (above menu bar)">
119 title="Show/Hide the IPython Notebook logo and notebook title (above menu bar)">
120 <a href="#">Toggle Header</a></li>
120 <a href="#">Toggle Header</a></li>
121 <li id="toggle_toolbar"
121 <li id="toggle_toolbar"
122 title="Show/Hide the action icons (below menu bar)">
122 title="Show/Hide the action icons (below menu bar)">
123 <a href="#">Toggle Toolbar</a></li>
123 <a href="#">Toggle Toolbar</a></li>
124 </ul>
124 </ul>
125 </li>
125 </li>
126 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Insert</a>
126 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Insert</a>
127 <ul id="insert_menu" class="dropdown-menu">
127 <ul id="insert_menu" class="dropdown-menu">
128 <li id="insert_cell_above"
128 <li id="insert_cell_above"
129 title="Insert an empty Code cell above the currently active cell">
129 title="Insert an empty Code cell above the currently active cell">
130 <a href="#">Insert Cell Above</a></li>
130 <a href="#">Insert Cell Above</a></li>
131 <li id="insert_cell_below"
131 <li id="insert_cell_below"
132 title="Insert an empty Code cell below the currently active cell">
132 title="Insert an empty Code cell below the currently active cell">
133 <a href="#">Insert Cell Below</a></li>
133 <a href="#">Insert Cell Below</a></li>
134 </ul>
134 </ul>
135 </li>
135 </li>
136 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Cell</a>
136 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Cell</a>
137 <ul id="cell_menu" class="dropdown-menu">
137 <ul id="cell_menu" class="dropdown-menu">
138 <li id="run_cell" title="Run this cell, and move cursor to the next one">
138 <li id="run_cell" title="Run this cell, and move cursor to the next one">
139 <a href="#">Run</a></li>
139 <a href="#">Run</a></li>
140 <li id="run_cell_select_below" title="Run this cell, select below">
140 <li id="run_cell_select_below" title="Run this cell, select below">
141 <a href="#">Run and Select Below</a></li>
141 <a href="#">Run and Select Below</a></li>
142 <li id="run_cell_insert_below" title="Run this cell, insert below">
142 <li id="run_cell_insert_below" title="Run this cell, insert below">
143 <a href="#">Run and Insert Below</a></li>
143 <a href="#">Run and Insert Below</a></li>
144 <li id="run_all_cells" title="Run all cells in the notebook">
144 <li id="run_all_cells" title="Run all cells in the notebook">
145 <a href="#">Run All</a></li>
145 <a href="#">Run All</a></li>
146 <li id="run_all_cells_above" title="Run all cells above (but not including) this cell">
146 <li id="run_all_cells_above" title="Run all cells above (but not including) this cell">
147 <a href="#">Run All Above</a></li>
147 <a href="#">Run All Above</a></li>
148 <li id="run_all_cells_below" title="Run this cell and all cells below it">
148 <li id="run_all_cells_below" title="Run this cell and all cells below it">
149 <a href="#">Run All Below</a></li>
149 <a href="#">Run All Below</a></li>
150 <li class="divider"></li>
150 <li class="divider"></li>
151 <li id="change_cell_type" class="dropdown-submenu"
151 <li id="change_cell_type" class="dropdown-submenu"
152 title="All cells in the notebook have a cell type. By default, new cells are created as 'Code' cells">
152 title="All cells in the notebook have a cell type. By default, new cells are created as 'Code' cells">
153 <a href="#">Cell Type</a>
153 <a href="#">Cell Type</a>
154 <ul class="dropdown-menu">
154 <ul class="dropdown-menu">
155 <li id="to_code"
155 <li id="to_code"
156 title="Contents will be sent to the kernel for execution, and output will display in the footer of cell">
156 title="Contents will be sent to the kernel for execution, and output will display in the footer of cell">
157 <a href="#">Code</a></li>
157 <a href="#">Code</a></li>
158 <li id="to_markdown"
158 <li id="to_markdown"
159 title="Contents will be rendered as HTML and serve as explanatory text">
159 title="Contents will be rendered as HTML and serve as explanatory text">
160 <a href="#">Markdown</a></li>
160 <a href="#">Markdown</a></li>
161 <li id="to_raw"
161 <li id="to_raw"
162 title="Contents will pass through nbconvert unmodified">
162 title="Contents will pass through nbconvert unmodified">
163 <a href="#">Raw NBConvert</a></li>
163 <a href="#">Raw NBConvert</a></li>
164 <li id="to_heading1"><a href="#">Heading 1</a></li>
164 <li id="to_heading1"><a href="#">Heading 1</a></li>
165 <li id="to_heading2"><a href="#">Heading 2</a></li>
165 <li id="to_heading2"><a href="#">Heading 2</a></li>
166 <li id="to_heading3"><a href="#">Heading 3</a></li>
166 <li id="to_heading3"><a href="#">Heading 3</a></li>
167 <li id="to_heading4"><a href="#">Heading 4</a></li>
167 <li id="to_heading4"><a href="#">Heading 4</a></li>
168 <li id="to_heading5"><a href="#">Heading 5</a></li>
168 <li id="to_heading5"><a href="#">Heading 5</a></li>
169 <li id="to_heading6"><a href="#">Heading 6</a></li>
169 <li id="to_heading6"><a href="#">Heading 6</a></li>
170 </ul>
170 </ul>
171 </li>
171 </li>
172 <li class="divider"></li>
172 <li class="divider"></li>
173 <li id="current_outputs" class="dropdown-submenu"><a href="#">Current Output</a>
173 <li id="current_outputs" class="dropdown-submenu"><a href="#">Current Output</a>
174 <ul class="dropdown-menu">
174 <ul class="dropdown-menu">
175 <li id="toggle_current_output"
175 <li id="toggle_current_output"
176 title="Hide/Show the output of the current cell">
176 title="Hide/Show the output of the current cell">
177 <a href="#">Toggle</a>
177 <a href="#">Toggle</a>
178 </li>
178 </li>
179 <li id="toggle_current_output_scroll"
179 <li id="toggle_current_output_scroll"
180 title="Scroll the output of the current cell">
180 title="Scroll the output of the current cell">
181 <a href="#">Toggle Scrolling</a>
181 <a href="#">Toggle Scrolling</a>
182 </li>
182 </li>
183 <li id="clear_current_output"
183 <li id="clear_current_output"
184 title="Clear the output of the current cell">
184 title="Clear the output of the current cell">
185 <a href="#">Clear</a>
185 <a href="#">Clear</a>
186 </li>
186 </li>
187 </ul>
187 </ul>
188 </li>
188 </li>
189 <li id="all_outputs" class="dropdown-submenu"><a href="#">All Output</a>
189 <li id="all_outputs" class="dropdown-submenu"><a href="#">All Output</a>
190 <ul class="dropdown-menu">
190 <ul class="dropdown-menu">
191 <li id="toggle_all_output"
191 <li id="toggle_all_output"
192 title="Hide/Show the output of all cells">
192 title="Hide/Show the output of all cells">
193 <a href="#">Toggle</a>
193 <a href="#">Toggle</a>
194 </li>
194 </li>
195 <li id="toggle_all_output_scroll"
195 <li id="toggle_all_output_scroll"
196 title="Scroll the output of all cells">
196 title="Scroll the output of all cells">
197 <a href="#">Toggle Scrolling</a>
197 <a href="#">Toggle Scrolling</a>
198 </li>
198 </li>
199 <li id="clear_all_output"
199 <li id="clear_all_output"
200 title="Clear the output of all cells">
200 title="Clear the output of all cells">
201 <a href="#">Clear</a>
201 <a href="#">Clear</a>
202 </li>
202 </li>
203 </ul>
203 </ul>
204 </li>
204 </li>
205 </ul>
205 </ul>
206 </li>
206 </li>
207 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Kernel</a>
207 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Kernel</a>
208 <ul id="kernel_menu" class="dropdown-menu">
208 <ul id="kernel_menu" class="dropdown-menu">
209 <li id="int_kernel"
209 <li id="int_kernel"
210 title="Send KeyboardInterrupt (CTRL-C) to the Kernel">
210 title="Send KeyboardInterrupt (CTRL-C) to the Kernel">
211 <a href="#">Interrupt</a></li>
211 <a href="#">Interrupt</a></li>
212 <li id="restart_kernel"
212 <li id="restart_kernel"
213 title="Restart the Kernel">
213 title="Restart the Kernel">
214 <a href="#">Restart</a></li>
214 <a href="#">Restart</a></li>
215 </ul>
215 </ul>
216 </li>
216 </li>
217 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Help</a>
217 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Help</a>
218 <ul id="help_menu" class="dropdown-menu">
218 <ul id="help_menu" class="dropdown-menu">
219 <li id="keyboard_shortcuts" title="Opens a tooltip with all keyboard shortcuts"><a href="#">Keyboard Shortcuts</a></li>
219 <li id="keyboard_shortcuts" title="Opens a tooltip with all keyboard shortcuts"><a href="#">Keyboard Shortcuts</a></li>
220 <li class="divider"></li>
220 <li class="divider"></li>
221 {% set
221 {% set
222 sections = (
222 sections = (
223 (
223 (
224 ("http://ipython.org/documentation.html","IPython Help",True),
224 ("http://ipython.org/documentation.html","IPython Help",True),
225 ("http://nbviewer.ipython.org/github/ipython/ipython/tree/master/examples/notebooks/", "Notebook Examples", True),
225 ("http://nbviewer.ipython.org/github/ipython/ipython/tree/master/examples/notebooks/", "Notebook Examples", True),
226 ("http://ipython.org/ipython-doc/stable/interactive/notebook.html","Notebook Help",True),
226 ("http://ipython.org/ipython-doc/stable/interactive/notebook.html","Notebook Help",True),
227 ("http://ipython.org/ipython-doc/dev/interactive/cm_keyboard.html","Editor Shortcuts",True),
227 ("http://ipython.org/ipython-doc/dev/interactive/cm_keyboard.html","Editor Shortcuts",True),
228 ),(
228 ),(
229 ("http://docs.python.org","Python",True),
229 ("http://docs.python.org","Python",True),
230 ("http://docs.scipy.org/doc/numpy/reference/","NumPy",True),
230 ("http://docs.scipy.org/doc/numpy/reference/","NumPy",True),
231 ("http://docs.scipy.org/doc/scipy/reference/","SciPy",True),
231 ("http://docs.scipy.org/doc/scipy/reference/","SciPy",True),
232 ("http://matplotlib.org/contents.html","Matplotlib",True),
232 ("http://matplotlib.org/contents.html","Matplotlib",True),
233 ("http://docs.sympy.org/dev/index.html","SymPy",True),
233 ("http://docs.sympy.org/dev/index.html","SymPy",True),
234 ("http://pandas.pydata.org/pandas-docs/stable/","pandas", True)
234 ("http://pandas.pydata.org/pandas-docs/stable/","pandas", True)
235 )
235 )
236 )
236 )
237 %}
237 %}
238
238
239 {% for helplinks in sections %}
239 {% for helplinks in sections %}
240 {% for link in helplinks %}
240 {% for link in helplinks %}
241 <li><a href="{{link[0]}}" {{'target="_blank" title="Opens in a new window"' if link[2]}}>
241 <li><a href="{{link[0]}}" {{'target="_blank" title="Opens in a new window"' if link[2]}}>
242 {{'<i class="icon-external-link menu-icon pull-right"></i>' if link[2]}}
242 {{'<i class="icon-external-link menu-icon pull-right"></i>' if link[2]}}
243 {{link[1]}}
243 {{link[1]}}
244 </a></li>
244 </a></li>
245 {% endfor %}
245 {% endfor %}
246 {% if not loop.last %}
246 {% if not loop.last %}
247 <li class="divider"></li>
247 <li class="divider"></li>
248 {% endif %}
248 {% endif %}
249 {% endfor %}
249 {% endfor %}
250 </li>
250 </li>
251 </ul>
251 </ul>
252 </li>
252 </li>
253 </ul>
253 </ul>
254 <div id="kernel_indicator" class="indicator_area pull-right">
254 <div id="kernel_indicator" class="indicator_area pull-right">
255 <i id="kernel_indicator_icon"></i>
255 <i id="kernel_indicator_icon"></i>
256 </div>
256 </div>
257 <div id="modal_indicator" class="indicator_area pull-right">
257 <div id="modal_indicator" class="indicator_area pull-right">
258 <i id="modal_indicator_icon"></i>
258 <i id="modal_indicator_icon"></i>
259 </div>
259 </div>
260 <div id="notification_area"></div>
260 <div id="notification_area"></div>
261 </div>
261 </div>
262 </div>
262 </div>
263 </div>
263 </div>
264 </div>
264 </div>
265 <div id="maintoolbar" class="navbar">
265 <div id="maintoolbar" class="navbar">
266 <div class="toolbar-inner navbar-inner navbar-nobg">
266 <div class="toolbar-inner navbar-inner navbar-nobg">
267 <div id="maintoolbar-container" class="container"></div>
267 <div id="maintoolbar-container" class="container"></div>
268 </div>
268 </div>
269 </div>
269 </div>
270 </div>
270 </div>
271
271
272 <div id="ipython-main-app">
272 <div id="ipython-main-app">
273
273
274 <div id="notebook_panel">
274 <div id="notebook_panel">
275 <div id="notebook"></div>
275 <div id="notebook"></div>
276 <div id="pager_splitter"></div>
276 <div id="pager_splitter"></div>
277 <div id="pager">
277 <div id="pager">
278 <div id='pager_button_area'>
278 <div id='pager_button_area'>
279 </div>
279 </div>
280 <div id="pager-container" class="container"></div>
280 <div id="pager-container" class="container"></div>
281 </div>
281 </div>
282 </div>
282 </div>
283
283
284 </div>
284 </div>
285 <div id='tooltip' class='ipython_tooltip' style='display:none'></div>
285 <div id='tooltip' class='ipython_tooltip' style='display:none'></div>
286
286
287
287
288 {% endblock %}
288 {% endblock %}
289
289
290
290
291 {% block script %}
291 {% block script %}
292
292
293 {{super()}}
293 {{super()}}
294
294
295 <script src="{{ static_url("components/codemirror/lib/codemirror.js") }}" charset="utf-8"></script>
295 <script src="{{ static_url("components/codemirror/lib/codemirror.js") }}" charset="utf-8"></script>
296 <script type="text/javascript">
296 <script type="text/javascript">
297 CodeMirror.modeURL = "{{ static_url("components/codemirror/mode/%N/%N.js") }}";
297 CodeMirror.modeURL = "{{ static_url("components/codemirror/mode/%N/%N.js") }}";
298 </script>
298 </script>
299 <script src="{{ static_url("components/codemirror/addon/mode/loadmode.js") }}" charset="utf-8"></script>
299 <script src="{{ static_url("components/codemirror/addon/mode/loadmode.js") }}" charset="utf-8"></script>
300 <script src="{{ static_url("components/codemirror/addon/mode/multiplex.js") }}" charset="utf-8"></script>
300 <script src="{{ static_url("components/codemirror/addon/mode/multiplex.js") }}" charset="utf-8"></script>
301 <script src="{{ static_url("components/codemirror/addon/mode/overlay.js") }}" charset="utf-8"></script>
301 <script src="{{ static_url("components/codemirror/addon/mode/overlay.js") }}" charset="utf-8"></script>
302 <script src="{{ static_url("components/codemirror/addon/edit/matchbrackets.js") }}" charset="utf-8"></script>
302 <script src="{{ static_url("components/codemirror/addon/edit/matchbrackets.js") }}" charset="utf-8"></script>
303 <script src="{{ static_url("components/codemirror/addon/comment/comment.js") }}" charset="utf-8"></script>
303 <script src="{{ static_url("components/codemirror/addon/comment/comment.js") }}" charset="utf-8"></script>
304 <script src="{{ static_url("components/codemirror/mode/htmlmixed/htmlmixed.js") }}" charset="utf-8"></script>
304 <script src="{{ static_url("components/codemirror/mode/htmlmixed/htmlmixed.js") }}" charset="utf-8"></script>
305 <script src="{{ static_url("components/codemirror/mode/xml/xml.js") }}" charset="utf-8"></script>
305 <script src="{{ static_url("components/codemirror/mode/xml/xml.js") }}" charset="utf-8"></script>
306 <script src="{{ static_url("components/codemirror/mode/javascript/javascript.js") }}" charset="utf-8"></script>
306 <script src="{{ static_url("components/codemirror/mode/javascript/javascript.js") }}" charset="utf-8"></script>
307 <script src="{{ static_url("components/codemirror/mode/css/css.js") }}" charset="utf-8"></script>
307 <script src="{{ static_url("components/codemirror/mode/css/css.js") }}" charset="utf-8"></script>
308 <script src="{{ static_url("components/codemirror/mode/rst/rst.js") }}" charset="utf-8"></script>
308 <script src="{{ static_url("components/codemirror/mode/rst/rst.js") }}" charset="utf-8"></script>
309 <script src="{{ static_url("components/codemirror/mode/markdown/markdown.js") }}" charset="utf-8"></script>
309 <script src="{{ static_url("components/codemirror/mode/markdown/markdown.js") }}" charset="utf-8"></script>
310 <script src="{{ static_url("components/codemirror/mode/gfm/gfm.js") }}" charset="utf-8"></script>
310 <script src="{{ static_url("components/codemirror/mode/gfm/gfm.js") }}" charset="utf-8"></script>
311 <script src="{{ static_url("components/codemirror/mode/python/python.js") }}" charset="utf-8"></script>
311 <script src="{{ static_url("components/codemirror/mode/python/python.js") }}" charset="utf-8"></script>
312 <script src="{{ static_url("notebook/js/codemirror-ipython.js") }}" charset="utf-8"></script>
312 <script src="{{ static_url("notebook/js/codemirror-ipython.js") }}" charset="utf-8"></script>
313
313
314 <script src="{{ static_url("components/highlight.js/build/highlight.pack.js") }}" charset="utf-8"></script>
314 <script src="{{ static_url("components/highlight.js/build/highlight.pack.js") }}" charset="utf-8"></script>
315
315
316 <script src="{{ static_url("dateformat/date.format.js") }}" charset="utf-8"></script>
316 <script src="{{ static_url("dateformat/date.format.js") }}" charset="utf-8"></script>
317
317
318 <script src="{{ static_url("base/js/events.js") }}" type="text/javascript" charset="utf-8"></script>
318 <script src="{{ static_url("base/js/events.js") }}" type="text/javascript" charset="utf-8"></script>
319 <script src="{{ static_url("base/js/utils.js") }}" type="text/javascript" charset="utf-8"></script>
319 <script src="{{ static_url("base/js/utils.js") }}" type="text/javascript" charset="utf-8"></script>
320 <script src="{{ static_url("base/js/dialog.js") }}" type="text/javascript" charset="utf-8"></script>
320 <script src="{{ static_url("base/js/dialog.js") }}" type="text/javascript" charset="utf-8"></script>
321 <script src="{{ static_url("services/kernels/js/kernel.js") }}" type="text/javascript" charset="utf-8"></script>
321 <script src="{{ static_url("services/kernels/js/kernel.js") }}" type="text/javascript" charset="utf-8"></script>
322 <script src="{{ static_url("services/kernels/js/comm.js") }}" type="text/javascript" charset="utf-8"></script>
322 <script src="{{ static_url("services/kernels/js/comm.js") }}" type="text/javascript" charset="utf-8"></script>
323 <script src="{{ static_url("services/sessions/js/session.js") }}" type="text/javascript" charset="utf-8"></script>
323 <script src="{{ static_url("services/sessions/js/session.js") }}" type="text/javascript" charset="utf-8"></script>
324 <script src="{{ static_url("notebook/js/layoutmanager.js") }}" type="text/javascript" charset="utf-8"></script>
324 <script src="{{ static_url("notebook/js/layoutmanager.js") }}" type="text/javascript" charset="utf-8"></script>
325 <script src="{{ static_url("notebook/js/mathjaxutils.js") }}" type="text/javascript" charset="utf-8"></script>
325 <script src="{{ static_url("notebook/js/mathjaxutils.js") }}" type="text/javascript" charset="utf-8"></script>
326 <script src="{{ static_url("notebook/js/outputarea.js") }}" type="text/javascript" charset="utf-8"></script>
326 <script src="{{ static_url("notebook/js/outputarea.js") }}" type="text/javascript" charset="utf-8"></script>
327 <script src="{{ static_url("notebook/js/cell.js") }}" type="text/javascript" charset="utf-8"></script>
327 <script src="{{ static_url("notebook/js/cell.js") }}" type="text/javascript" charset="utf-8"></script>
328 <script src="{{ static_url("notebook/js/celltoolbar.js") }}" type="text/javascript" charset="utf-8"></script>
328 <script src="{{ static_url("notebook/js/celltoolbar.js") }}" type="text/javascript" charset="utf-8"></script>
329 <script src="{{ static_url("notebook/js/codecell.js") }}" type="text/javascript" charset="utf-8"></script>
329 <script src="{{ static_url("notebook/js/codecell.js") }}" type="text/javascript" charset="utf-8"></script>
330 <script src="{{ static_url("notebook/js/completer.js") }}" type="text/javascript" charset="utf-8"></script>
330 <script src="{{ static_url("notebook/js/completer.js") }}" type="text/javascript" charset="utf-8"></script>
331 <script src="{{ static_url("notebook/js/textcell.js") }}" type="text/javascript" charset="utf-8"></script>
331 <script src="{{ static_url("notebook/js/textcell.js") }}" type="text/javascript" charset="utf-8"></script>
332 <script src="{{ static_url("notebook/js/savewidget.js") }}" type="text/javascript" charset="utf-8"></script>
332 <script src="{{ static_url("notebook/js/savewidget.js") }}" type="text/javascript" charset="utf-8"></script>
333 <script src="{{ static_url("notebook/js/quickhelp.js") }}" type="text/javascript" charset="utf-8"></script>
333 <script src="{{ static_url("notebook/js/quickhelp.js") }}" type="text/javascript" charset="utf-8"></script>
334 <script src="{{ static_url("notebook/js/pager.js") }}" type="text/javascript" charset="utf-8"></script>
334 <script src="{{ static_url("notebook/js/pager.js") }}" type="text/javascript" charset="utf-8"></script>
335 <script src="{{ static_url("notebook/js/menubar.js") }}" type="text/javascript" charset="utf-8"></script>
335 <script src="{{ static_url("notebook/js/menubar.js") }}" type="text/javascript" charset="utf-8"></script>
336 <script src="{{ static_url("notebook/js/toolbar.js") }}" type="text/javascript" charset="utf-8"></script>
336 <script src="{{ static_url("notebook/js/toolbar.js") }}" type="text/javascript" charset="utf-8"></script>
337 <script src="{{ static_url("notebook/js/maintoolbar.js") }}" type="text/javascript" charset="utf-8"></script>
337 <script src="{{ static_url("notebook/js/maintoolbar.js") }}" type="text/javascript" charset="utf-8"></script>
338 <script src="{{ static_url("notebook/js/notebook.js") }}" type="text/javascript" charset="utf-8"></script>
338 <script src="{{ static_url("notebook/js/notebook.js") }}" type="text/javascript" charset="utf-8"></script>
339 <script src="{{ static_url("notebook/js/keyboardmanager.js") }}" type="text/javascript" charset="utf-8"></script>
339 <script src="{{ static_url("notebook/js/keyboardmanager.js") }}" type="text/javascript" charset="utf-8"></script>
340 <script src="{{ static_url("notebook/js/notificationwidget.js") }}" type="text/javascript" charset="utf-8"></script>
340 <script src="{{ static_url("notebook/js/notificationwidget.js") }}" type="text/javascript" charset="utf-8"></script>
341 <script src="{{ static_url("notebook/js/notificationarea.js") }}" type="text/javascript" charset="utf-8"></script>
341 <script src="{{ static_url("notebook/js/notificationarea.js") }}" type="text/javascript" charset="utf-8"></script>
342 <script src="{{ static_url("notebook/js/tooltip.js") }}" type="text/javascript" charset="utf-8"></script>
342 <script src="{{ static_url("notebook/js/tooltip.js") }}" type="text/javascript" charset="utf-8"></script>
343 <script src="{{ static_url("notebook/js/config.js") }}" type="text/javascript" charset="utf-8"></script>
343 <script src="{{ static_url("notebook/js/config.js") }}" type="text/javascript" charset="utf-8"></script>
344 <script src="{{ static_url("notebook/js/main.js") }}" type="text/javascript" charset="utf-8"></script>
344 <script src="{{ static_url("notebook/js/main.js") }}" type="text/javascript" charset="utf-8"></script>
345
345
346 <script src="{{ static_url("notebook/js/contexthint.js") }}" charset="utf-8"></script>
346 <script src="{{ static_url("notebook/js/contexthint.js") }}" charset="utf-8"></script>
347
347
348 <script src="{{ static_url("notebook/js/celltoolbarpresets/default.js") }}" type="text/javascript" charset="utf-8"></script>
348 <script src="{{ static_url("notebook/js/celltoolbarpresets/default.js") }}" type="text/javascript" charset="utf-8"></script>
349 <script src="{{ static_url("notebook/js/celltoolbarpresets/rawcell.js") }}" type="text/javascript" charset="utf-8"></script>
349 <script src="{{ static_url("notebook/js/celltoolbarpresets/rawcell.js") }}" type="text/javascript" charset="utf-8"></script>
350 <script src="{{ static_url("notebook/js/celltoolbarpresets/slideshow.js") }}" type="text/javascript" charset="utf-8"></script>
350 <script src="{{ static_url("notebook/js/celltoolbarpresets/slideshow.js") }}" type="text/javascript" charset="utf-8"></script>
351
351
352 {% endblock %}
352 {% endblock %}
@@ -1,96 +1,96 b''
1
1
2
2
3 <!DOCTYPE HTML>
3 <!DOCTYPE HTML>
4 <html>
4 <html>
5
5
6 <head>
6 <head>
7 <meta charset="utf-8">
7 <meta charset="utf-8">
8
8
9 <title>{% block title %}IPython Notebook{% endblock %}</title>
9 <title>{% block title %}IPython Notebook{% endblock %}</title>
10 <link rel="shortcut icon" type="image/x-icon" href="{{static_url("base/images/favicon.ico") }}">
10 <link rel="shortcut icon" type="image/x-icon" href="{{static_url("base/images/favicon.ico") }}">
11 <meta http-equiv="X-UA-Compatible" content="chrome=1">
11 <meta http-equiv="X-UA-Compatible" content="chrome=1">
12 <link rel="stylesheet" href="{{static_url("components/jquery-ui/themes/smoothness/jquery-ui.min.css") }}" type="text/css" />
12 <link rel="stylesheet" href="{{static_url("components/jquery-ui/themes/smoothness/jquery-ui.min.css") }}" type="text/css" />
13 <meta name="viewport" content="width=device-width, initial-scale=1.0">
13 <meta name="viewport" content="width=device-width, initial-scale=1.0">
14
14
15 {% block stylesheet %}
15 {% block stylesheet %}
16 <link rel="stylesheet" href="{{ static_url("style/style.min.css") }}" type="text/css"/>
16 <link rel="stylesheet" href="{{ static_url("style/style.min.css") }}" type="text/css"/>
17 {% endblock %}
17 {% endblock %}
18 <link rel="stylesheet" href="{{ static_url("custom/custom.css") }}" type="text/css" />
18 <link rel="stylesheet" href="{{ static_url("custom/custom.css") }}" type="text/css" />
19 <script src="{{static_url("components/requirejs/require.js") }}" type="text/javascript" charset="utf-8"></script>
19 <script src="{{static_url("components/requirejs/require.js") }}" type="text/javascript" charset="utf-8"></script>
20 <script>
20 <script>
21 require.config({
21 require.config({
22 baseUrl: '{{static_url("")}}',
22 baseUrl: '{{static_url("")}}',
23 paths: {
23 paths: {
24 nbextensions : '{{ base_project_url }}nbextensions',
24 nbextensions : '{{ base_url }}nbextensions',
25 underscore : '{{static_url("components/underscore/underscore-min")}}',
25 underscore : '{{static_url("components/underscore/underscore-min")}}',
26 backbone : '{{static_url("components/backbone/backbone-min")}}',
26 backbone : '{{static_url("components/backbone/backbone-min")}}',
27 },
27 },
28 shim: {
28 shim: {
29 underscore: {
29 underscore: {
30 exports: '_'
30 exports: '_'
31 },
31 },
32 backbone: {
32 backbone: {
33 deps: ["underscore", "jquery"],
33 deps: ["underscore", "jquery"],
34 exports: "Backbone"
34 exports: "Backbone"
35 }
35 }
36 }
36 }
37 });
37 });
38 </script>
38 </script>
39
39
40 {% block meta %}
40 {% block meta %}
41 {% endblock %}
41 {% endblock %}
42
42
43 </head>
43 </head>
44
44
45 <body {% block params %}{% endblock %}>
45 <body {% block params %}{% endblock %}>
46
46
47 <noscript>
47 <noscript>
48 <div id='noscript'>
48 <div id='noscript'>
49 IPython Notebook requires JavaScript.<br>
49 IPython Notebook requires JavaScript.<br>
50 Please enable it to proceed.
50 Please enable it to proceed.
51 </div>
51 </div>
52 </noscript>
52 </noscript>
53
53
54 <div id="header" class="navbar navbar-static-top">
54 <div id="header" class="navbar navbar-static-top">
55 <div class="navbar-inner navbar-nobg">
55 <div class="navbar-inner navbar-nobg">
56 <div class="container">
56 <div class="container">
57 <div id="ipython_notebook" class="nav brand pull-left"><a href="{{base_project_url}}tree/{{notebook_path}}" alt='dashboard'><img src='{{static_url("base/images/ipynblogo.png") }}' alt='IPython Notebook'/></a></div>
57 <div id="ipython_notebook" class="nav brand pull-left"><a href="{{base_url}}tree/{{notebook_path}}" alt='dashboard'><img src='{{static_url("base/images/ipynblogo.png") }}' alt='IPython Notebook'/></a></div>
58
58
59 {% block login_widget %}
59 {% block login_widget %}
60
60
61 <span id="login_widget">
61 <span id="login_widget">
62 {% if logged_in %}
62 {% if logged_in %}
63 <button id="logout">Logout</button>
63 <button id="logout">Logout</button>
64 {% elif login_available and not logged_in %}
64 {% elif login_available and not logged_in %}
65 <button id="login">Login</button>
65 <button id="login">Login</button>
66 {% endif %}
66 {% endif %}
67 </span>
67 </span>
68
68
69 {% endblock %}
69 {% endblock %}
70
70
71 {% block header %}
71 {% block header %}
72 {% endblock %}
72 {% endblock %}
73 </div>
73 </div>
74 </div>
74 </div>
75 </div>
75 </div>
76
76
77 <div id="site">
77 <div id="site">
78 {% block site %}
78 {% block site %}
79 {% endblock %}
79 {% endblock %}
80 </div>
80 </div>
81
81
82 <script src="{{static_url("components/jquery/jquery.min.js") }}" type="text/javascript" charset="utf-8"></script>
82 <script src="{{static_url("components/jquery/jquery.min.js") }}" type="text/javascript" charset="utf-8"></script>
83 <script src="{{static_url("components/jquery-ui/ui/minified/jquery-ui.min.js") }}" type="text/javascript" charset="utf-8"></script>
83 <script src="{{static_url("components/jquery-ui/ui/minified/jquery-ui.min.js") }}" type="text/javascript" charset="utf-8"></script>
84 <script src="{{static_url("components/bootstrap/bootstrap/js/bootstrap.min.js") }}" type="text/javascript" charset="utf-8"></script>
84 <script src="{{static_url("components/bootstrap/bootstrap/js/bootstrap.min.js") }}" type="text/javascript" charset="utf-8"></script>
85 <script src="{{static_url("base/js/namespace.js") }}" type="text/javascript" charset="utf-8"></script>
85 <script src="{{static_url("base/js/namespace.js") }}" type="text/javascript" charset="utf-8"></script>
86 <script src="{{static_url("base/js/page.js") }}" type="text/javascript" charset="utf-8"></script>
86 <script src="{{static_url("base/js/page.js") }}" type="text/javascript" charset="utf-8"></script>
87 <script src="{{static_url("auth/js/loginwidget.js") }}" type="text/javascript" charset="utf-8"></script>
87 <script src="{{static_url("auth/js/loginwidget.js") }}" type="text/javascript" charset="utf-8"></script>
88
88
89 {% block script %}
89 {% block script %}
90 {% endblock %}
90 {% endblock %}
91
91
92 <script src="{{static_url("custom/custom.js") }}" type="text/javascript" charset="utf-8"></script>
92 <script src="{{static_url("custom/custom.js") }}" type="text/javascript" charset="utf-8"></script>
93
93
94 </body>
94 </body>
95
95
96 </html>
96 </html>
@@ -1,99 +1,99 b''
1 {% extends "page.html" %}
1 {% extends "page.html" %}
2
2
3 {% block title %}{{page_title}}{% endblock %}
3 {% block title %}{{page_title}}{% endblock %}
4
4
5
5
6 {% block stylesheet %}
6 {% block stylesheet %}
7 {{super()}}
7 {{super()}}
8 <link rel="stylesheet" href="{{ static_url("tree/css/override.css") }}" type="text/css" />
8 <link rel="stylesheet" href="{{ static_url("tree/css/override.css") }}" type="text/css" />
9 {% endblock %}
9 {% endblock %}
10
10
11 {% block params %}
11 {% block params %}
12
12
13 data-project="{{project}}"
13 data-project="{{project}}"
14 data-base-project-url="{{base_project_url}}"
14 data-base-url="{{base_url}}"
15 data-notebook-path="{{notebook_path}}"
15 data-notebook-path="{{notebook_path}}"
16 data-base-kernel-url="{{base_kernel_url}}"
16 data-base-kernel-url="{{base_kernel_url}}"
17
17
18 {% endblock %}
18 {% endblock %}
19
19
20
20
21 {% block site %}
21 {% block site %}
22
22
23 <div id="ipython-main-app" class="container">
23 <div id="ipython-main-app" class="container">
24
24
25 <div id="tab_content" class="tabbable">
25 <div id="tab_content" class="tabbable">
26 <ul id="tabs" class="nav nav-tabs">
26 <ul id="tabs" class="nav nav-tabs">
27 <li class="active"><a href="#notebooks" data-toggle="tab">Notebooks</a></li>
27 <li class="active"><a href="#notebooks" data-toggle="tab">Notebooks</a></li>
28 <li><a href="#clusters" data-toggle="tab">Clusters</a></li>
28 <li><a href="#clusters" data-toggle="tab">Clusters</a></li>
29 </ul>
29 </ul>
30
30
31 <div class="tab-content">
31 <div class="tab-content">
32 <div id="notebooks" class="tab-pane active">
32 <div id="notebooks" class="tab-pane active">
33 <div id="notebook_toolbar" class="row-fluid">
33 <div id="notebook_toolbar" class="row-fluid">
34 <div class="span8">
34 <div class="span8">
35 <form id='alternate_upload' class='alternate_upload' >
35 <form id='alternate_upload' class='alternate_upload' >
36 <span id="drag_info" style="position:absolute" >
36 <span id="drag_info" style="position:absolute" >
37 To import a notebook, drag the file onto the listing below or <strong>click here</strong>.
37 To import a notebook, drag the file onto the listing below or <strong>click here</strong>.
38 </span>
38 </span>
39 <input type="file" name="datafile" class="fileinput" multiple='multiple'>
39 <input type="file" name="datafile" class="fileinput" multiple='multiple'>
40 </form>
40 </form>
41 </div>
41 </div>
42 <div class="span4 clearfix">
42 <div class="span4 clearfix">
43 <span id="notebook_buttons" class="pull-right">
43 <span id="notebook_buttons" class="pull-right">
44 <button id="new_notebook" title="Create new notebook" class="btn btn-small">New Notebook</button>
44 <button id="new_notebook" title="Create new notebook" class="btn btn-small">New Notebook</button>
45 <button id="refresh_notebook_list" title="Refresh notebook list" class="btn btn-small"><i class="icon-refresh"></i></button>
45 <button id="refresh_notebook_list" title="Refresh notebook list" class="btn btn-small"><i class="icon-refresh"></i></button>
46 </span>
46 </span>
47 </div>
47 </div>
48 </div>
48 </div>
49
49
50 <div id="notebook_list">
50 <div id="notebook_list">
51 <div id="notebook_list_header" class="row-fluid list_header">
51 <div id="notebook_list_header" class="row-fluid list_header">
52 <div id="project_name">
52 <div id="project_name">
53 <ul class="breadcrumb">
53 <ul class="breadcrumb">
54 <li><a href="{{breadcrumbs[0][0]}}"><i class="icon-home"></i></a><span>/</span></li>
54 <li><a href="{{breadcrumbs[0][0]}}"><i class="icon-home"></i></a><span>/</span></li>
55 {% for crumb in breadcrumbs[1:] %}
55 {% for crumb in breadcrumbs[1:] %}
56 <li><a href="{{crumb[0]}}">{{crumb[1]}}</a> <span>/</span></li>
56 <li><a href="{{crumb[0]}}">{{crumb[1]}}</a> <span>/</span></li>
57 {% endfor %}
57 {% endfor %}
58 </ul>
58 </ul>
59 </div>
59 </div>
60 </div>
60 </div>
61 </div>
61 </div>
62 </div>
62 </div>
63
63
64 <div id="clusters" class="tab-pane">
64 <div id="clusters" class="tab-pane">
65
65
66 <div id="cluster_toolbar" class="row-fluid">
66 <div id="cluster_toolbar" class="row-fluid">
67 <div class="span8">
67 <div class="span8">
68 <span id="cluster_list_info">IPython parallel computing clusters</span>
68 <span id="cluster_list_info">IPython parallel computing clusters</span>
69 </div>
69 </div>
70 <div class="span4" class="clearfix">
70 <div class="span4" class="clearfix">
71 <span id="cluster_buttons" class="pull-right">
71 <span id="cluster_buttons" class="pull-right">
72 <button id="refresh_cluster_list" title="Refresh cluster list" class="btn btn-small"><i class="icon-refresh"></i></button>
72 <button id="refresh_cluster_list" title="Refresh cluster list" class="btn btn-small"><i class="icon-refresh"></i></button>
73 </span>
73 </span>
74 </div>
74 </div>
75 </div>
75 </div>
76
76
77 <div id="cluster_list">
77 <div id="cluster_list">
78 <div id="cluster_list_header" class="row-fluid list_header">
78 <div id="cluster_list_header" class="row-fluid list_header">
79 <div class="profile_col span4">profile</div>
79 <div class="profile_col span4">profile</div>
80 <div class="status_col span3">status</div>
80 <div class="status_col span3">status</div>
81 <div class="engines_col span3" title="Enter the number of engines to start or empty for default"># of engines</div>
81 <div class="engines_col span3" title="Enter the number of engines to start or empty for default"># of engines</div>
82 <div class="action_col span2">action</div>
82 <div class="action_col span2">action</div>
83 </div>
83 </div>
84 </div>
84 </div>
85 </div>
85 </div>
86 </div>
86 </div>
87
87
88 </div>
88 </div>
89
89
90 {% endblock %}
90 {% endblock %}
91
91
92 {% block script %}
92 {% block script %}
93 {{super()}}
93 {{super()}}
94 <script src="{{ static_url("base/js/utils.js") }}" type="text/javascript" charset="utf-8"></script>
94 <script src="{{ static_url("base/js/utils.js") }}" type="text/javascript" charset="utf-8"></script>
95 <script src="{{static_url("base/js/dialog.js") }}" type="text/javascript" charset="utf-8"></script>
95 <script src="{{static_url("base/js/dialog.js") }}" type="text/javascript" charset="utf-8"></script>
96 <script src="{{static_url("tree/js/notebooklist.js") }}" type="text/javascript" charset="utf-8"></script>
96 <script src="{{static_url("tree/js/notebooklist.js") }}" type="text/javascript" charset="utf-8"></script>
97 <script src="{{static_url("tree/js/clusterlist.js") }}" type="text/javascript" charset="utf-8"></script>
97 <script src="{{static_url("tree/js/clusterlist.js") }}" type="text/javascript" charset="utf-8"></script>
98 <script src="{{static_url("tree/js/main.js") }}" type="text/javascript" charset="utf-8"></script>
98 <script src="{{static_url("tree/js/main.js") }}" type="text/javascript" charset="utf-8"></script>
99 {% endblock %}
99 {% endblock %}
@@ -1,37 +1,40 b''
1
1
2
2
3 casper.get_list_items = function () {
3 casper.get_list_items = function () {
4 return this.evaluate(function () {
4 return this.evaluate(function () {
5 return $.makeArray($('.item_link').map(function () {
5 return $.makeArray($('.item_link').map(function () {
6 return {
6 return {
7 link: $(this).attr('href'),
7 link: $(this).attr('href'),
8 label: $(this).find('.item_name').text()
8 label: $(this).find('.item_name').text()
9 }
9 }
10 }));
10 }));
11 });
11 });
12 }
12 }
13
13
14 casper.test_items = function (baseUrl) {
14 casper.test_items = function (baseUrl) {
15 casper.then(function () {
15 casper.then(function () {
16 var items = casper.get_list_items();
16 var items = casper.get_list_items();
17 casper.each(items, function (self, item) {
17 casper.each(items, function (self, item) {
18 if (!item.label.match('.ipynb$')) {
18 if (!item.label.match('.ipynb$')) {
19 var followed_url = baseUrl+item.link;
19 var followed_url = baseUrl+item.link;
20 if (!followed_url.match('/\.\.$')) {
20 if (!followed_url.match('/\.\.$')) {
21 casper.thenOpen(baseUrl+item.link, function () {
21 casper.thenOpen(followed_url, function () {
22 casper.wait_for_dashboard();
22 casper.wait_for_dashboard();
23 this.test.assertEquals(this.getCurrentUrl(), followed_url, 'Testing dashboard link: '+followed_url);
23 // getCurrentUrl is with host, and url-decoded,
24 // but item.link is without host, and url-encoded
25 var expected = baseUrl + decodeURIComponent(item.link);
26 this.test.assertEquals(this.getCurrentUrl(), expected, 'Testing dashboard link: ' + expected);
24 casper.test_items(baseUrl);
27 casper.test_items(baseUrl);
25 this.back();
28 this.back();
26 });
29 });
27 }
30 }
28 }
31 }
29 });
32 });
30 });
33 });
31 }
34 }
32
35
33 casper.dashboard_test(function () {
36 casper.dashboard_test(function () {
34 baseUrl = this.get_notebook_server()
37 baseUrl = this.get_notebook_server();
35 casper.test_items(baseUrl);
38 casper.test_items(baseUrl);
36 })
39 })
37
40
@@ -1,100 +1,100 b''
1 """Tornado handlers for the tree view.
1 """Tornado handlers for the tree view.
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2011 The IPython Development Team
9 # Copyright (C) 2011 The IPython Development Team
10 #
10 #
11 # Distributed under the terms of the BSD License. The full license is in
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18 import os
18 import os
19
19
20 from tornado import web
20 from tornado import web
21 from ..base.handlers import IPythonHandler, notebook_path_regex, path_regex
21 from ..base.handlers import IPythonHandler, notebook_path_regex, path_regex
22 from ..utils import url_path_join, path2url, url2path, url_escape, is_hidden
22 from ..utils import url_path_join, path2url, url2path, url_escape, is_hidden
23
23
24 #-----------------------------------------------------------------------------
24 #-----------------------------------------------------------------------------
25 # Handlers
25 # Handlers
26 #-----------------------------------------------------------------------------
26 #-----------------------------------------------------------------------------
27
27
28
28
29 class TreeHandler(IPythonHandler):
29 class TreeHandler(IPythonHandler):
30 """Render the tree view, listing notebooks, clusters, etc."""
30 """Render the tree view, listing notebooks, clusters, etc."""
31
31
32 def generate_breadcrumbs(self, path):
32 def generate_breadcrumbs(self, path):
33 breadcrumbs = [(url_escape(url_path_join(self.base_project_url, 'tree')), '')]
33 breadcrumbs = [(url_escape(url_path_join(self.base_url, 'tree')), '')]
34 comps = path.split('/')
34 comps = path.split('/')
35 ncomps = len(comps)
35 ncomps = len(comps)
36 for i in range(ncomps):
36 for i in range(ncomps):
37 if comps[i]:
37 if comps[i]:
38 link = url_escape(url_path_join(self.base_project_url, 'tree', *comps[0:i+1]))
38 link = url_escape(url_path_join(self.base_url, 'tree', *comps[0:i+1]))
39 breadcrumbs.append((link, comps[i]))
39 breadcrumbs.append((link, comps[i]))
40 return breadcrumbs
40 return breadcrumbs
41
41
42 def generate_page_title(self, path):
42 def generate_page_title(self, path):
43 comps = path.split('/')
43 comps = path.split('/')
44 if len(comps) > 3:
44 if len(comps) > 3:
45 for i in range(len(comps)-2):
45 for i in range(len(comps)-2):
46 comps.pop(0)
46 comps.pop(0)
47 page_title = url_escape(url_path_join(*comps))
47 page_title = url_escape(url_path_join(*comps))
48 if page_title:
48 if page_title:
49 return page_title+'/'
49 return page_title+'/'
50 else:
50 else:
51 return 'Home'
51 return 'Home'
52
52
53 @web.authenticated
53 @web.authenticated
54 def get(self, path='', name=None):
54 def get(self, path='', name=None):
55 path = path.strip('/')
55 path = path.strip('/')
56 nbm = self.notebook_manager
56 nbm = self.notebook_manager
57 if name is not None:
57 if name is not None:
58 # is a notebook, redirect to notebook handler
58 # is a notebook, redirect to notebook handler
59 url = url_escape(url_path_join(
59 url = url_escape(url_path_join(
60 self.base_project_url, 'notebooks', path, name
60 self.base_url, 'notebooks', path, name
61 ))
61 ))
62 self.log.debug("Redirecting %s to %s", self.request.path, url)
62 self.log.debug("Redirecting %s to %s", self.request.path, url)
63 self.redirect(url)
63 self.redirect(url)
64 else:
64 else:
65 if not nbm.path_exists(path=path) or nbm.is_hidden(path):
65 if not nbm.path_exists(path=path) or nbm.is_hidden(path):
66 # Directory is hidden or does not exist.
66 # Directory is hidden or does not exist.
67 raise web.HTTPError(404)
67 raise web.HTTPError(404)
68 breadcrumbs = self.generate_breadcrumbs(path)
68 breadcrumbs = self.generate_breadcrumbs(path)
69 page_title = self.generate_page_title(path)
69 page_title = self.generate_page_title(path)
70 self.write(self.render_template('tree.html',
70 self.write(self.render_template('tree.html',
71 project=self.project_dir,
71 project=self.project_dir,
72 page_title=page_title,
72 page_title=page_title,
73 notebook_path=path,
73 notebook_path=path,
74 breadcrumbs=breadcrumbs
74 breadcrumbs=breadcrumbs
75 ))
75 ))
76
76
77
77
78 class TreeRedirectHandler(IPythonHandler):
78 class TreeRedirectHandler(IPythonHandler):
79 """Redirect a request to the corresponding tree URL"""
79 """Redirect a request to the corresponding tree URL"""
80
80
81 @web.authenticated
81 @web.authenticated
82 def get(self, path=''):
82 def get(self, path=''):
83 url = url_escape(url_path_join(
83 url = url_escape(url_path_join(
84 self.base_project_url, 'tree', path.strip('/')
84 self.base_url, 'tree', path.strip('/')
85 ))
85 ))
86 self.log.debug("Redirecting %s to %s", self.request.path, url)
86 self.log.debug("Redirecting %s to %s", self.request.path, url)
87 self.redirect(url)
87 self.redirect(url)
88
88
89
89
90 #-----------------------------------------------------------------------------
90 #-----------------------------------------------------------------------------
91 # URL to handler mappings
91 # URL to handler mappings
92 #-----------------------------------------------------------------------------
92 #-----------------------------------------------------------------------------
93
93
94
94
95 default_handlers = [
95 default_handlers = [
96 (r"/tree%s" % notebook_path_regex, TreeHandler),
96 (r"/tree%s" % notebook_path_regex, TreeHandler),
97 (r"/tree%s" % path_regex, TreeHandler),
97 (r"/tree%s" % path_regex, TreeHandler),
98 (r"/tree", TreeHandler),
98 (r"/tree", TreeHandler),
99 (r"/", TreeRedirectHandler),
99 (r"/", TreeRedirectHandler),
100 ]
100 ]
@@ -1,520 +1,520 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """IPython Test Process Controller
2 """IPython Test Process Controller
3
3
4 This module runs one or more subprocesses which will actually run the IPython
4 This module runs one or more subprocesses which will actually run the IPython
5 test suite.
5 test suite.
6
6
7 """
7 """
8
8
9 #-----------------------------------------------------------------------------
9 #-----------------------------------------------------------------------------
10 # Copyright (C) 2009-2011 The IPython Development Team
10 # Copyright (C) 2009-2011 The IPython Development Team
11 #
11 #
12 # Distributed under the terms of the BSD License. The full license is in
12 # Distributed under the terms of the BSD License. The full license is in
13 # the file COPYING, distributed as part of this software.
13 # the file COPYING, distributed as part of this software.
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15
15
16 #-----------------------------------------------------------------------------
16 #-----------------------------------------------------------------------------
17 # Imports
17 # Imports
18 #-----------------------------------------------------------------------------
18 #-----------------------------------------------------------------------------
19 from __future__ import print_function
19 from __future__ import print_function
20
20
21 import argparse
21 import argparse
22 import multiprocessing.pool
22 import multiprocessing.pool
23 from multiprocessing import Process, Queue
23 from multiprocessing import Process, Queue
24 import os
24 import os
25 import shutil
25 import shutil
26 import signal
26 import signal
27 import sys
27 import sys
28 import subprocess
28 import subprocess
29 import time
29 import time
30
30
31 from .iptest import have, test_group_names as py_test_group_names, test_sections
31 from .iptest import have, test_group_names as py_test_group_names, test_sections
32 from IPython.utils.path import compress_user
32 from IPython.utils.path import compress_user
33 from IPython.utils.py3compat import bytes_to_str
33 from IPython.utils.py3compat import bytes_to_str
34 from IPython.utils.sysinfo import get_sys_info
34 from IPython.utils.sysinfo import get_sys_info
35 from IPython.utils.tempdir import TemporaryDirectory
35 from IPython.utils.tempdir import TemporaryDirectory
36
36
37
37
38 class TestController(object):
38 class TestController(object):
39 """Run tests in a subprocess
39 """Run tests in a subprocess
40 """
40 """
41 #: str, IPython test suite to be executed.
41 #: str, IPython test suite to be executed.
42 section = None
42 section = None
43 #: list, command line arguments to be executed
43 #: list, command line arguments to be executed
44 cmd = None
44 cmd = None
45 #: dict, extra environment variables to set for the subprocess
45 #: dict, extra environment variables to set for the subprocess
46 env = None
46 env = None
47 #: list, TemporaryDirectory instances to clear up when the process finishes
47 #: list, TemporaryDirectory instances to clear up when the process finishes
48 dirs = None
48 dirs = None
49 #: subprocess.Popen instance
49 #: subprocess.Popen instance
50 process = None
50 process = None
51 #: str, process stdout+stderr
51 #: str, process stdout+stderr
52 stdout = None
52 stdout = None
53 #: bool, whether to capture process stdout & stderr
53 #: bool, whether to capture process stdout & stderr
54 buffer_output = False
54 buffer_output = False
55
55
56 def __init__(self):
56 def __init__(self):
57 self.cmd = []
57 self.cmd = []
58 self.env = {}
58 self.env = {}
59 self.dirs = []
59 self.dirs = []
60
60
61 def launch(self):
61 def launch(self):
62 # print('*** ENV:', self.env) # dbg
62 # print('*** ENV:', self.env) # dbg
63 # print('*** CMD:', self.cmd) # dbg
63 # print('*** CMD:', self.cmd) # dbg
64 env = os.environ.copy()
64 env = os.environ.copy()
65 env.update(self.env)
65 env.update(self.env)
66 output = subprocess.PIPE if self.buffer_output else None
66 output = subprocess.PIPE if self.buffer_output else None
67 stdout = subprocess.STDOUT if self.buffer_output else None
67 stdout = subprocess.STDOUT if self.buffer_output else None
68 self.process = subprocess.Popen(self.cmd, stdout=output,
68 self.process = subprocess.Popen(self.cmd, stdout=output,
69 stderr=stdout, env=env)
69 stderr=stdout, env=env)
70
70
71 def wait(self):
71 def wait(self):
72 self.stdout, _ = self.process.communicate()
72 self.stdout, _ = self.process.communicate()
73 return self.process.returncode
73 return self.process.returncode
74
74
75 def cleanup_process(self):
75 def cleanup_process(self):
76 """Cleanup on exit by killing any leftover processes."""
76 """Cleanup on exit by killing any leftover processes."""
77 subp = self.process
77 subp = self.process
78 if subp is None or (subp.poll() is not None):
78 if subp is None or (subp.poll() is not None):
79 return # Process doesn't exist, or is already dead.
79 return # Process doesn't exist, or is already dead.
80
80
81 try:
81 try:
82 print('Cleaning up stale PID: %d' % subp.pid)
82 print('Cleaning up stale PID: %d' % subp.pid)
83 subp.kill()
83 subp.kill()
84 except: # (OSError, WindowsError) ?
84 except: # (OSError, WindowsError) ?
85 # This is just a best effort, if we fail or the process was
85 # This is just a best effort, if we fail or the process was
86 # really gone, ignore it.
86 # really gone, ignore it.
87 pass
87 pass
88 else:
88 else:
89 for i in range(10):
89 for i in range(10):
90 if subp.poll() is None:
90 if subp.poll() is None:
91 time.sleep(0.1)
91 time.sleep(0.1)
92 else:
92 else:
93 break
93 break
94
94
95 if subp.poll() is None:
95 if subp.poll() is None:
96 # The process did not die...
96 # The process did not die...
97 print('... failed. Manual cleanup may be required.')
97 print('... failed. Manual cleanup may be required.')
98
98
99 def cleanup(self):
99 def cleanup(self):
100 "Kill process if it's still alive, and clean up temporary directories"
100 "Kill process if it's still alive, and clean up temporary directories"
101 self.cleanup_process()
101 self.cleanup_process()
102 for td in self.dirs:
102 for td in self.dirs:
103 td.cleanup()
103 td.cleanup()
104
104
105 __del__ = cleanup
105 __del__ = cleanup
106
106
107 class PyTestController(TestController):
107 class PyTestController(TestController):
108 """Run Python tests using IPython.testing.iptest"""
108 """Run Python tests using IPython.testing.iptest"""
109 #: str, Python command to execute in subprocess
109 #: str, Python command to execute in subprocess
110 pycmd = None
110 pycmd = None
111
111
112 def __init__(self, section):
112 def __init__(self, section):
113 """Create new test runner."""
113 """Create new test runner."""
114 TestController.__init__(self)
114 TestController.__init__(self)
115 self.section = section
115 self.section = section
116 # pycmd is put into cmd[2] in PyTestController.launch()
116 # pycmd is put into cmd[2] in PyTestController.launch()
117 self.cmd = [sys.executable, '-c', None, section]
117 self.cmd = [sys.executable, '-c', None, section]
118 self.pycmd = "from IPython.testing.iptest import run_iptest; run_iptest()"
118 self.pycmd = "from IPython.testing.iptest import run_iptest; run_iptest()"
119 ipydir = TemporaryDirectory()
119 ipydir = TemporaryDirectory()
120 self.dirs.append(ipydir)
120 self.dirs.append(ipydir)
121 self.env['IPYTHONDIR'] = ipydir.name
121 self.env['IPYTHONDIR'] = ipydir.name
122 self.workingdir = workingdir = TemporaryDirectory()
122 self.workingdir = workingdir = TemporaryDirectory()
123 self.dirs.append(workingdir)
123 self.dirs.append(workingdir)
124 self.env['IPTEST_WORKING_DIR'] = workingdir.name
124 self.env['IPTEST_WORKING_DIR'] = workingdir.name
125 # This means we won't get odd effects from our own matplotlib config
125 # This means we won't get odd effects from our own matplotlib config
126 self.env['MPLCONFIGDIR'] = workingdir.name
126 self.env['MPLCONFIGDIR'] = workingdir.name
127
127
128 @property
128 @property
129 def will_run(self):
129 def will_run(self):
130 try:
130 try:
131 return test_sections[self.section].will_run
131 return test_sections[self.section].will_run
132 except KeyError:
132 except KeyError:
133 return True
133 return True
134
134
135 def add_xunit(self):
135 def add_xunit(self):
136 xunit_file = os.path.abspath(self.section + '.xunit.xml')
136 xunit_file = os.path.abspath(self.section + '.xunit.xml')
137 self.cmd.extend(['--with-xunit', '--xunit-file', xunit_file])
137 self.cmd.extend(['--with-xunit', '--xunit-file', xunit_file])
138
138
139 def add_coverage(self):
139 def add_coverage(self):
140 try:
140 try:
141 sources = test_sections[self.section].includes
141 sources = test_sections[self.section].includes
142 except KeyError:
142 except KeyError:
143 sources = ['IPython']
143 sources = ['IPython']
144
144
145 coverage_rc = ("[run]\n"
145 coverage_rc = ("[run]\n"
146 "data_file = {data_file}\n"
146 "data_file = {data_file}\n"
147 "source =\n"
147 "source =\n"
148 " {source}\n"
148 " {source}\n"
149 ).format(data_file=os.path.abspath('.coverage.'+self.section),
149 ).format(data_file=os.path.abspath('.coverage.'+self.section),
150 source="\n ".join(sources))
150 source="\n ".join(sources))
151 config_file = os.path.join(self.workingdir.name, '.coveragerc')
151 config_file = os.path.join(self.workingdir.name, '.coveragerc')
152 with open(config_file, 'w') as f:
152 with open(config_file, 'w') as f:
153 f.write(coverage_rc)
153 f.write(coverage_rc)
154
154
155 self.env['COVERAGE_PROCESS_START'] = config_file
155 self.env['COVERAGE_PROCESS_START'] = config_file
156 self.pycmd = "import coverage; coverage.process_startup(); " + self.pycmd
156 self.pycmd = "import coverage; coverage.process_startup(); " + self.pycmd
157
157
158 def launch(self):
158 def launch(self):
159 self.cmd[2] = self.pycmd
159 self.cmd[2] = self.pycmd
160 super(PyTestController, self).launch()
160 super(PyTestController, self).launch()
161
161
162 js_prefix = 'js/'
162 js_prefix = 'js/'
163
163
164 def get_js_test_dir():
164 def get_js_test_dir():
165 import IPython.html.tests as t
165 import IPython.html.tests as t
166 return os.path.join(os.path.dirname(t.__file__), '')
166 return os.path.join(os.path.dirname(t.__file__), '')
167
167
168 def all_js_groups():
168 def all_js_groups():
169 import glob
169 import glob
170 test_dir = get_js_test_dir()
170 test_dir = get_js_test_dir()
171 all_subdirs = glob.glob(test_dir + '*/')
171 all_subdirs = glob.glob(test_dir + '*/')
172 return [js_prefix+os.path.relpath(x, test_dir) for x in all_subdirs if os.path.relpath(x, test_dir) != '__pycache__']
172 return [js_prefix+os.path.relpath(x, test_dir) for x in all_subdirs if os.path.relpath(x, test_dir) != '__pycache__']
173
173
174 class JSController(TestController):
174 class JSController(TestController):
175 """Run CasperJS tests """
175 """Run CasperJS tests """
176 def __init__(self, section):
176 def __init__(self, section):
177 """Create new test runner."""
177 """Create new test runner."""
178 TestController.__init__(self)
178 TestController.__init__(self)
179 self.section = section
179 self.section = section
180
180
181 self.ipydir = TemporaryDirectory()
181 self.ipydir = TemporaryDirectory()
182 self.nbdir = TemporaryDirectory()
182 self.nbdir = TemporaryDirectory()
183 print("Running notebook tests in directory: %r" % self.nbdir.name)
183 print("Running notebook tests in directory: %r" % self.nbdir.name)
184 os.makedirs(os.path.join(self.nbdir.name, os.path.join('subdir1', 'subdir1a')))
184 os.makedirs(os.path.join(self.nbdir.name, os.path.join(u'sub βˆ‚ir1', u'sub βˆ‚ir 1a')))
185 os.makedirs(os.path.join(self.nbdir.name, os.path.join('subdir2', 'subdir2a')))
185 os.makedirs(os.path.join(self.nbdir.name, os.path.join(u'sub βˆ‚ir2', u'sub βˆ‚ir 1b')))
186 self.dirs.append(self.ipydir)
186 self.dirs.append(self.ipydir)
187 self.dirs.append(self.nbdir)
187 self.dirs.append(self.nbdir)
188
188
189 def launch(self):
189 def launch(self):
190 # start the ipython notebook, so we get the port number
190 # start the ipython notebook, so we get the port number
191 self._init_server()
191 self._init_server()
192 js_test_dir = get_js_test_dir()
192 js_test_dir = get_js_test_dir()
193 includes = '--includes=' + os.path.join(js_test_dir,'util.js')
193 includes = '--includes=' + os.path.join(js_test_dir,'util.js')
194 test_cases = os.path.join(js_test_dir, self.section[len(js_prefix):])
194 test_cases = os.path.join(js_test_dir, self.section[len(js_prefix):])
195 port = '--port=' + str(self.server_port)
195 port = '--port=' + str(self.server_port)
196 self.cmd = ['casperjs', 'test', port, includes, test_cases]
196 self.cmd = ['casperjs', 'test', port, includes, test_cases]
197 super(JSController, self).launch()
197 super(JSController, self).launch()
198
198
199 @property
199 @property
200 def will_run(self):
200 def will_run(self):
201 return all(have[a] for a in ['zmq', 'tornado', 'jinja2', 'casperjs'])
201 return all(have[a] for a in ['zmq', 'tornado', 'jinja2', 'casperjs'])
202
202
203 def _init_server(self):
203 def _init_server(self):
204 "Start the notebook server in a separate process"
204 "Start the notebook server in a separate process"
205 self.queue = q = Queue()
205 self.queue = q = Queue()
206 self.server = Process(target=run_webapp, args=(q, self.ipydir.name, self.nbdir.name))
206 self.server = Process(target=run_webapp, args=(q, self.ipydir.name, self.nbdir.name))
207 self.server.start()
207 self.server.start()
208 self.server_port = q.get()
208 self.server_port = q.get()
209
209
210 def cleanup(self):
210 def cleanup(self):
211 self.server.terminate()
211 self.server.terminate()
212 self.server.join()
212 self.server.join()
213 TestController.cleanup(self)
213 TestController.cleanup(self)
214
214
215 def run_webapp(q, ipydir, nbdir, loglevel=0):
215 def run_webapp(q, ipydir, nbdir, loglevel=0):
216 """start the IPython Notebook, and pass port back to the queue"""
216 """start the IPython Notebook, and pass port back to the queue"""
217 import os
217 import os
218 import IPython.html.notebookapp as nbapp
218 import IPython.html.notebookapp as nbapp
219 import sys
219 import sys
220 sys.stderr = open(os.devnull, 'w')
220 sys.stderr = open(os.devnull, 'w')
221 server = nbapp.NotebookApp()
221 server = nbapp.NotebookApp()
222 args = ['--no-browser']
222 args = ['--no-browser']
223 args.extend(['--ipython-dir', ipydir])
223 args.extend(['--ipython-dir', ipydir])
224 args.extend(['--notebook-dir', nbdir])
224 args.extend(['--notebook-dir', nbdir])
225 args.extend(['--log-level', str(loglevel)])
225 args.extend(['--log-level', str(loglevel)])
226 server.initialize(args)
226 server.initialize(args)
227 # communicate the port number to the parent process
227 # communicate the port number to the parent process
228 q.put(server.port)
228 q.put(server.port)
229 server.start()
229 server.start()
230
230
231 def prepare_controllers(options):
231 def prepare_controllers(options):
232 """Returns two lists of TestController instances, those to run, and those
232 """Returns two lists of TestController instances, those to run, and those
233 not to run."""
233 not to run."""
234 testgroups = options.testgroups
234 testgroups = options.testgroups
235
235
236 if testgroups:
236 if testgroups:
237 py_testgroups = [g for g in testgroups if (g in py_test_group_names) \
237 py_testgroups = [g for g in testgroups if (g in py_test_group_names) \
238 or g.startswith('IPython.')]
238 or g.startswith('IPython.')]
239 if 'js' in testgroups:
239 if 'js' in testgroups:
240 js_testgroups = all_js_groups()
240 js_testgroups = all_js_groups()
241 else:
241 else:
242 js_testgroups = [g for g in testgroups if g not in py_testgroups]
242 js_testgroups = [g for g in testgroups if g not in py_testgroups]
243 else:
243 else:
244 py_testgroups = py_test_group_names
244 py_testgroups = py_test_group_names
245 js_testgroups = all_js_groups()
245 js_testgroups = all_js_groups()
246 if not options.all:
246 if not options.all:
247 test_sections['parallel'].enabled = False
247 test_sections['parallel'].enabled = False
248
248
249 c_js = [JSController(name) for name in js_testgroups]
249 c_js = [JSController(name) for name in js_testgroups]
250 c_py = [PyTestController(name) for name in py_testgroups]
250 c_py = [PyTestController(name) for name in py_testgroups]
251
251
252 configure_py_controllers(c_py, xunit=options.xunit,
252 configure_py_controllers(c_py, xunit=options.xunit,
253 coverage=options.coverage, subproc_streams=options.subproc_streams,
253 coverage=options.coverage, subproc_streams=options.subproc_streams,
254 extra_args=options.extra_args)
254 extra_args=options.extra_args)
255
255
256 controllers = c_py + c_js
256 controllers = c_py + c_js
257 to_run = [c for c in controllers if c.will_run]
257 to_run = [c for c in controllers if c.will_run]
258 not_run = [c for c in controllers if not c.will_run]
258 not_run = [c for c in controllers if not c.will_run]
259 return to_run, not_run
259 return to_run, not_run
260
260
261 def configure_py_controllers(controllers, xunit=False, coverage=False,
261 def configure_py_controllers(controllers, xunit=False, coverage=False,
262 subproc_streams='capture', extra_args=()):
262 subproc_streams='capture', extra_args=()):
263 """Apply options for a collection of TestController objects."""
263 """Apply options for a collection of TestController objects."""
264 for controller in controllers:
264 for controller in controllers:
265 if xunit:
265 if xunit:
266 controller.add_xunit()
266 controller.add_xunit()
267 if coverage:
267 if coverage:
268 controller.add_coverage()
268 controller.add_coverage()
269 controller.env['IPTEST_SUBPROC_STREAMS'] = subproc_streams
269 controller.env['IPTEST_SUBPROC_STREAMS'] = subproc_streams
270 controller.cmd.extend(extra_args)
270 controller.cmd.extend(extra_args)
271
271
272 def do_run(controller):
272 def do_run(controller):
273 try:
273 try:
274 try:
274 try:
275 controller.launch()
275 controller.launch()
276 except Exception:
276 except Exception:
277 import traceback
277 import traceback
278 traceback.print_exc()
278 traceback.print_exc()
279 return controller, 1 # signal failure
279 return controller, 1 # signal failure
280
280
281 exitcode = controller.wait()
281 exitcode = controller.wait()
282 return controller, exitcode
282 return controller, exitcode
283
283
284 except KeyboardInterrupt:
284 except KeyboardInterrupt:
285 return controller, -signal.SIGINT
285 return controller, -signal.SIGINT
286 finally:
286 finally:
287 controller.cleanup()
287 controller.cleanup()
288
288
289 def report():
289 def report():
290 """Return a string with a summary report of test-related variables."""
290 """Return a string with a summary report of test-related variables."""
291 inf = get_sys_info()
291 inf = get_sys_info()
292 out = []
292 out = []
293 def _add(name, value):
293 def _add(name, value):
294 out.append((name, value))
294 out.append((name, value))
295
295
296 _add('IPython version', inf['ipython_version'])
296 _add('IPython version', inf['ipython_version'])
297 _add('IPython commit', "{} ({})".format(inf['commit_hash'], inf['commit_source']))
297 _add('IPython commit', "{} ({})".format(inf['commit_hash'], inf['commit_source']))
298 _add('IPython package', compress_user(inf['ipython_path']))
298 _add('IPython package', compress_user(inf['ipython_path']))
299 _add('Python version', inf['sys_version'].replace('\n',''))
299 _add('Python version', inf['sys_version'].replace('\n',''))
300 _add('sys.executable', compress_user(inf['sys_executable']))
300 _add('sys.executable', compress_user(inf['sys_executable']))
301 _add('Platform', inf['platform'])
301 _add('Platform', inf['platform'])
302
302
303 width = max(len(n) for (n,v) in out)
303 width = max(len(n) for (n,v) in out)
304 out = ["{:<{width}}: {}\n".format(n, v, width=width) for (n,v) in out]
304 out = ["{:<{width}}: {}\n".format(n, v, width=width) for (n,v) in out]
305
305
306 avail = []
306 avail = []
307 not_avail = []
307 not_avail = []
308
308
309 for k, is_avail in have.items():
309 for k, is_avail in have.items():
310 if is_avail:
310 if is_avail:
311 avail.append(k)
311 avail.append(k)
312 else:
312 else:
313 not_avail.append(k)
313 not_avail.append(k)
314
314
315 if avail:
315 if avail:
316 out.append('\nTools and libraries available at test time:\n')
316 out.append('\nTools and libraries available at test time:\n')
317 avail.sort()
317 avail.sort()
318 out.append(' ' + ' '.join(avail)+'\n')
318 out.append(' ' + ' '.join(avail)+'\n')
319
319
320 if not_avail:
320 if not_avail:
321 out.append('\nTools and libraries NOT available at test time:\n')
321 out.append('\nTools and libraries NOT available at test time:\n')
322 not_avail.sort()
322 not_avail.sort()
323 out.append(' ' + ' '.join(not_avail)+'\n')
323 out.append(' ' + ' '.join(not_avail)+'\n')
324
324
325 return ''.join(out)
325 return ''.join(out)
326
326
327 def run_iptestall(options):
327 def run_iptestall(options):
328 """Run the entire IPython test suite by calling nose and trial.
328 """Run the entire IPython test suite by calling nose and trial.
329
329
330 This function constructs :class:`IPTester` instances for all IPython
330 This function constructs :class:`IPTester` instances for all IPython
331 modules and package and then runs each of them. This causes the modules
331 modules and package and then runs each of them. This causes the modules
332 and packages of IPython to be tested each in their own subprocess using
332 and packages of IPython to be tested each in their own subprocess using
333 nose.
333 nose.
334
334
335 Parameters
335 Parameters
336 ----------
336 ----------
337
337
338 All parameters are passed as attributes of the options object.
338 All parameters are passed as attributes of the options object.
339
339
340 testgroups : list of str
340 testgroups : list of str
341 Run only these sections of the test suite. If empty, run all the available
341 Run only these sections of the test suite. If empty, run all the available
342 sections.
342 sections.
343
343
344 fast : int or None
344 fast : int or None
345 Run the test suite in parallel, using n simultaneous processes. If None
345 Run the test suite in parallel, using n simultaneous processes. If None
346 is passed, one process is used per CPU core. Default 1 (i.e. sequential)
346 is passed, one process is used per CPU core. Default 1 (i.e. sequential)
347
347
348 inc_slow : bool
348 inc_slow : bool
349 Include slow tests, like IPython.parallel. By default, these tests aren't
349 Include slow tests, like IPython.parallel. By default, these tests aren't
350 run.
350 run.
351
351
352 xunit : bool
352 xunit : bool
353 Produce Xunit XML output. This is written to multiple foo.xunit.xml files.
353 Produce Xunit XML output. This is written to multiple foo.xunit.xml files.
354
354
355 coverage : bool or str
355 coverage : bool or str
356 Measure code coverage from tests. True will store the raw coverage data,
356 Measure code coverage from tests. True will store the raw coverage data,
357 or pass 'html' or 'xml' to get reports.
357 or pass 'html' or 'xml' to get reports.
358
358
359 extra_args : list
359 extra_args : list
360 Extra arguments to pass to the test subprocesses, e.g. '-v'
360 Extra arguments to pass to the test subprocesses, e.g. '-v'
361 """
361 """
362 if options.fast != 1:
362 if options.fast != 1:
363 # If running in parallel, capture output so it doesn't get interleaved
363 # If running in parallel, capture output so it doesn't get interleaved
364 TestController.buffer_output = True
364 TestController.buffer_output = True
365
365
366 to_run, not_run = prepare_controllers(options)
366 to_run, not_run = prepare_controllers(options)
367
367
368 def justify(ltext, rtext, width=70, fill='-'):
368 def justify(ltext, rtext, width=70, fill='-'):
369 ltext += ' '
369 ltext += ' '
370 rtext = (' ' + rtext).rjust(width - len(ltext), fill)
370 rtext = (' ' + rtext).rjust(width - len(ltext), fill)
371 return ltext + rtext
371 return ltext + rtext
372
372
373 # Run all test runners, tracking execution time
373 # Run all test runners, tracking execution time
374 failed = []
374 failed = []
375 t_start = time.time()
375 t_start = time.time()
376
376
377 print()
377 print()
378 if options.fast == 1:
378 if options.fast == 1:
379 # This actually means sequential, i.e. with 1 job
379 # This actually means sequential, i.e. with 1 job
380 for controller in to_run:
380 for controller in to_run:
381 print('IPython test group:', controller.section)
381 print('IPython test group:', controller.section)
382 sys.stdout.flush() # Show in correct order when output is piped
382 sys.stdout.flush() # Show in correct order when output is piped
383 controller, res = do_run(controller)
383 controller, res = do_run(controller)
384 if res:
384 if res:
385 failed.append(controller)
385 failed.append(controller)
386 if res == -signal.SIGINT:
386 if res == -signal.SIGINT:
387 print("Interrupted")
387 print("Interrupted")
388 break
388 break
389 print()
389 print()
390
390
391 else:
391 else:
392 # Run tests concurrently
392 # Run tests concurrently
393 try:
393 try:
394 pool = multiprocessing.pool.ThreadPool(options.fast)
394 pool = multiprocessing.pool.ThreadPool(options.fast)
395 for (controller, res) in pool.imap_unordered(do_run, to_run):
395 for (controller, res) in pool.imap_unordered(do_run, to_run):
396 res_string = 'OK' if res == 0 else 'FAILED'
396 res_string = 'OK' if res == 0 else 'FAILED'
397 print(justify('IPython test group: ' + controller.section, res_string))
397 print(justify('IPython test group: ' + controller.section, res_string))
398 if res:
398 if res:
399 print(bytes_to_str(controller.stdout))
399 print(bytes_to_str(controller.stdout))
400 failed.append(controller)
400 failed.append(controller)
401 if res == -signal.SIGINT:
401 if res == -signal.SIGINT:
402 print("Interrupted")
402 print("Interrupted")
403 break
403 break
404 except KeyboardInterrupt:
404 except KeyboardInterrupt:
405 return
405 return
406
406
407 for controller in not_run:
407 for controller in not_run:
408 print(justify('IPython test group: ' + controller.section, 'NOT RUN'))
408 print(justify('IPython test group: ' + controller.section, 'NOT RUN'))
409
409
410 t_end = time.time()
410 t_end = time.time()
411 t_tests = t_end - t_start
411 t_tests = t_end - t_start
412 nrunners = len(to_run)
412 nrunners = len(to_run)
413 nfail = len(failed)
413 nfail = len(failed)
414 # summarize results
414 # summarize results
415 print('_'*70)
415 print('_'*70)
416 print('Test suite completed for system with the following information:')
416 print('Test suite completed for system with the following information:')
417 print(report())
417 print(report())
418 took = "Took %.3fs." % t_tests
418 took = "Took %.3fs." % t_tests
419 print('Status: ', end='')
419 print('Status: ', end='')
420 if not failed:
420 if not failed:
421 print('OK (%d test groups).' % nrunners, took)
421 print('OK (%d test groups).' % nrunners, took)
422 else:
422 else:
423 # If anything went wrong, point out what command to rerun manually to
423 # If anything went wrong, point out what command to rerun manually to
424 # see the actual errors and individual summary
424 # see the actual errors and individual summary
425 failed_sections = [c.section for c in failed]
425 failed_sections = [c.section for c in failed]
426 print('ERROR - {} out of {} test groups failed ({}).'.format(nfail,
426 print('ERROR - {} out of {} test groups failed ({}).'.format(nfail,
427 nrunners, ', '.join(failed_sections)), took)
427 nrunners, ', '.join(failed_sections)), took)
428 print()
428 print()
429 print('You may wish to rerun these, with:')
429 print('You may wish to rerun these, with:')
430 print(' iptest', *failed_sections)
430 print(' iptest', *failed_sections)
431 print()
431 print()
432
432
433 if options.coverage:
433 if options.coverage:
434 from coverage import coverage
434 from coverage import coverage
435 cov = coverage(data_file='.coverage')
435 cov = coverage(data_file='.coverage')
436 cov.combine()
436 cov.combine()
437 cov.save()
437 cov.save()
438
438
439 # Coverage HTML report
439 # Coverage HTML report
440 if options.coverage == 'html':
440 if options.coverage == 'html':
441 html_dir = 'ipy_htmlcov'
441 html_dir = 'ipy_htmlcov'
442 shutil.rmtree(html_dir, ignore_errors=True)
442 shutil.rmtree(html_dir, ignore_errors=True)
443 print("Writing HTML coverage report to %s/ ... " % html_dir, end="")
443 print("Writing HTML coverage report to %s/ ... " % html_dir, end="")
444 sys.stdout.flush()
444 sys.stdout.flush()
445
445
446 # Custom HTML reporter to clean up module names.
446 # Custom HTML reporter to clean up module names.
447 from coverage.html import HtmlReporter
447 from coverage.html import HtmlReporter
448 class CustomHtmlReporter(HtmlReporter):
448 class CustomHtmlReporter(HtmlReporter):
449 def find_code_units(self, morfs):
449 def find_code_units(self, morfs):
450 super(CustomHtmlReporter, self).find_code_units(morfs)
450 super(CustomHtmlReporter, self).find_code_units(morfs)
451 for cu in self.code_units:
451 for cu in self.code_units:
452 nameparts = cu.name.split(os.sep)
452 nameparts = cu.name.split(os.sep)
453 if 'IPython' not in nameparts:
453 if 'IPython' not in nameparts:
454 continue
454 continue
455 ix = nameparts.index('IPython')
455 ix = nameparts.index('IPython')
456 cu.name = '.'.join(nameparts[ix:])
456 cu.name = '.'.join(nameparts[ix:])
457
457
458 # Reimplement the html_report method with our custom reporter
458 # Reimplement the html_report method with our custom reporter
459 cov._harvest_data()
459 cov._harvest_data()
460 cov.config.from_args(omit='*%stests' % os.sep, html_dir=html_dir,
460 cov.config.from_args(omit='*%stests' % os.sep, html_dir=html_dir,
461 html_title='IPython test coverage',
461 html_title='IPython test coverage',
462 )
462 )
463 reporter = CustomHtmlReporter(cov, cov.config)
463 reporter = CustomHtmlReporter(cov, cov.config)
464 reporter.report(None)
464 reporter.report(None)
465 print('done.')
465 print('done.')
466
466
467 # Coverage XML report
467 # Coverage XML report
468 elif options.coverage == 'xml':
468 elif options.coverage == 'xml':
469 cov.xml_report(outfile='ipy_coverage.xml')
469 cov.xml_report(outfile='ipy_coverage.xml')
470
470
471 if failed:
471 if failed:
472 # Ensure that our exit code indicates failure
472 # Ensure that our exit code indicates failure
473 sys.exit(1)
473 sys.exit(1)
474
474
475 argparser = argparse.ArgumentParser(description='Run IPython test suite')
475 argparser = argparse.ArgumentParser(description='Run IPython test suite')
476 argparser.add_argument('testgroups', nargs='*',
476 argparser.add_argument('testgroups', nargs='*',
477 help='Run specified groups of tests. If omitted, run '
477 help='Run specified groups of tests. If omitted, run '
478 'all tests.')
478 'all tests.')
479 argparser.add_argument('--all', action='store_true',
479 argparser.add_argument('--all', action='store_true',
480 help='Include slow tests not run by default.')
480 help='Include slow tests not run by default.')
481 argparser.add_argument('-j', '--fast', nargs='?', const=None, default=1, type=int,
481 argparser.add_argument('-j', '--fast', nargs='?', const=None, default=1, type=int,
482 help='Run test sections in parallel.')
482 help='Run test sections in parallel.')
483 argparser.add_argument('--xunit', action='store_true',
483 argparser.add_argument('--xunit', action='store_true',
484 help='Produce Xunit XML results')
484 help='Produce Xunit XML results')
485 argparser.add_argument('--coverage', nargs='?', const=True, default=False,
485 argparser.add_argument('--coverage', nargs='?', const=True, default=False,
486 help="Measure test coverage. Specify 'html' or "
486 help="Measure test coverage. Specify 'html' or "
487 "'xml' to get reports.")
487 "'xml' to get reports.")
488 argparser.add_argument('--subproc-streams', default='capture',
488 argparser.add_argument('--subproc-streams', default='capture',
489 help="What to do with stdout/stderr from subprocesses. "
489 help="What to do with stdout/stderr from subprocesses. "
490 "'capture' (default), 'show' and 'discard' are the options.")
490 "'capture' (default), 'show' and 'discard' are the options.")
491
491
492 def default_options():
492 def default_options():
493 """Get an argparse Namespace object with the default arguments, to pass to
493 """Get an argparse Namespace object with the default arguments, to pass to
494 :func:`run_iptestall`.
494 :func:`run_iptestall`.
495 """
495 """
496 options = argparser.parse_args([])
496 options = argparser.parse_args([])
497 options.extra_args = []
497 options.extra_args = []
498 return options
498 return options
499
499
500 def main():
500 def main():
501 # Arguments after -- should be passed through to nose. Argparse treats
501 # Arguments after -- should be passed through to nose. Argparse treats
502 # everything after -- as regular positional arguments, so we separate them
502 # everything after -- as regular positional arguments, so we separate them
503 # first.
503 # first.
504 try:
504 try:
505 ix = sys.argv.index('--')
505 ix = sys.argv.index('--')
506 except ValueError:
506 except ValueError:
507 to_parse = sys.argv[1:]
507 to_parse = sys.argv[1:]
508 extra_args = []
508 extra_args = []
509 else:
509 else:
510 to_parse = sys.argv[1:ix]
510 to_parse = sys.argv[1:ix]
511 extra_args = sys.argv[ix+1:]
511 extra_args = sys.argv[ix+1:]
512
512
513 options = argparser.parse_args(to_parse)
513 options = argparser.parse_args(to_parse)
514 options.extra_args = extra_args
514 options.extra_args = extra_args
515
515
516 run_iptestall(options)
516 run_iptestall(options)
517
517
518
518
519 if __name__ == '__main__':
519 if __name__ == '__main__':
520 main()
520 main()
General Comments 0
You need to be logged in to leave comments. Login now