##// END OF EJS Templates
Merge pull request #2175 from minrk/staticfile...
Bussonnier Matthias -
r8044:0eec72e0 merge
parent child Browse files
Show More
@@ -0,0 +1,7
1 /*
2 Placeholder for custom user CSS
3
4 mainly to be overridden in profile/static/css/custom.css
5
6 This will always be an empty file in IPython
7 */ No newline at end of file
@@ -0,0 +1,7
1 /*
2 Placeholder for custom user javascript
3
4 mainly to be overridden in profile/static/js/custom.js
5
6 This will always be an empty file in IPython
7 */
@@ -1,82 +1,93
1 """Utility function for installing MathJax javascript library into
1 """Utility function for installing MathJax javascript library into
2 the notebook's 'static' directory, for offline use.
2 the notebook's 'static' directory, for offline use.
3
3
4 Authors:
4 Authors:
5
5
6 * Min RK
6 * Min RK
7 """
7 """
8
8
9 #-----------------------------------------------------------------------------
9 #-----------------------------------------------------------------------------
10 # Copyright (C) 2008-2011 The IPython Development Team
10 # Copyright (C) 2008-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
19
20 import os
20 import os
21 import shutil
21 import shutil
22 import urllib2
22 import urllib2
23 import tempfile
23 import tempfile
24 import tarfile
24 import tarfile
25
25
26 from IPython.frontend.html import notebook as nbmod
26 from IPython.utils.path import locate_profile
27
27
28 #-----------------------------------------------------------------------------
28 #-----------------------------------------------------------------------------
29 # Imports
29 # Imports
30 #-----------------------------------------------------------------------------
30 #-----------------------------------------------------------------------------
31
31
32 def install_mathjax(tag='v1.1', replace=False):
32 def install_mathjax(tag='v2.0', replace=False, dest=None):
33 """Download and install MathJax for offline use.
33 """Download and install MathJax for offline use.
34
34
35 This will install mathjax to the 'static' dir in the IPython notebook
35 You can use this to install mathjax to a location on your static file
36 package, so it will fail if the caller does not have write access
36 path. This includes the `static` directory within your IPython profile,
37 to that location.
37 which is the default location for this install.
38
38
39 MathJax is a ~15MB download, and ~150MB installed.
39 MathJax is a ~15MB download, and ~150MB installed.
40
40
41 Parameters
41 Parameters
42 ----------
42 ----------
43
43
44 replace : bool [False]
44 replace : bool [False]
45 Whether to remove and replace an existing install.
45 Whether to remove and replace an existing install.
46 tag : str ['v1.1']
46 tag : str ['v2.0']
47 Which tag to download. Default is 'v1.1', the current stable release,
47 Which tag to download. Default is 'v2.0', the current stable release,
48 but alternatives include 'v1.1a' and 'master'.
48 but alternatives include 'v1.1' and 'master'.
49 dest : path
50 The path to the directory in which mathjax will be installed.
51 The default is `IPYTHONDIR/profile_default/static`.
52 dest must be on your notebook static_path when you run the notebook server.
53 The default location works for this.
49 """
54 """
50 mathjax_url = "https://github.com/mathjax/MathJax/tarball/%s"%tag
51
55
52 nbdir = os.path.dirname(os.path.abspath(nbmod.__file__))
56 mathjax_url = "https://github.com/mathjax/MathJax/tarball/%s" % tag
53 static = os.path.join(nbdir, 'static')
57
58 if dest is None:
59 dest = os.path.join(locate_profile('default'), 'static')
60
61 if not os.path.exists(dest):
62 os.mkdir(dest)
63
64 static = dest
54 dest = os.path.join(static, 'mathjax')
65 dest = os.path.join(static, 'mathjax')
55
66
56 # check for existence and permissions
67 # check for existence and permissions
57 if not os.access(static, os.W_OK):
68 if not os.access(static, os.W_OK):
58 raise IOError("Need have write access to %s"%static)
69 raise IOError("Need have write access to %s" % static)
59 if os.path.exists(dest):
70 if os.path.exists(dest):
60 if replace:
71 if replace:
61 if not os.access(dest, os.W_OK):
72 if not os.access(dest, os.W_OK):
62 raise IOError("Need have write access to %s"%dest)
73 raise IOError("Need have write access to %s" % dest)
63 print "removing previous MathJax install"
74 print "removing previous MathJax install"
64 shutil.rmtree(dest)
75 shutil.rmtree(dest)
65 else:
76 else:
66 print "offline MathJax apparently already installed"
77 print "offline MathJax apparently already installed"
67 return
78 return
68
79
69 # download mathjax
80 # download mathjax
70 print "Downloading mathjax source..."
81 print "Downloading mathjax source from %s ..." % mathjax_url
71 response = urllib2.urlopen(mathjax_url)
82 response = urllib2.urlopen(mathjax_url)
72 print "done"
83 print "done"
73 # use 'r|gz' stream mode, because socket file-like objects can't seek:
84 # use 'r|gz' stream mode, because socket file-like objects can't seek:
74 tar = tarfile.open(fileobj=response.fp, mode='r|gz')
85 tar = tarfile.open(fileobj=response.fp, mode='r|gz')
75 topdir = tar.firstmember.path
86 topdir = tar.firstmember.path
76 print "Extracting to %s"%dest
87 print "Extracting to %s" % dest
77 tar.extractall(static)
88 tar.extractall(static)
78 # it will be mathjax-MathJax-<sha>, rename to just mathjax
89 # it will be mathjax-MathJax-<sha>, rename to just mathjax
79 os.rename(os.path.join(static, topdir), dest)
90 os.rename(os.path.join(static, topdir), dest)
80
91
81
92
82 __all__ = ['install_mathjax']
93 __all__ = ['install_mathjax']
@@ -1,738 +1,920
1 """Tornado handlers for the notebook.
1 """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) 2008-2011 The IPython Development Team
9 # Copyright (C) 2008-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 logging
20 import Cookie
19 import Cookie
20 import datetime
21 import email.utils
22 import hashlib
23 import logging
24 import mimetypes
25 import os
26 import stat
27 import threading
21 import time
28 import time
22 import uuid
29 import uuid
23
30
24 from tornado import web
31 from tornado import web
25 from tornado import websocket
32 from tornado import websocket
26
33
27 from zmq.eventloop import ioloop
34 from zmq.eventloop import ioloop
28 from zmq.utils import jsonapi
35 from zmq.utils import jsonapi
29
36
30 from IPython.external.decorator import decorator
37 from IPython.external.decorator import decorator
31 from IPython.zmq.session import Session
38 from IPython.zmq.session import Session
32 from IPython.lib.security import passwd_check
39 from IPython.lib.security import passwd_check
33 from IPython.utils.jsonutil import date_default
40 from IPython.utils.jsonutil import date_default
41 from IPython.utils.path import filefind
34
42
35 try:
43 try:
36 from docutils.core import publish_string
44 from docutils.core import publish_string
37 except ImportError:
45 except ImportError:
38 publish_string = None
46 publish_string = None
39
47
40 #-----------------------------------------------------------------------------
48 #-----------------------------------------------------------------------------
41 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
49 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
42 #-----------------------------------------------------------------------------
50 #-----------------------------------------------------------------------------
43
51
44 # Google Chrome, as of release 16, changed its websocket protocol number. The
52 # Google Chrome, as of release 16, changed its websocket protocol number. The
45 # parts tornado cares about haven't really changed, so it's OK to continue
53 # parts tornado cares about haven't really changed, so it's OK to continue
46 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
54 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
47 # version as of Oct 30/2011) the version check fails, see the issue report:
55 # version as of Oct 30/2011) the version check fails, see the issue report:
48
56
49 # https://github.com/facebook/tornado/issues/385
57 # https://github.com/facebook/tornado/issues/385
50
58
51 # This issue has been fixed in Tornado post 2.1.1:
59 # This issue has been fixed in Tornado post 2.1.1:
52
60
53 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
61 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
54
62
55 # Here we manually apply the same patch as above so that users of IPython can
63 # Here we manually apply the same patch as above so that users of IPython can
56 # continue to work with an officially released Tornado. We make the
64 # continue to work with an officially released Tornado. We make the
57 # monkeypatch version check as narrow as possible to limit its effects; once
65 # monkeypatch version check as narrow as possible to limit its effects; once
58 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
66 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
59
67
60 import tornado
68 import tornado
61
69
62 if tornado.version_info <= (2,1,1):
70 if tornado.version_info <= (2,1,1):
63
71
64 def _execute(self, transforms, *args, **kwargs):
72 def _execute(self, transforms, *args, **kwargs):
65 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
73 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
66
74
67 self.open_args = args
75 self.open_args = args
68 self.open_kwargs = kwargs
76 self.open_kwargs = kwargs
69
77
70 # The difference between version 8 and 13 is that in 8 the
78 # The difference between version 8 and 13 is that in 8 the
71 # client sends a "Sec-Websocket-Origin" header and in 13 it's
79 # client sends a "Sec-Websocket-Origin" header and in 13 it's
72 # simply "Origin".
80 # simply "Origin".
73 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
81 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
74 self.ws_connection = WebSocketProtocol8(self)
82 self.ws_connection = WebSocketProtocol8(self)
75 self.ws_connection.accept_connection()
83 self.ws_connection.accept_connection()
76
84
77 elif self.request.headers.get("Sec-WebSocket-Version"):
85 elif self.request.headers.get("Sec-WebSocket-Version"):
78 self.stream.write(tornado.escape.utf8(
86 self.stream.write(tornado.escape.utf8(
79 "HTTP/1.1 426 Upgrade Required\r\n"
87 "HTTP/1.1 426 Upgrade Required\r\n"
80 "Sec-WebSocket-Version: 8\r\n\r\n"))
88 "Sec-WebSocket-Version: 8\r\n\r\n"))
81 self.stream.close()
89 self.stream.close()
82
90
83 else:
91 else:
84 self.ws_connection = WebSocketProtocol76(self)
92 self.ws_connection = WebSocketProtocol76(self)
85 self.ws_connection.accept_connection()
93 self.ws_connection.accept_connection()
86
94
87 websocket.WebSocketHandler._execute = _execute
95 websocket.WebSocketHandler._execute = _execute
88 del _execute
96 del _execute
89
97
90 #-----------------------------------------------------------------------------
98 #-----------------------------------------------------------------------------
91 # Decorator for disabling read-only handlers
99 # Decorator for disabling read-only handlers
92 #-----------------------------------------------------------------------------
100 #-----------------------------------------------------------------------------
93
101
94 @decorator
102 @decorator
95 def not_if_readonly(f, self, *args, **kwargs):
103 def not_if_readonly(f, self, *args, **kwargs):
96 if self.application.read_only:
104 if self.application.read_only:
97 raise web.HTTPError(403, "Notebook server is read-only")
105 raise web.HTTPError(403, "Notebook server is read-only")
98 else:
106 else:
99 return f(self, *args, **kwargs)
107 return f(self, *args, **kwargs)
100
108
101 @decorator
109 @decorator
102 def authenticate_unless_readonly(f, self, *args, **kwargs):
110 def authenticate_unless_readonly(f, self, *args, **kwargs):
103 """authenticate this page *unless* readonly view is active.
111 """authenticate this page *unless* readonly view is active.
104
112
105 In read-only mode, the notebook list and print view should
113 In read-only mode, the notebook list and print view should
106 be accessible without authentication.
114 be accessible without authentication.
107 """
115 """
108
116
109 @web.authenticated
117 @web.authenticated
110 def auth_f(self, *args, **kwargs):
118 def auth_f(self, *args, **kwargs):
111 return f(self, *args, **kwargs)
119 return f(self, *args, **kwargs)
112
120
113 if self.application.read_only:
121 if self.application.read_only:
114 return f(self, *args, **kwargs)
122 return f(self, *args, **kwargs)
115 else:
123 else:
116 return auth_f(self, *args, **kwargs)
124 return auth_f(self, *args, **kwargs)
117
125
118 #-----------------------------------------------------------------------------
126 #-----------------------------------------------------------------------------
119 # Top-level handlers
127 # Top-level handlers
120 #-----------------------------------------------------------------------------
128 #-----------------------------------------------------------------------------
121
129
122 class RequestHandler(web.RequestHandler):
130 class RequestHandler(web.RequestHandler):
123 """RequestHandler with default variable setting."""
131 """RequestHandler with default variable setting."""
124
132
125 def render(*args, **kwargs):
133 def render(*args, **kwargs):
126 kwargs.setdefault('message', '')
134 kwargs.setdefault('message', '')
127 return web.RequestHandler.render(*args, **kwargs)
135 return web.RequestHandler.render(*args, **kwargs)
128
136
129 class AuthenticatedHandler(RequestHandler):
137 class AuthenticatedHandler(RequestHandler):
130 """A RequestHandler with an authenticated user."""
138 """A RequestHandler with an authenticated user."""
131
139
132 def get_current_user(self):
140 def get_current_user(self):
133 user_id = self.get_secure_cookie("username")
141 user_id = self.get_secure_cookie("username")
134 # For now the user_id should not return empty, but it could eventually
142 # For now the user_id should not return empty, but it could eventually
135 if user_id == '':
143 if user_id == '':
136 user_id = 'anonymous'
144 user_id = 'anonymous'
137 if user_id is None:
145 if user_id is None:
138 # prevent extra Invalid cookie sig warnings:
146 # prevent extra Invalid cookie sig warnings:
139 self.clear_cookie('username')
147 self.clear_cookie('username')
140 if not self.application.password and not self.application.read_only:
148 if not self.application.password and not self.application.read_only:
141 user_id = 'anonymous'
149 user_id = 'anonymous'
142 return user_id
150 return user_id
143
151
144 @property
152 @property
145 def logged_in(self):
153 def logged_in(self):
146 """Is a user currently logged in?
154 """Is a user currently logged in?
147
155
148 """
156 """
149 user = self.get_current_user()
157 user = self.get_current_user()
150 return (user and not user == 'anonymous')
158 return (user and not user == 'anonymous')
151
159
152 @property
160 @property
153 def login_available(self):
161 def login_available(self):
154 """May a user proceed to log in?
162 """May a user proceed to log in?
155
163
156 This returns True if login capability is available, irrespective of
164 This returns True if login capability is available, irrespective of
157 whether the user is already logged in or not.
165 whether the user is already logged in or not.
158
166
159 """
167 """
160 return bool(self.application.password)
168 return bool(self.application.password)
161
169
162 @property
170 @property
163 def read_only(self):
171 def read_only(self):
164 """Is the notebook read-only?
172 """Is the notebook read-only?
165
173
166 """
174 """
167 return self.application.read_only
175 return self.application.read_only
168
176
169 @property
177 @property
170 def ws_url(self):
178 def ws_url(self):
171 """websocket url matching the current request
179 """websocket url matching the current request
172
180
173 turns http[s]://host[:port] into
181 turns http[s]://host[:port] into
174 ws[s]://host[:port]
182 ws[s]://host[:port]
175 """
183 """
176 proto = self.request.protocol.replace('http', 'ws')
184 proto = self.request.protocol.replace('http', 'ws')
177 host = self.application.ipython_app.websocket_host # default to config value
185 host = self.application.ipython_app.websocket_host # default to config value
178 if host == '':
186 if host == '':
179 host = self.request.host # get from request
187 host = self.request.host # get from request
180 return "%s://%s" % (proto, host)
188 return "%s://%s" % (proto, host)
181
189
182
190
183 class AuthenticatedFileHandler(AuthenticatedHandler, web.StaticFileHandler):
191 class AuthenticatedFileHandler(AuthenticatedHandler, web.StaticFileHandler):
184 """static files should only be accessible when logged in"""
192 """static files should only be accessible when logged in"""
185
193
186 @authenticate_unless_readonly
194 @authenticate_unless_readonly
187 def get(self, path):
195 def get(self, path):
188 return web.StaticFileHandler.get(self, path)
196 return web.StaticFileHandler.get(self, path)
189
197
190
198
191 class ProjectDashboardHandler(AuthenticatedHandler):
199 class ProjectDashboardHandler(AuthenticatedHandler):
192
200
193 @authenticate_unless_readonly
201 @authenticate_unless_readonly
194 def get(self):
202 def get(self):
195 nbm = self.application.notebook_manager
203 nbm = self.application.notebook_manager
196 project = nbm.notebook_dir
204 project = nbm.notebook_dir
197 self.render(
205 self.render(
198 'projectdashboard.html', project=project,
206 'projectdashboard.html', project=project,
199 base_project_url=self.application.ipython_app.base_project_url,
207 base_project_url=self.application.ipython_app.base_project_url,
200 base_kernel_url=self.application.ipython_app.base_kernel_url,
208 base_kernel_url=self.application.ipython_app.base_kernel_url,
201 read_only=self.read_only,
209 read_only=self.read_only,
202 logged_in=self.logged_in,
210 logged_in=self.logged_in,
203 login_available=self.login_available
211 login_available=self.login_available
204 )
212 )
205
213
206
214
207 class LoginHandler(AuthenticatedHandler):
215 class LoginHandler(AuthenticatedHandler):
208
216
209 def _render(self, message=None):
217 def _render(self, message=None):
210 self.render('login.html',
218 self.render('login.html',
211 next=self.get_argument('next', default=self.application.ipython_app.base_project_url),
219 next=self.get_argument('next', default=self.application.ipython_app.base_project_url),
212 read_only=self.read_only,
220 read_only=self.read_only,
213 logged_in=self.logged_in,
221 logged_in=self.logged_in,
214 login_available=self.login_available,
222 login_available=self.login_available,
215 base_project_url=self.application.ipython_app.base_project_url,
223 base_project_url=self.application.ipython_app.base_project_url,
216 message=message
224 message=message
217 )
225 )
218
226
219 def get(self):
227 def get(self):
220 if self.current_user:
228 if self.current_user:
221 self.redirect(self.get_argument('next', default=self.application.ipython_app.base_project_url))
229 self.redirect(self.get_argument('next', default=self.application.ipython_app.base_project_url))
222 else:
230 else:
223 self._render()
231 self._render()
224
232
225 def post(self):
233 def post(self):
226 pwd = self.get_argument('password', default=u'')
234 pwd = self.get_argument('password', default=u'')
227 if self.application.password:
235 if self.application.password:
228 if passwd_check(self.application.password, pwd):
236 if passwd_check(self.application.password, pwd):
229 self.set_secure_cookie('username', str(uuid.uuid4()))
237 self.set_secure_cookie('username', str(uuid.uuid4()))
230 else:
238 else:
231 self._render(message={'error': 'Invalid password'})
239 self._render(message={'error': 'Invalid password'})
232 return
240 return
233
241
234 self.redirect(self.get_argument('next', default=self.application.ipython_app.base_project_url))
242 self.redirect(self.get_argument('next', default=self.application.ipython_app.base_project_url))
235
243
236
244
237 class LogoutHandler(AuthenticatedHandler):
245 class LogoutHandler(AuthenticatedHandler):
238
246
239 def get(self):
247 def get(self):
240 self.clear_cookie('username')
248 self.clear_cookie('username')
241 if self.login_available:
249 if self.login_available:
242 message = {'info': 'Successfully logged out.'}
250 message = {'info': 'Successfully logged out.'}
243 else:
251 else:
244 message = {'warning': 'Cannot log out. Notebook authentication '
252 message = {'warning': 'Cannot log out. Notebook authentication '
245 'is disabled.'}
253 'is disabled.'}
246
254
247 self.render('logout.html',
255 self.render('logout.html',
248 read_only=self.read_only,
256 read_only=self.read_only,
249 logged_in=self.logged_in,
257 logged_in=self.logged_in,
250 login_available=self.login_available,
258 login_available=self.login_available,
251 base_project_url=self.application.ipython_app.base_project_url,
259 base_project_url=self.application.ipython_app.base_project_url,
252 message=message)
260 message=message)
253
261
254
262
255 class NewHandler(AuthenticatedHandler):
263 class NewHandler(AuthenticatedHandler):
256
264
257 @web.authenticated
265 @web.authenticated
258 def get(self):
266 def get(self):
259 nbm = self.application.notebook_manager
267 nbm = self.application.notebook_manager
260 project = nbm.notebook_dir
268 project = nbm.notebook_dir
261 notebook_id = nbm.new_notebook()
269 notebook_id = nbm.new_notebook()
262 self.render(
270 self.render(
263 'notebook.html', project=project,
271 'notebook.html', project=project,
264 notebook_id=notebook_id,
272 notebook_id=notebook_id,
265 base_project_url=self.application.ipython_app.base_project_url,
273 base_project_url=self.application.ipython_app.base_project_url,
266 base_kernel_url=self.application.ipython_app.base_kernel_url,
274 base_kernel_url=self.application.ipython_app.base_kernel_url,
267 kill_kernel=False,
275 kill_kernel=False,
268 read_only=False,
276 read_only=False,
269 logged_in=self.logged_in,
277 logged_in=self.logged_in,
270 login_available=self.login_available,
278 login_available=self.login_available,
271 mathjax_url=self.application.ipython_app.mathjax_url,
279 mathjax_url=self.application.ipython_app.mathjax_url,
272 )
280 )
273
281
274
282
275 class NamedNotebookHandler(AuthenticatedHandler):
283 class NamedNotebookHandler(AuthenticatedHandler):
276
284
277 @authenticate_unless_readonly
285 @authenticate_unless_readonly
278 def get(self, notebook_id):
286 def get(self, notebook_id):
279 nbm = self.application.notebook_manager
287 nbm = self.application.notebook_manager
280 project = nbm.notebook_dir
288 project = nbm.notebook_dir
281 if not nbm.notebook_exists(notebook_id):
289 if not nbm.notebook_exists(notebook_id):
282 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
290 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
283
291
284 self.render(
292 self.render(
285 'notebook.html', project=project,
293 'notebook.html', project=project,
286 notebook_id=notebook_id,
294 notebook_id=notebook_id,
287 base_project_url=self.application.ipython_app.base_project_url,
295 base_project_url=self.application.ipython_app.base_project_url,
288 base_kernel_url=self.application.ipython_app.base_kernel_url,
296 base_kernel_url=self.application.ipython_app.base_kernel_url,
289 kill_kernel=False,
297 kill_kernel=False,
290 read_only=self.read_only,
298 read_only=self.read_only,
291 logged_in=self.logged_in,
299 logged_in=self.logged_in,
292 login_available=self.login_available,
300 login_available=self.login_available,
293 mathjax_url=self.application.ipython_app.mathjax_url,
301 mathjax_url=self.application.ipython_app.mathjax_url,
294 )
302 )
295
303
296
304
297 class PrintNotebookHandler(AuthenticatedHandler):
305 class PrintNotebookHandler(AuthenticatedHandler):
298
306
299 @authenticate_unless_readonly
307 @authenticate_unless_readonly
300 def get(self, notebook_id):
308 def get(self, notebook_id):
301 nbm = self.application.notebook_manager
309 nbm = self.application.notebook_manager
302 project = nbm.notebook_dir
310 project = nbm.notebook_dir
303 if not nbm.notebook_exists(notebook_id):
311 if not nbm.notebook_exists(notebook_id):
304 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
312 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
305
313
306 self.render(
314 self.render(
307 'printnotebook.html', project=project,
315 'printnotebook.html', project=project,
308 notebook_id=notebook_id,
316 notebook_id=notebook_id,
309 base_project_url=self.application.ipython_app.base_project_url,
317 base_project_url=self.application.ipython_app.base_project_url,
310 base_kernel_url=self.application.ipython_app.base_kernel_url,
318 base_kernel_url=self.application.ipython_app.base_kernel_url,
311 kill_kernel=False,
319 kill_kernel=False,
312 read_only=self.read_only,
320 read_only=self.read_only,
313 logged_in=self.logged_in,
321 logged_in=self.logged_in,
314 login_available=self.login_available,
322 login_available=self.login_available,
315 mathjax_url=self.application.ipython_app.mathjax_url,
323 mathjax_url=self.application.ipython_app.mathjax_url,
316 )
324 )
317
325
318 #-----------------------------------------------------------------------------
326 #-----------------------------------------------------------------------------
319 # Kernel handlers
327 # Kernel handlers
320 #-----------------------------------------------------------------------------
328 #-----------------------------------------------------------------------------
321
329
322
330
323 class MainKernelHandler(AuthenticatedHandler):
331 class MainKernelHandler(AuthenticatedHandler):
324
332
325 @web.authenticated
333 @web.authenticated
326 def get(self):
334 def get(self):
327 km = self.application.kernel_manager
335 km = self.application.kernel_manager
328 self.finish(jsonapi.dumps(km.kernel_ids))
336 self.finish(jsonapi.dumps(km.kernel_ids))
329
337
330 @web.authenticated
338 @web.authenticated
331 def post(self):
339 def post(self):
332 km = self.application.kernel_manager
340 km = self.application.kernel_manager
333 nbm = self.application.notebook_manager
341 nbm = self.application.notebook_manager
334 notebook_id = self.get_argument('notebook', default=None)
342 notebook_id = self.get_argument('notebook', default=None)
335 kernel_id = km.start_kernel(notebook_id, cwd=nbm.notebook_dir)
343 kernel_id = km.start_kernel(notebook_id, cwd=nbm.notebook_dir)
336 data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
344 data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
337 self.set_header('Location', '/'+kernel_id)
345 self.set_header('Location', '/'+kernel_id)
338 self.finish(jsonapi.dumps(data))
346 self.finish(jsonapi.dumps(data))
339
347
340
348
341 class KernelHandler(AuthenticatedHandler):
349 class KernelHandler(AuthenticatedHandler):
342
350
343 SUPPORTED_METHODS = ('DELETE')
351 SUPPORTED_METHODS = ('DELETE')
344
352
345 @web.authenticated
353 @web.authenticated
346 def delete(self, kernel_id):
354 def delete(self, kernel_id):
347 km = self.application.kernel_manager
355 km = self.application.kernel_manager
348 km.shutdown_kernel(kernel_id)
356 km.shutdown_kernel(kernel_id)
349 self.set_status(204)
357 self.set_status(204)
350 self.finish()
358 self.finish()
351
359
352
360
353 class KernelActionHandler(AuthenticatedHandler):
361 class KernelActionHandler(AuthenticatedHandler):
354
362
355 @web.authenticated
363 @web.authenticated
356 def post(self, kernel_id, action):
364 def post(self, kernel_id, action):
357 km = self.application.kernel_manager
365 km = self.application.kernel_manager
358 if action == 'interrupt':
366 if action == 'interrupt':
359 km.interrupt_kernel(kernel_id)
367 km.interrupt_kernel(kernel_id)
360 self.set_status(204)
368 self.set_status(204)
361 if action == 'restart':
369 if action == 'restart':
362 new_kernel_id = km.restart_kernel(kernel_id)
370 new_kernel_id = km.restart_kernel(kernel_id)
363 data = {'ws_url':self.ws_url,'kernel_id':new_kernel_id}
371 data = {'ws_url':self.ws_url,'kernel_id':new_kernel_id}
364 self.set_header('Location', '/'+new_kernel_id)
372 self.set_header('Location', '/'+new_kernel_id)
365 self.write(jsonapi.dumps(data))
373 self.write(jsonapi.dumps(data))
366 self.finish()
374 self.finish()
367
375
368
376
369 class ZMQStreamHandler(websocket.WebSocketHandler):
377 class ZMQStreamHandler(websocket.WebSocketHandler):
370
378
371 def _reserialize_reply(self, msg_list):
379 def _reserialize_reply(self, msg_list):
372 """Reserialize a reply message using JSON.
380 """Reserialize a reply message using JSON.
373
381
374 This takes the msg list from the ZMQ socket, unserializes it using
382 This takes the msg list from the ZMQ socket, unserializes it using
375 self.session and then serializes the result using JSON. This method
383 self.session and then serializes the result using JSON. This method
376 should be used by self._on_zmq_reply to build messages that can
384 should be used by self._on_zmq_reply to build messages that can
377 be sent back to the browser.
385 be sent back to the browser.
378 """
386 """
379 idents, msg_list = self.session.feed_identities(msg_list)
387 idents, msg_list = self.session.feed_identities(msg_list)
380 msg = self.session.unserialize(msg_list)
388 msg = self.session.unserialize(msg_list)
381 try:
389 try:
382 msg['header'].pop('date')
390 msg['header'].pop('date')
383 except KeyError:
391 except KeyError:
384 pass
392 pass
385 try:
393 try:
386 msg['parent_header'].pop('date')
394 msg['parent_header'].pop('date')
387 except KeyError:
395 except KeyError:
388 pass
396 pass
389 msg.pop('buffers')
397 msg.pop('buffers')
390 return jsonapi.dumps(msg, default=date_default)
398 return jsonapi.dumps(msg, default=date_default)
391
399
392 def _on_zmq_reply(self, msg_list):
400 def _on_zmq_reply(self, msg_list):
393 try:
401 try:
394 msg = self._reserialize_reply(msg_list)
402 msg = self._reserialize_reply(msg_list)
395 except Exception:
403 except Exception:
396 self.application.log.critical("Malformed message: %r" % msg_list, exc_info=True)
404 self.application.log.critical("Malformed message: %r" % msg_list, exc_info=True)
397 else:
405 else:
398 self.write_message(msg)
406 self.write_message(msg)
399
407
400 def allow_draft76(self):
408 def allow_draft76(self):
401 """Allow draft 76, until browsers such as Safari update to RFC 6455.
409 """Allow draft 76, until browsers such as Safari update to RFC 6455.
402
410
403 This has been disabled by default in tornado in release 2.2.0, and
411 This has been disabled by default in tornado in release 2.2.0, and
404 support will be removed in later versions.
412 support will be removed in later versions.
405 """
413 """
406 return True
414 return True
407
415
408
416
409 class AuthenticatedZMQStreamHandler(ZMQStreamHandler):
417 class AuthenticatedZMQStreamHandler(ZMQStreamHandler):
410
418
411 def open(self, kernel_id):
419 def open(self, kernel_id):
412 self.kernel_id = kernel_id.decode('ascii')
420 self.kernel_id = kernel_id.decode('ascii')
413 try:
421 try:
414 cfg = self.application.ipython_app.config
422 cfg = self.application.ipython_app.config
415 except AttributeError:
423 except AttributeError:
416 # protect from the case where this is run from something other than
424 # protect from the case where this is run from something other than
417 # the notebook app:
425 # the notebook app:
418 cfg = None
426 cfg = None
419 self.session = Session(config=cfg)
427 self.session = Session(config=cfg)
420 self.save_on_message = self.on_message
428 self.save_on_message = self.on_message
421 self.on_message = self.on_first_message
429 self.on_message = self.on_first_message
422
430
423 def get_current_user(self):
431 def get_current_user(self):
424 user_id = self.get_secure_cookie("username")
432 user_id = self.get_secure_cookie("username")
425 if user_id == '' or (user_id is None and not self.application.password):
433 if user_id == '' or (user_id is None and not self.application.password):
426 user_id = 'anonymous'
434 user_id = 'anonymous'
427 return user_id
435 return user_id
428
436
429 def _inject_cookie_message(self, msg):
437 def _inject_cookie_message(self, msg):
430 """Inject the first message, which is the document cookie,
438 """Inject the first message, which is the document cookie,
431 for authentication."""
439 for authentication."""
432 if isinstance(msg, unicode):
440 if isinstance(msg, unicode):
433 # Cookie can't constructor doesn't accept unicode strings for some reason
441 # Cookie can't constructor doesn't accept unicode strings for some reason
434 msg = msg.encode('utf8', 'replace')
442 msg = msg.encode('utf8', 'replace')
435 try:
443 try:
436 self.request._cookies = Cookie.SimpleCookie(msg)
444 self.request._cookies = Cookie.SimpleCookie(msg)
437 except:
445 except:
438 logging.warn("couldn't parse cookie string: %s",msg, exc_info=True)
446 logging.warn("couldn't parse cookie string: %s",msg, exc_info=True)
439
447
440 def on_first_message(self, msg):
448 def on_first_message(self, msg):
441 self._inject_cookie_message(msg)
449 self._inject_cookie_message(msg)
442 if self.get_current_user() is None:
450 if self.get_current_user() is None:
443 logging.warn("Couldn't authenticate WebSocket connection")
451 logging.warn("Couldn't authenticate WebSocket connection")
444 raise web.HTTPError(403)
452 raise web.HTTPError(403)
445 self.on_message = self.save_on_message
453 self.on_message = self.save_on_message
446
454
447
455
448 class IOPubHandler(AuthenticatedZMQStreamHandler):
456 class IOPubHandler(AuthenticatedZMQStreamHandler):
449
457
450 def initialize(self, *args, **kwargs):
458 def initialize(self, *args, **kwargs):
451 self._kernel_alive = True
459 self._kernel_alive = True
452 self._beating = False
460 self._beating = False
453 self.iopub_stream = None
461 self.iopub_stream = None
454 self.hb_stream = None
462 self.hb_stream = None
455
463
456 def on_first_message(self, msg):
464 def on_first_message(self, msg):
457 try:
465 try:
458 super(IOPubHandler, self).on_first_message(msg)
466 super(IOPubHandler, self).on_first_message(msg)
459 except web.HTTPError:
467 except web.HTTPError:
460 self.close()
468 self.close()
461 return
469 return
462 km = self.application.kernel_manager
470 km = self.application.kernel_manager
463 self.time_to_dead = km.time_to_dead
471 self.time_to_dead = km.time_to_dead
464 self.first_beat = km.first_beat
472 self.first_beat = km.first_beat
465 kernel_id = self.kernel_id
473 kernel_id = self.kernel_id
466 try:
474 try:
467 self.iopub_stream = km.create_iopub_stream(kernel_id)
475 self.iopub_stream = km.create_iopub_stream(kernel_id)
468 self.hb_stream = km.create_hb_stream(kernel_id)
476 self.hb_stream = km.create_hb_stream(kernel_id)
469 except web.HTTPError:
477 except web.HTTPError:
470 # WebSockets don't response to traditional error codes so we
478 # WebSockets don't response to traditional error codes so we
471 # close the connection.
479 # close the connection.
472 if not self.stream.closed():
480 if not self.stream.closed():
473 self.stream.close()
481 self.stream.close()
474 self.close()
482 self.close()
475 else:
483 else:
476 self.iopub_stream.on_recv(self._on_zmq_reply)
484 self.iopub_stream.on_recv(self._on_zmq_reply)
477 self.start_hb(self.kernel_died)
485 self.start_hb(self.kernel_died)
478
486
479 def on_message(self, msg):
487 def on_message(self, msg):
480 pass
488 pass
481
489
482 def on_close(self):
490 def on_close(self):
483 # This method can be called twice, once by self.kernel_died and once
491 # This method can be called twice, once by self.kernel_died and once
484 # from the WebSocket close event. If the WebSocket connection is
492 # from the WebSocket close event. If the WebSocket connection is
485 # closed before the ZMQ streams are setup, they could be None.
493 # closed before the ZMQ streams are setup, they could be None.
486 self.stop_hb()
494 self.stop_hb()
487 if self.iopub_stream is not None and not self.iopub_stream.closed():
495 if self.iopub_stream is not None and not self.iopub_stream.closed():
488 self.iopub_stream.on_recv(None)
496 self.iopub_stream.on_recv(None)
489 self.iopub_stream.close()
497 self.iopub_stream.close()
490 if self.hb_stream is not None and not self.hb_stream.closed():
498 if self.hb_stream is not None and not self.hb_stream.closed():
491 self.hb_stream.close()
499 self.hb_stream.close()
492
500
493 def start_hb(self, callback):
501 def start_hb(self, callback):
494 """Start the heartbeating and call the callback if the kernel dies."""
502 """Start the heartbeating and call the callback if the kernel dies."""
495 if not self._beating:
503 if not self._beating:
496 self._kernel_alive = True
504 self._kernel_alive = True
497
505
498 def ping_or_dead():
506 def ping_or_dead():
499 self.hb_stream.flush()
507 self.hb_stream.flush()
500 if self._kernel_alive:
508 if self._kernel_alive:
501 self._kernel_alive = False
509 self._kernel_alive = False
502 self.hb_stream.send(b'ping')
510 self.hb_stream.send(b'ping')
503 # flush stream to force immediate socket send
511 # flush stream to force immediate socket send
504 self.hb_stream.flush()
512 self.hb_stream.flush()
505 else:
513 else:
506 try:
514 try:
507 callback()
515 callback()
508 except:
516 except:
509 pass
517 pass
510 finally:
518 finally:
511 self.stop_hb()
519 self.stop_hb()
512
520
513 def beat_received(msg):
521 def beat_received(msg):
514 self._kernel_alive = True
522 self._kernel_alive = True
515
523
516 self.hb_stream.on_recv(beat_received)
524 self.hb_stream.on_recv(beat_received)
517 loop = ioloop.IOLoop.instance()
525 loop = ioloop.IOLoop.instance()
518 self._hb_periodic_callback = ioloop.PeriodicCallback(ping_or_dead, self.time_to_dead*1000, loop)
526 self._hb_periodic_callback = ioloop.PeriodicCallback(ping_or_dead, self.time_to_dead*1000, loop)
519 loop.add_timeout(time.time()+self.first_beat, self._really_start_hb)
527 loop.add_timeout(time.time()+self.first_beat, self._really_start_hb)
520 self._beating= True
528 self._beating= True
521
529
522 def _really_start_hb(self):
530 def _really_start_hb(self):
523 """callback for delayed heartbeat start
531 """callback for delayed heartbeat start
524
532
525 Only start the hb loop if we haven't been closed during the wait.
533 Only start the hb loop if we haven't been closed during the wait.
526 """
534 """
527 if self._beating and not self.hb_stream.closed():
535 if self._beating and not self.hb_stream.closed():
528 self._hb_periodic_callback.start()
536 self._hb_periodic_callback.start()
529
537
530 def stop_hb(self):
538 def stop_hb(self):
531 """Stop the heartbeating and cancel all related callbacks."""
539 """Stop the heartbeating and cancel all related callbacks."""
532 if self._beating:
540 if self._beating:
533 self._beating = False
541 self._beating = False
534 self._hb_periodic_callback.stop()
542 self._hb_periodic_callback.stop()
535 if not self.hb_stream.closed():
543 if not self.hb_stream.closed():
536 self.hb_stream.on_recv(None)
544 self.hb_stream.on_recv(None)
537
545
538 def kernel_died(self):
546 def kernel_died(self):
539 self.application.kernel_manager.delete_mapping_for_kernel(self.kernel_id)
547 self.application.kernel_manager.delete_mapping_for_kernel(self.kernel_id)
540 self.application.log.error("Kernel %s failed to respond to heartbeat", self.kernel_id)
548 self.application.log.error("Kernel %s failed to respond to heartbeat", self.kernel_id)
541 self.write_message(
549 self.write_message(
542 {'header': {'msg_type': 'status'},
550 {'header': {'msg_type': 'status'},
543 'parent_header': {},
551 'parent_header': {},
544 'content': {'execution_state':'dead'}
552 'content': {'execution_state':'dead'}
545 }
553 }
546 )
554 )
547 self.on_close()
555 self.on_close()
548
556
549
557
550 class ShellHandler(AuthenticatedZMQStreamHandler):
558 class ShellHandler(AuthenticatedZMQStreamHandler):
551
559
552 def initialize(self, *args, **kwargs):
560 def initialize(self, *args, **kwargs):
553 self.shell_stream = None
561 self.shell_stream = None
554
562
555 def on_first_message(self, msg):
563 def on_first_message(self, msg):
556 try:
564 try:
557 super(ShellHandler, self).on_first_message(msg)
565 super(ShellHandler, self).on_first_message(msg)
558 except web.HTTPError:
566 except web.HTTPError:
559 self.close()
567 self.close()
560 return
568 return
561 km = self.application.kernel_manager
569 km = self.application.kernel_manager
562 self.max_msg_size = km.max_msg_size
570 self.max_msg_size = km.max_msg_size
563 kernel_id = self.kernel_id
571 kernel_id = self.kernel_id
564 try:
572 try:
565 self.shell_stream = km.create_shell_stream(kernel_id)
573 self.shell_stream = km.create_shell_stream(kernel_id)
566 except web.HTTPError:
574 except web.HTTPError:
567 # WebSockets don't response to traditional error codes so we
575 # WebSockets don't response to traditional error codes so we
568 # close the connection.
576 # close the connection.
569 if not self.stream.closed():
577 if not self.stream.closed():
570 self.stream.close()
578 self.stream.close()
571 self.close()
579 self.close()
572 else:
580 else:
573 self.shell_stream.on_recv(self._on_zmq_reply)
581 self.shell_stream.on_recv(self._on_zmq_reply)
574
582
575 def on_message(self, msg):
583 def on_message(self, msg):
576 if len(msg) < self.max_msg_size:
584 if len(msg) < self.max_msg_size:
577 msg = jsonapi.loads(msg)
585 msg = jsonapi.loads(msg)
578 self.session.send(self.shell_stream, msg)
586 self.session.send(self.shell_stream, msg)
579
587
580 def on_close(self):
588 def on_close(self):
581 # Make sure the stream exists and is not already closed.
589 # Make sure the stream exists and is not already closed.
582 if self.shell_stream is not None and not self.shell_stream.closed():
590 if self.shell_stream is not None and not self.shell_stream.closed():
583 self.shell_stream.close()
591 self.shell_stream.close()
584
592
585
593
586 #-----------------------------------------------------------------------------
594 #-----------------------------------------------------------------------------
587 # Notebook web service handlers
595 # Notebook web service handlers
588 #-----------------------------------------------------------------------------
596 #-----------------------------------------------------------------------------
589
597
590 class NotebookRootHandler(AuthenticatedHandler):
598 class NotebookRootHandler(AuthenticatedHandler):
591
599
592 @authenticate_unless_readonly
600 @authenticate_unless_readonly
593 def get(self):
601 def get(self):
594 nbm = self.application.notebook_manager
602 nbm = self.application.notebook_manager
595 km = self.application.kernel_manager
603 km = self.application.kernel_manager
596 files = nbm.list_notebooks()
604 files = nbm.list_notebooks()
597 for f in files :
605 for f in files :
598 f['kernel_id'] = km.kernel_for_notebook(f['notebook_id'])
606 f['kernel_id'] = km.kernel_for_notebook(f['notebook_id'])
599 self.finish(jsonapi.dumps(files))
607 self.finish(jsonapi.dumps(files))
600
608
601 @web.authenticated
609 @web.authenticated
602 def post(self):
610 def post(self):
603 nbm = self.application.notebook_manager
611 nbm = self.application.notebook_manager
604 body = self.request.body.strip()
612 body = self.request.body.strip()
605 format = self.get_argument('format', default='json')
613 format = self.get_argument('format', default='json')
606 name = self.get_argument('name', default=None)
614 name = self.get_argument('name', default=None)
607 if body:
615 if body:
608 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
616 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
609 else:
617 else:
610 notebook_id = nbm.new_notebook()
618 notebook_id = nbm.new_notebook()
611 self.set_header('Location', '/'+notebook_id)
619 self.set_header('Location', '/'+notebook_id)
612 self.finish(jsonapi.dumps(notebook_id))
620 self.finish(jsonapi.dumps(notebook_id))
613
621
614
622
615 class NotebookHandler(AuthenticatedHandler):
623 class NotebookHandler(AuthenticatedHandler):
616
624
617 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
625 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
618
626
619 @authenticate_unless_readonly
627 @authenticate_unless_readonly
620 def get(self, notebook_id):
628 def get(self, notebook_id):
621 nbm = self.application.notebook_manager
629 nbm = self.application.notebook_manager
622 format = self.get_argument('format', default='json')
630 format = self.get_argument('format', default='json')
623 last_mod, name, data = nbm.get_notebook(notebook_id, format)
631 last_mod, name, data = nbm.get_notebook(notebook_id, format)
624
632
625 if format == u'json':
633 if format == u'json':
626 self.set_header('Content-Type', 'application/json')
634 self.set_header('Content-Type', 'application/json')
627 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
635 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
628 elif format == u'py':
636 elif format == u'py':
629 self.set_header('Content-Type', 'application/x-python')
637 self.set_header('Content-Type', 'application/x-python')
630 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
638 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
631 self.set_header('Last-Modified', last_mod)
639 self.set_header('Last-Modified', last_mod)
632 self.finish(data)
640 self.finish(data)
633
641
634 @web.authenticated
642 @web.authenticated
635 def put(self, notebook_id):
643 def put(self, notebook_id):
636 nbm = self.application.notebook_manager
644 nbm = self.application.notebook_manager
637 format = self.get_argument('format', default='json')
645 format = self.get_argument('format', default='json')
638 name = self.get_argument('name', default=None)
646 name = self.get_argument('name', default=None)
639 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
647 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
640 self.set_status(204)
648 self.set_status(204)
641 self.finish()
649 self.finish()
642
650
643 @web.authenticated
651 @web.authenticated
644 def delete(self, notebook_id):
652 def delete(self, notebook_id):
645 nbm = self.application.notebook_manager
653 nbm = self.application.notebook_manager
646 nbm.delete_notebook(notebook_id)
654 nbm.delete_notebook(notebook_id)
647 self.set_status(204)
655 self.set_status(204)
648 self.finish()
656 self.finish()
649
657
650
658
651 class NotebookCopyHandler(AuthenticatedHandler):
659 class NotebookCopyHandler(AuthenticatedHandler):
652
660
653 @web.authenticated
661 @web.authenticated
654 def get(self, notebook_id):
662 def get(self, notebook_id):
655 nbm = self.application.notebook_manager
663 nbm = self.application.notebook_manager
656 project = nbm.notebook_dir
664 project = nbm.notebook_dir
657 notebook_id = nbm.copy_notebook(notebook_id)
665 notebook_id = nbm.copy_notebook(notebook_id)
658 self.render(
666 self.render(
659 'notebook.html', project=project,
667 'notebook.html', project=project,
660 notebook_id=notebook_id,
668 notebook_id=notebook_id,
661 base_project_url=self.application.ipython_app.base_project_url,
669 base_project_url=self.application.ipython_app.base_project_url,
662 base_kernel_url=self.application.ipython_app.base_kernel_url,
670 base_kernel_url=self.application.ipython_app.base_kernel_url,
663 kill_kernel=False,
671 kill_kernel=False,
664 read_only=False,
672 read_only=False,
665 logged_in=self.logged_in,
673 logged_in=self.logged_in,
666 login_available=self.login_available,
674 login_available=self.login_available,
667 mathjax_url=self.application.ipython_app.mathjax_url,
675 mathjax_url=self.application.ipython_app.mathjax_url,
668 )
676 )
669
677
670
678
671 #-----------------------------------------------------------------------------
679 #-----------------------------------------------------------------------------
672 # Cluster handlers
680 # Cluster handlers
673 #-----------------------------------------------------------------------------
681 #-----------------------------------------------------------------------------
674
682
675
683
676 class MainClusterHandler(AuthenticatedHandler):
684 class MainClusterHandler(AuthenticatedHandler):
677
685
678 @web.authenticated
686 @web.authenticated
679 def get(self):
687 def get(self):
680 cm = self.application.cluster_manager
688 cm = self.application.cluster_manager
681 self.finish(jsonapi.dumps(cm.list_profiles()))
689 self.finish(jsonapi.dumps(cm.list_profiles()))
682
690
683
691
684 class ClusterProfileHandler(AuthenticatedHandler):
692 class ClusterProfileHandler(AuthenticatedHandler):
685
693
686 @web.authenticated
694 @web.authenticated
687 def get(self, profile):
695 def get(self, profile):
688 cm = self.application.cluster_manager
696 cm = self.application.cluster_manager
689 self.finish(jsonapi.dumps(cm.profile_info(profile)))
697 self.finish(jsonapi.dumps(cm.profile_info(profile)))
690
698
691
699
692 class ClusterActionHandler(AuthenticatedHandler):
700 class ClusterActionHandler(AuthenticatedHandler):
693
701
694 @web.authenticated
702 @web.authenticated
695 def post(self, profile, action):
703 def post(self, profile, action):
696 cm = self.application.cluster_manager
704 cm = self.application.cluster_manager
697 if action == 'start':
705 if action == 'start':
698 n = self.get_argument('n',default=None)
706 n = self.get_argument('n',default=None)
699 if n is None:
707 if n is None:
700 data = cm.start_cluster(profile)
708 data = cm.start_cluster(profile)
701 else:
709 else:
702 data = cm.start_cluster(profile,int(n))
710 data = cm.start_cluster(profile,int(n))
703 if action == 'stop':
711 if action == 'stop':
704 data = cm.stop_cluster(profile)
712 data = cm.stop_cluster(profile)
705 self.finish(jsonapi.dumps(data))
713 self.finish(jsonapi.dumps(data))
706
714
707
715
708 #-----------------------------------------------------------------------------
716 #-----------------------------------------------------------------------------
709 # RST web service handlers
717 # RST web service handlers
710 #-----------------------------------------------------------------------------
718 #-----------------------------------------------------------------------------
711
719
712
720
713 class RSTHandler(AuthenticatedHandler):
721 class RSTHandler(AuthenticatedHandler):
714
722
715 @web.authenticated
723 @web.authenticated
716 def post(self):
724 def post(self):
717 if publish_string is None:
725 if publish_string is None:
718 raise web.HTTPError(503, u'docutils not available')
726 raise web.HTTPError(503, u'docutils not available')
719 body = self.request.body.strip()
727 body = self.request.body.strip()
720 source = body
728 source = body
721 # template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html')
729 # template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html')
722 defaults = {'file_insertion_enabled': 0,
730 defaults = {'file_insertion_enabled': 0,
723 'raw_enabled': 0,
731 'raw_enabled': 0,
724 '_disable_config': 1,
732 '_disable_config': 1,
725 'stylesheet_path': 0
733 'stylesheet_path': 0
726 # 'template': template_path
734 # 'template': template_path
727 }
735 }
728 try:
736 try:
729 html = publish_string(source, writer_name='html',
737 html = publish_string(source, writer_name='html',
730 settings_overrides=defaults
738 settings_overrides=defaults
731 )
739 )
732 except:
740 except:
733 raise web.HTTPError(400, u'Invalid RST')
741 raise web.HTTPError(400, u'Invalid RST')
734 print html
742 print html
735 self.set_header('Content-Type', 'text/html')
743 self.set_header('Content-Type', 'text/html')
736 self.finish(html)
744 self.finish(html)
737
745
746 # to minimize subclass changes:
747 HTTPError = web.HTTPError
748
749 class FileFindHandler(web.StaticFileHandler):
750 """subclass of StaticFileHandler for serving files from a search path"""
751
752 _static_paths = {}
753 # _lock is needed for tornado < 2.2.0 compat
754 _lock = threading.Lock() # protects _static_hashes
755
756 def initialize(self, path, default_filename=None):
757 if isinstance(path, basestring):
758 path = [path]
759 self.roots = tuple(
760 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in path
761 )
762 self.default_filename = default_filename
763
764 @classmethod
765 def locate_file(cls, path, roots):
766 """locate a file to serve on our static file search path"""
767 with cls._lock:
768 if path in cls._static_paths:
769 return cls._static_paths[path]
770 try:
771 abspath = os.path.abspath(filefind(path, roots))
772 except IOError:
773 # empty string should always give exists=False
774 return ''
775
776 # os.path.abspath strips a trailing /
777 # it needs to be temporarily added back for requests to root/
778 if not (abspath + os.path.sep).startswith(roots):
779 raise HTTPError(403, "%s is not in root static directory", path)
780
781 cls._static_paths[path] = abspath
782 return abspath
783
784 def get(self, path, include_body=True):
785 path = self.parse_url_path(path)
786
787 # begin subclass override
788 abspath = self.locate_file(path, self.roots)
789 # end subclass override
790
791 if os.path.isdir(abspath) and self.default_filename is not None:
792 # need to look at the request.path here for when path is empty
793 # but there is some prefix to the path that was already
794 # trimmed by the routing
795 if not self.request.path.endswith("/"):
796 self.redirect(self.request.path + "/")
797 return
798 abspath = os.path.join(abspath, self.default_filename)
799 if not os.path.exists(abspath):
800 raise HTTPError(404)
801 if not os.path.isfile(abspath):
802 raise HTTPError(403, "%s is not a file", path)
803
804 stat_result = os.stat(abspath)
805 modified = datetime.datetime.fromtimestamp(stat_result[stat.ST_MTIME])
806
807 self.set_header("Last-Modified", modified)
808
809 mime_type, encoding = mimetypes.guess_type(abspath)
810 if mime_type:
811 self.set_header("Content-Type", mime_type)
812
813 cache_time = self.get_cache_time(path, modified, mime_type)
814
815 if cache_time > 0:
816 self.set_header("Expires", datetime.datetime.utcnow() + \
817 datetime.timedelta(seconds=cache_time))
818 self.set_header("Cache-Control", "max-age=" + str(cache_time))
819 else:
820 self.set_header("Cache-Control", "public")
821
822 self.set_extra_headers(path)
823
824 # Check the If-Modified-Since, and don't send the result if the
825 # content has not been modified
826 ims_value = self.request.headers.get("If-Modified-Since")
827 if ims_value is not None:
828 date_tuple = email.utils.parsedate(ims_value)
829 if_since = datetime.datetime.fromtimestamp(time.mktime(date_tuple))
830 if if_since >= modified:
831 self.set_status(304)
832 return
833
834 with open(abspath, "rb") as file:
835 data = file.read()
836 hasher = hashlib.sha1()
837 hasher.update(data)
838 self.set_header("Etag", '"%s"' % hasher.hexdigest())
839 if include_body:
840 self.write(data)
841 else:
842 assert self.request.method == "HEAD"
843 self.set_header("Content-Length", len(data))
844
845 @classmethod
846 def get_version(cls, settings, path):
847 """Generate the version string to be used in static URLs.
848
849 This method may be overridden in subclasses (but note that it
850 is a class method rather than a static method). The default
851 implementation uses a hash of the file's contents.
852
853 ``settings`` is the `Application.settings` dictionary and ``path``
854 is the relative location of the requested asset on the filesystem.
855 The returned value should be a string, or ``None`` if no version
856 could be determined.
857 """
858 # begin subclass override:
859 static_paths = settings['static_path']
860 if isinstance(static_paths, basestring):
861 static_paths = [static_paths]
862 roots = tuple(
863 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in static_paths
864 )
865
866 try:
867 abs_path = filefind(path, roots)
868 except IOError:
869 logging.error("Could not find static file %r", path)
870 return None
871
872 # end subclass override
873
874 with cls._lock:
875 hashes = cls._static_hashes
876 if abs_path not in hashes:
877 try:
878 f = open(abs_path, "rb")
879 hashes[abs_path] = hashlib.md5(f.read()).hexdigest()
880 f.close()
881 except Exception:
882 logging.error("Could not open static file %r", path)
883 hashes[abs_path] = None
884 hsh = hashes.get(abs_path)
885 if hsh:
886 return hsh[:5]
887 return None
888
889
890 # make_static_url and parse_url_path totally unchanged from tornado 2.2.0
891 # but needed for tornado < 2.2.0 compat
892 @classmethod
893 def make_static_url(cls, settings, path):
894 """Constructs a versioned url for the given path.
895
896 This method may be overridden in subclasses (but note that it is
897 a class method rather than an instance method).
898
899 ``settings`` is the `Application.settings` dictionary. ``path``
900 is the static path being requested. The url returned should be
901 relative to the current host.
902 """
903 static_url_prefix = settings.get('static_url_prefix', '/static/')
904 version_hash = cls.get_version(settings, path)
905 if version_hash:
906 return static_url_prefix + path + "?v=" + version_hash
907 return static_url_prefix + path
908
909 def parse_url_path(self, url_path):
910 """Converts a static URL path into a filesystem path.
911
912 ``url_path`` is the path component of the URL with
913 ``static_url_prefix`` removed. The return value should be
914 filesystem path relative to ``static_path``.
915 """
916 if os.path.sep != "/":
917 url_path = url_path.replace("/", os.path.sep)
918 return url_path
919
738
920
@@ -1,593 +1,611
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 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2008-2011 The IPython Development Team
9 # Copyright (C) 2008-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 # stdlib
19 # stdlib
20 import errno
20 import errno
21 import logging
21 import logging
22 import os
22 import os
23 import random
23 import random
24 import re
24 import re
25 import select
25 import select
26 import signal
26 import signal
27 import socket
27 import socket
28 import sys
28 import sys
29 import threading
29 import threading
30 import time
30 import time
31 import webbrowser
31 import webbrowser
32
32
33 # Third party
33 # Third party
34 import zmq
34 import zmq
35
35
36 # Install the pyzmq ioloop. This has to be done before anything else from
36 # Install the pyzmq ioloop. This has to be done before anything else from
37 # tornado is imported.
37 # tornado is imported.
38 from zmq.eventloop import ioloop
38 from zmq.eventloop import ioloop
39 ioloop.install()
39 ioloop.install()
40
40
41 from tornado import httpserver
41 from tornado import httpserver
42 from tornado import web
42 from tornado import web
43
43
44 # Our own libraries
44 # Our own libraries
45 from .kernelmanager import MappingKernelManager
45 from .kernelmanager import MappingKernelManager
46 from .handlers import (LoginHandler, LogoutHandler,
46 from .handlers import (LoginHandler, LogoutHandler,
47 ProjectDashboardHandler, NewHandler, NamedNotebookHandler,
47 ProjectDashboardHandler, NewHandler, NamedNotebookHandler,
48 MainKernelHandler, KernelHandler, KernelActionHandler, IOPubHandler,
48 MainKernelHandler, KernelHandler, KernelActionHandler, IOPubHandler,
49 ShellHandler, NotebookRootHandler, NotebookHandler, NotebookCopyHandler,
49 ShellHandler, NotebookRootHandler, NotebookHandler, NotebookCopyHandler,
50 RSTHandler, AuthenticatedFileHandler, PrintNotebookHandler,
50 RSTHandler, AuthenticatedFileHandler, PrintNotebookHandler,
51 MainClusterHandler, ClusterProfileHandler, ClusterActionHandler
51 MainClusterHandler, ClusterProfileHandler, ClusterActionHandler,
52 FileFindHandler,
52 )
53 )
53 from .notebookmanager import NotebookManager
54 from .notebookmanager import NotebookManager
54 from .clustermanager import ClusterManager
55 from .clustermanager import ClusterManager
55
56
56 from IPython.config.application import catch_config_error, boolean_flag
57 from IPython.config.application import catch_config_error, boolean_flag
57 from IPython.core.application import BaseIPythonApplication
58 from IPython.core.application import BaseIPythonApplication
58 from IPython.core.profiledir import ProfileDir
59 from IPython.core.profiledir import ProfileDir
59 from IPython.frontend.consoleapp import IPythonConsoleApp
60 from IPython.frontend.consoleapp import IPythonConsoleApp
60 from IPython.lib.kernel import swallow_argv
61 from IPython.lib.kernel import swallow_argv
61 from IPython.zmq.session import Session, default_secure
62 from IPython.zmq.session import Session, default_secure
62 from IPython.zmq.zmqshell import ZMQInteractiveShell
63 from IPython.zmq.zmqshell import ZMQInteractiveShell
63 from IPython.zmq.ipkernel import (
64 from IPython.zmq.ipkernel import (
64 flags as ipkernel_flags,
65 flags as ipkernel_flags,
65 aliases as ipkernel_aliases,
66 aliases as ipkernel_aliases,
66 IPKernelApp
67 IPKernelApp
67 )
68 )
68 from IPython.utils.traitlets import Dict, Unicode, Integer, List, Enum, Bool
69 from IPython.utils.traitlets import Dict, Unicode, Integer, List, Enum, Bool
69 from IPython.utils import py3compat
70 from IPython.utils import py3compat
71 from IPython.utils.path import filefind
70
72
71 #-----------------------------------------------------------------------------
73 #-----------------------------------------------------------------------------
72 # Module globals
74 # Module globals
73 #-----------------------------------------------------------------------------
75 #-----------------------------------------------------------------------------
74
76
75 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
77 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
76 _kernel_action_regex = r"(?P<action>restart|interrupt)"
78 _kernel_action_regex = r"(?P<action>restart|interrupt)"
77 _notebook_id_regex = r"(?P<notebook_id>\w+-\w+-\w+-\w+-\w+)"
79 _notebook_id_regex = r"(?P<notebook_id>\w+-\w+-\w+-\w+-\w+)"
78 _profile_regex = r"(?P<profile>[^\/]+)" # there is almost no text that is invalid
80 _profile_regex = r"(?P<profile>[^\/]+)" # there is almost no text that is invalid
79 _cluster_action_regex = r"(?P<action>start|stop)"
81 _cluster_action_regex = r"(?P<action>start|stop)"
80
82
81
83
82 LOCALHOST = '127.0.0.1'
84 LOCALHOST = '127.0.0.1'
83
85
84 _examples = """
86 _examples = """
85 ipython notebook # start the notebook
87 ipython notebook # start the notebook
86 ipython notebook --profile=sympy # use the sympy profile
88 ipython notebook --profile=sympy # use the sympy profile
87 ipython notebook --pylab=inline # pylab in inline plotting mode
89 ipython notebook --pylab=inline # pylab in inline plotting mode
88 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
90 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
89 ipython notebook --port=5555 --ip=* # Listen on port 5555, all interfaces
91 ipython notebook --port=5555 --ip=* # Listen on port 5555, all interfaces
90 """
92 """
91
93
92 #-----------------------------------------------------------------------------
94 #-----------------------------------------------------------------------------
93 # Helper functions
95 # Helper functions
94 #-----------------------------------------------------------------------------
96 #-----------------------------------------------------------------------------
95
97
96 def url_path_join(a,b):
98 def url_path_join(a,b):
97 if a.endswith('/') and b.startswith('/'):
99 if a.endswith('/') and b.startswith('/'):
98 return a[:-1]+b
100 return a[:-1]+b
99 else:
101 else:
100 return a+b
102 return a+b
101
103
102 def random_ports(port, n):
104 def random_ports(port, n):
103 """Generate a list of n random ports near the given port.
105 """Generate a list of n random ports near the given port.
104
106
105 The first 5 ports will be sequential, and the remaining n-5 will be
107 The first 5 ports will be sequential, and the remaining n-5 will be
106 randomly selected in the range [port-2*n, port+2*n].
108 randomly selected in the range [port-2*n, port+2*n].
107 """
109 """
108 for i in range(min(5, n)):
110 for i in range(min(5, n)):
109 yield port + i
111 yield port + i
110 for i in range(n-5):
112 for i in range(n-5):
111 yield port + random.randint(-2*n, 2*n)
113 yield port + random.randint(-2*n, 2*n)
112
114
113 #-----------------------------------------------------------------------------
115 #-----------------------------------------------------------------------------
114 # The Tornado web application
116 # The Tornado web application
115 #-----------------------------------------------------------------------------
117 #-----------------------------------------------------------------------------
116
118
117 class NotebookWebApplication(web.Application):
119 class NotebookWebApplication(web.Application):
118
120
119 def __init__(self, ipython_app, kernel_manager, notebook_manager,
121 def __init__(self, ipython_app, kernel_manager, notebook_manager,
120 cluster_manager, log,
122 cluster_manager, log,
121 base_project_url, settings_overrides):
123 base_project_url, settings_overrides):
122 handlers = [
124 handlers = [
123 (r"/", ProjectDashboardHandler),
125 (r"/", ProjectDashboardHandler),
124 (r"/login", LoginHandler),
126 (r"/login", LoginHandler),
125 (r"/logout", LogoutHandler),
127 (r"/logout", LogoutHandler),
126 (r"/new", NewHandler),
128 (r"/new", NewHandler),
127 (r"/%s" % _notebook_id_regex, NamedNotebookHandler),
129 (r"/%s" % _notebook_id_regex, NamedNotebookHandler),
128 (r"/%s/copy" % _notebook_id_regex, NotebookCopyHandler),
130 (r"/%s/copy" % _notebook_id_regex, NotebookCopyHandler),
129 (r"/%s/print" % _notebook_id_regex, PrintNotebookHandler),
131 (r"/%s/print" % _notebook_id_regex, PrintNotebookHandler),
130 (r"/kernels", MainKernelHandler),
132 (r"/kernels", MainKernelHandler),
131 (r"/kernels/%s" % _kernel_id_regex, KernelHandler),
133 (r"/kernels/%s" % _kernel_id_regex, KernelHandler),
132 (r"/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
134 (r"/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
133 (r"/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler),
135 (r"/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler),
134 (r"/kernels/%s/shell" % _kernel_id_regex, ShellHandler),
136 (r"/kernels/%s/shell" % _kernel_id_regex, ShellHandler),
135 (r"/notebooks", NotebookRootHandler),
137 (r"/notebooks", NotebookRootHandler),
136 (r"/notebooks/%s" % _notebook_id_regex, NotebookHandler),
138 (r"/notebooks/%s" % _notebook_id_regex, NotebookHandler),
137 (r"/rstservice/render", RSTHandler),
139 (r"/rstservice/render", RSTHandler),
138 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : notebook_manager.notebook_dir}),
140 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : notebook_manager.notebook_dir}),
139 (r"/clusters", MainClusterHandler),
141 (r"/clusters", MainClusterHandler),
140 (r"/clusters/%s/%s" % (_profile_regex, _cluster_action_regex), ClusterActionHandler),
142 (r"/clusters/%s/%s" % (_profile_regex, _cluster_action_regex), ClusterActionHandler),
141 (r"/clusters/%s" % _profile_regex, ClusterProfileHandler),
143 (r"/clusters/%s" % _profile_regex, ClusterProfileHandler),
142 ]
144 ]
143
145
144 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
146 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
145 # base_project_url will always be unicode, which will in turn
147 # base_project_url will always be unicode, which will in turn
146 # make the patterns unicode, and ultimately result in unicode
148 # make the patterns unicode, and ultimately result in unicode
147 # keys in kwargs to handler._execute(**kwargs) in tornado.
149 # keys in kwargs to handler._execute(**kwargs) in tornado.
148 # This enforces that base_project_url be ascii in that situation.
150 # This enforces that base_project_url be ascii in that situation.
149 #
151 #
150 # Note that the URLs these patterns check against are escaped,
152 # Note that the URLs these patterns check against are escaped,
151 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
153 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
152 base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii')
154 base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii')
153
155
154 settings = dict(
156 settings = dict(
155 template_path=os.path.join(os.path.dirname(__file__), "templates"),
157 template_path=os.path.join(os.path.dirname(__file__), "templates"),
156 static_path=os.path.join(os.path.dirname(__file__), "static"),
158 static_path=ipython_app.static_file_path,
159 static_handler_class = FileFindHandler,
157 cookie_secret=os.urandom(1024),
160 cookie_secret=os.urandom(1024),
158 login_url="%s/login"%(base_project_url.rstrip('/')),
161 login_url="%s/login"%(base_project_url.rstrip('/')),
159 )
162 )
160
163
161 # allow custom overrides for the tornado web app.
164 # allow custom overrides for the tornado web app.
162 settings.update(settings_overrides)
165 settings.update(settings_overrides)
163
166
164 # prepend base_project_url onto the patterns that we match
167 # prepend base_project_url onto the patterns that we match
165 new_handlers = []
168 new_handlers = []
166 for handler in handlers:
169 for handler in handlers:
167 pattern = url_path_join(base_project_url, handler[0])
170 pattern = url_path_join(base_project_url, handler[0])
168 new_handler = tuple([pattern]+list(handler[1:]))
171 new_handler = tuple([pattern]+list(handler[1:]))
169 new_handlers.append( new_handler )
172 new_handlers.append( new_handler )
170
173
171 super(NotebookWebApplication, self).__init__(new_handlers, **settings)
174 super(NotebookWebApplication, self).__init__(new_handlers, **settings)
172
175
173 self.kernel_manager = kernel_manager
176 self.kernel_manager = kernel_manager
174 self.notebook_manager = notebook_manager
177 self.notebook_manager = notebook_manager
175 self.cluster_manager = cluster_manager
178 self.cluster_manager = cluster_manager
176 self.ipython_app = ipython_app
179 self.ipython_app = ipython_app
177 self.read_only = self.ipython_app.read_only
180 self.read_only = self.ipython_app.read_only
178 self.log = log
181 self.log = log
179
182
180
183
181 #-----------------------------------------------------------------------------
184 #-----------------------------------------------------------------------------
182 # Aliases and Flags
185 # Aliases and Flags
183 #-----------------------------------------------------------------------------
186 #-----------------------------------------------------------------------------
184
187
185 flags = dict(ipkernel_flags)
188 flags = dict(ipkernel_flags)
186 flags['no-browser']=(
189 flags['no-browser']=(
187 {'NotebookApp' : {'open_browser' : False}},
190 {'NotebookApp' : {'open_browser' : False}},
188 "Don't open the notebook in a browser after startup."
191 "Don't open the notebook in a browser after startup."
189 )
192 )
190 flags['no-mathjax']=(
193 flags['no-mathjax']=(
191 {'NotebookApp' : {'enable_mathjax' : False}},
194 {'NotebookApp' : {'enable_mathjax' : False}},
192 """Disable MathJax
195 """Disable MathJax
193
196
194 MathJax is the javascript library IPython uses to render math/LaTeX. It is
197 MathJax is the javascript library IPython uses to render math/LaTeX. It is
195 very large, so you may want to disable it if you have a slow internet
198 very large, so you may want to disable it if you have a slow internet
196 connection, or for offline use of the notebook.
199 connection, or for offline use of the notebook.
197
200
198 When disabled, equations etc. will appear as their untransformed TeX source.
201 When disabled, equations etc. will appear as their untransformed TeX source.
199 """
202 """
200 )
203 )
201 flags['read-only'] = (
204 flags['read-only'] = (
202 {'NotebookApp' : {'read_only' : True}},
205 {'NotebookApp' : {'read_only' : True}},
203 """Allow read-only access to notebooks.
206 """Allow read-only access to notebooks.
204
207
205 When using a password to protect the notebook server, this flag
208 When using a password to protect the notebook server, this flag
206 allows unauthenticated clients to view the notebook list, and
209 allows unauthenticated clients to view the notebook list, and
207 individual notebooks, but not edit them, start kernels, or run
210 individual notebooks, but not edit them, start kernels, or run
208 code.
211 code.
209
212
210 If no password is set, the server will be entirely read-only.
213 If no password is set, the server will be entirely read-only.
211 """
214 """
212 )
215 )
213
216
214 # Add notebook manager flags
217 # Add notebook manager flags
215 flags.update(boolean_flag('script', 'NotebookManager.save_script',
218 flags.update(boolean_flag('script', 'NotebookManager.save_script',
216 'Auto-save a .py script everytime the .ipynb notebook is saved',
219 'Auto-save a .py script everytime the .ipynb notebook is saved',
217 'Do not auto-save .py scripts for every notebook'))
220 'Do not auto-save .py scripts for every notebook'))
218
221
219 # the flags that are specific to the frontend
222 # the flags that are specific to the frontend
220 # these must be scrubbed before being passed to the kernel,
223 # these must be scrubbed before being passed to the kernel,
221 # or it will raise an error on unrecognized flags
224 # or it will raise an error on unrecognized flags
222 notebook_flags = ['no-browser', 'no-mathjax', 'read-only', 'script', 'no-script']
225 notebook_flags = ['no-browser', 'no-mathjax', 'read-only', 'script', 'no-script']
223
226
224 aliases = dict(ipkernel_aliases)
227 aliases = dict(ipkernel_aliases)
225
228
226 aliases.update({
229 aliases.update({
227 'ip': 'NotebookApp.ip',
230 'ip': 'NotebookApp.ip',
228 'port': 'NotebookApp.port',
231 'port': 'NotebookApp.port',
229 'port-retries': 'NotebookApp.port_retries',
232 'port-retries': 'NotebookApp.port_retries',
230 'keyfile': 'NotebookApp.keyfile',
233 'keyfile': 'NotebookApp.keyfile',
231 'certfile': 'NotebookApp.certfile',
234 'certfile': 'NotebookApp.certfile',
232 'notebook-dir': 'NotebookManager.notebook_dir',
235 'notebook-dir': 'NotebookManager.notebook_dir',
233 'browser': 'NotebookApp.browser',
236 'browser': 'NotebookApp.browser',
234 })
237 })
235
238
236 # remove ipkernel flags that are singletons, and don't make sense in
239 # remove ipkernel flags that are singletons, and don't make sense in
237 # multi-kernel evironment:
240 # multi-kernel evironment:
238 aliases.pop('f', None)
241 aliases.pop('f', None)
239
242
240 notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
243 notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
241 u'notebook-dir']
244 u'notebook-dir']
242
245
243 #-----------------------------------------------------------------------------
246 #-----------------------------------------------------------------------------
244 # NotebookApp
247 # NotebookApp
245 #-----------------------------------------------------------------------------
248 #-----------------------------------------------------------------------------
246
249
247 class NotebookApp(BaseIPythonApplication):
250 class NotebookApp(BaseIPythonApplication):
248
251
249 name = 'ipython-notebook'
252 name = 'ipython-notebook'
250 default_config_file_name='ipython_notebook_config.py'
253 default_config_file_name='ipython_notebook_config.py'
251
254
252 description = """
255 description = """
253 The IPython HTML Notebook.
256 The IPython HTML Notebook.
254
257
255 This launches a Tornado based HTML Notebook Server that serves up an
258 This launches a Tornado based HTML Notebook Server that serves up an
256 HTML5/Javascript Notebook client.
259 HTML5/Javascript Notebook client.
257 """
260 """
258 examples = _examples
261 examples = _examples
259
262
260 classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager]
263 classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager]
261 flags = Dict(flags)
264 flags = Dict(flags)
262 aliases = Dict(aliases)
265 aliases = Dict(aliases)
263
266
264 kernel_argv = List(Unicode)
267 kernel_argv = List(Unicode)
265
268
266 log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'),
269 log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'),
267 default_value=logging.INFO,
270 default_value=logging.INFO,
268 config=True,
271 config=True,
269 help="Set the log level by value or name.")
272 help="Set the log level by value or name.")
270
273
271 # create requested profiles by default, if they don't exist:
274 # create requested profiles by default, if they don't exist:
272 auto_create = Bool(True)
275 auto_create = Bool(True)
273
276
274 # file to be opened in the notebook server
277 # file to be opened in the notebook server
275 file_to_run = Unicode('')
278 file_to_run = Unicode('')
276
279
277 # Network related information.
280 # Network related information.
278
281
279 ip = Unicode(LOCALHOST, config=True,
282 ip = Unicode(LOCALHOST, config=True,
280 help="The IP address the notebook server will listen on."
283 help="The IP address the notebook server will listen on."
281 )
284 )
282
285
283 def _ip_changed(self, name, old, new):
286 def _ip_changed(self, name, old, new):
284 if new == u'*': self.ip = u''
287 if new == u'*': self.ip = u''
285
288
286 port = Integer(8888, config=True,
289 port = Integer(8888, config=True,
287 help="The port the notebook server will listen on."
290 help="The port the notebook server will listen on."
288 )
291 )
289 port_retries = Integer(50, config=True,
292 port_retries = Integer(50, config=True,
290 help="The number of additional ports to try if the specified port is not available."
293 help="The number of additional ports to try if the specified port is not available."
291 )
294 )
292
295
293 certfile = Unicode(u'', config=True,
296 certfile = Unicode(u'', config=True,
294 help="""The full path to an SSL/TLS certificate file."""
297 help="""The full path to an SSL/TLS certificate file."""
295 )
298 )
296
299
297 keyfile = Unicode(u'', config=True,
300 keyfile = Unicode(u'', config=True,
298 help="""The full path to a private key file for usage with SSL/TLS."""
301 help="""The full path to a private key file for usage with SSL/TLS."""
299 )
302 )
300
303
301 password = Unicode(u'', config=True,
304 password = Unicode(u'', config=True,
302 help="""Hashed password to use for web authentication.
305 help="""Hashed password to use for web authentication.
303
306
304 To generate, type in a python/IPython shell:
307 To generate, type in a python/IPython shell:
305
308
306 from IPython.lib import passwd; passwd()
309 from IPython.lib import passwd; passwd()
307
310
308 The string should be of the form type:salt:hashed-password.
311 The string should be of the form type:salt:hashed-password.
309 """
312 """
310 )
313 )
311
314
312 open_browser = Bool(True, config=True,
315 open_browser = Bool(True, config=True,
313 help="""Whether to open in a browser after starting.
316 help="""Whether to open in a browser after starting.
314 The specific browser used is platform dependent and
317 The specific browser used is platform dependent and
315 determined by the python standard library `webbrowser`
318 determined by the python standard library `webbrowser`
316 module, unless it is overridden using the --browser
319 module, unless it is overridden using the --browser
317 (NotebookApp.browser) configuration option.
320 (NotebookApp.browser) configuration option.
318 """)
321 """)
319
322
320 browser = Unicode(u'', config=True,
323 browser = Unicode(u'', config=True,
321 help="""Specify what command to use to invoke a web
324 help="""Specify what command to use to invoke a web
322 browser when opening the notebook. If not specified, the
325 browser when opening the notebook. If not specified, the
323 default browser will be determined by the `webbrowser`
326 default browser will be determined by the `webbrowser`
324 standard library module, which allows setting of the
327 standard library module, which allows setting of the
325 BROWSER environment variable to override it.
328 BROWSER environment variable to override it.
326 """)
329 """)
327
330
328 read_only = Bool(False, config=True,
331 read_only = Bool(False, config=True,
329 help="Whether to prevent editing/execution of notebooks."
332 help="Whether to prevent editing/execution of notebooks."
330 )
333 )
331
334
332 webapp_settings = Dict(config=True,
335 webapp_settings = Dict(config=True,
333 help="Supply overrides for the tornado.web.Application that the "
336 help="Supply overrides for the tornado.web.Application that the "
334 "IPython notebook uses.")
337 "IPython notebook uses.")
335
338
336 enable_mathjax = Bool(True, config=True,
339 enable_mathjax = Bool(True, config=True,
337 help="""Whether to enable MathJax for typesetting math/TeX
340 help="""Whether to enable MathJax for typesetting math/TeX
338
341
339 MathJax is the javascript library IPython uses to render math/LaTeX. It is
342 MathJax is the javascript library IPython uses to render math/LaTeX. It is
340 very large, so you may want to disable it if you have a slow internet
343 very large, so you may want to disable it if you have a slow internet
341 connection, or for offline use of the notebook.
344 connection, or for offline use of the notebook.
342
345
343 When disabled, equations etc. will appear as their untransformed TeX source.
346 When disabled, equations etc. will appear as their untransformed TeX source.
344 """
347 """
345 )
348 )
346 def _enable_mathjax_changed(self, name, old, new):
349 def _enable_mathjax_changed(self, name, old, new):
347 """set mathjax url to empty if mathjax is disabled"""
350 """set mathjax url to empty if mathjax is disabled"""
348 if not new:
351 if not new:
349 self.mathjax_url = u''
352 self.mathjax_url = u''
350
353
351 base_project_url = Unicode('/', config=True,
354 base_project_url = Unicode('/', config=True,
352 help='''The base URL for the notebook server''')
355 help='''The base URL for the notebook server''')
353 base_kernel_url = Unicode('/', config=True,
356 base_kernel_url = Unicode('/', config=True,
354 help='''The base URL for the kernel server''')
357 help='''The base URL for the kernel server''')
355 websocket_host = Unicode("", config=True,
358 websocket_host = Unicode("", config=True,
356 help="""The hostname for the websocket server."""
359 help="""The hostname for the websocket server."""
357 )
360 )
361
362 extra_static_paths = List(Unicode, config=True,
363 help="""Extra paths to search for serving static files.
364
365 This allows adding javascript/css to be available from the notebook server machine,
366 or overriding individual files in the IPython"""
367 )
368 def _extra_static_paths_default(self):
369 return [os.path.join(self.profile_dir.location, 'static')]
370
371 @property
372 def static_file_path(self):
373 """return extra paths + the default location"""
374 return self.extra_static_paths + [os.path.join(os.path.dirname(__file__), "static")]
358
375
359 mathjax_url = Unicode("", config=True,
376 mathjax_url = Unicode("", config=True,
360 help="""The url for MathJax.js."""
377 help="""The url for MathJax.js."""
361 )
378 )
362 def _mathjax_url_default(self):
379 def _mathjax_url_default(self):
363 if not self.enable_mathjax:
380 if not self.enable_mathjax:
364 return u''
381 return u''
365 static_path = self.webapp_settings.get("static_path", os.path.join(os.path.dirname(__file__), "static"))
366 static_url_prefix = self.webapp_settings.get("static_url_prefix",
382 static_url_prefix = self.webapp_settings.get("static_url_prefix",
367 "/static/")
383 "/static/")
368 if os.path.exists(os.path.join(static_path, 'mathjax', "MathJax.js")):
384 try:
369 self.log.info("Using local MathJax")
385 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), self.static_file_path)
370 return static_url_prefix+u"mathjax/MathJax.js"
386 except IOError:
371 else:
372 if self.certfile:
387 if self.certfile:
373 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
388 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
374 base = u"https://c328740.ssl.cf1.rackcdn.com"
389 base = u"https://c328740.ssl.cf1.rackcdn.com"
375 else:
390 else:
376 base = u"http://cdn.mathjax.org"
391 base = u"http://cdn.mathjax.org"
377
392
378 url = base + u"/mathjax/latest/MathJax.js"
393 url = base + u"/mathjax/latest/MathJax.js"
379 self.log.info("Using MathJax from CDN: %s", url)
394 self.log.info("Using MathJax from CDN: %s", url)
380 return url
395 return url
396 else:
397 self.log.info("Using local MathJax from %s" % mathjax)
398 return static_url_prefix+u"mathjax/MathJax.js"
381
399
382 def _mathjax_url_changed(self, name, old, new):
400 def _mathjax_url_changed(self, name, old, new):
383 if new and not self.enable_mathjax:
401 if new and not self.enable_mathjax:
384 # enable_mathjax=False overrides mathjax_url
402 # enable_mathjax=False overrides mathjax_url
385 self.mathjax_url = u''
403 self.mathjax_url = u''
386 else:
404 else:
387 self.log.info("Using MathJax: %s", new)
405 self.log.info("Using MathJax: %s", new)
388
406
389 def parse_command_line(self, argv=None):
407 def parse_command_line(self, argv=None):
390 super(NotebookApp, self).parse_command_line(argv)
408 super(NotebookApp, self).parse_command_line(argv)
391 if argv is None:
409 if argv is None:
392 argv = sys.argv[1:]
410 argv = sys.argv[1:]
393
411
394 # Scrub frontend-specific flags
412 # Scrub frontend-specific flags
395 self.kernel_argv = swallow_argv(argv, notebook_aliases, notebook_flags)
413 self.kernel_argv = swallow_argv(argv, notebook_aliases, notebook_flags)
396 # Kernel should inherit default config file from frontend
414 # Kernel should inherit default config file from frontend
397 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
415 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
398
416
399 if self.extra_args:
417 if self.extra_args:
400 f = os.path.abspath(self.extra_args[0])
418 f = os.path.abspath(self.extra_args[0])
401 if os.path.isdir(f):
419 if os.path.isdir(f):
402 nbdir = f
420 nbdir = f
403 else:
421 else:
404 self.file_to_run = f
422 self.file_to_run = f
405 nbdir = os.path.dirname(f)
423 nbdir = os.path.dirname(f)
406 self.config.NotebookManager.notebook_dir = nbdir
424 self.config.NotebookManager.notebook_dir = nbdir
407
425
408 def init_configurables(self):
426 def init_configurables(self):
409 # force Session default to be secure
427 # force Session default to be secure
410 default_secure(self.config)
428 default_secure(self.config)
411 self.kernel_manager = MappingKernelManager(
429 self.kernel_manager = MappingKernelManager(
412 config=self.config, log=self.log, kernel_argv=self.kernel_argv,
430 config=self.config, log=self.log, kernel_argv=self.kernel_argv,
413 connection_dir = self.profile_dir.security_dir,
431 connection_dir = self.profile_dir.security_dir,
414 )
432 )
415 self.notebook_manager = NotebookManager(config=self.config, log=self.log)
433 self.notebook_manager = NotebookManager(config=self.config, log=self.log)
416 self.log.info("Serving notebooks from %s", self.notebook_manager.notebook_dir)
434 self.log.info("Serving notebooks from %s", self.notebook_manager.notebook_dir)
417 self.notebook_manager.list_notebooks()
435 self.notebook_manager.list_notebooks()
418 self.cluster_manager = ClusterManager(config=self.config, log=self.log)
436 self.cluster_manager = ClusterManager(config=self.config, log=self.log)
419 self.cluster_manager.update_profiles()
437 self.cluster_manager.update_profiles()
420
438
421 def init_logging(self):
439 def init_logging(self):
422 # This prevents double log messages because tornado use a root logger that
440 # This prevents double log messages because tornado use a root logger that
423 # self.log is a child of. The logging module dipatches log messages to a log
441 # self.log is a child of. The logging module dipatches log messages to a log
424 # and all of its ancenstors until propagate is set to False.
442 # and all of its ancenstors until propagate is set to False.
425 self.log.propagate = False
443 self.log.propagate = False
426
444
427 def init_webapp(self):
445 def init_webapp(self):
428 """initialize tornado webapp and httpserver"""
446 """initialize tornado webapp and httpserver"""
429 self.web_app = NotebookWebApplication(
447 self.web_app = NotebookWebApplication(
430 self, self.kernel_manager, self.notebook_manager,
448 self, self.kernel_manager, self.notebook_manager,
431 self.cluster_manager, self.log,
449 self.cluster_manager, self.log,
432 self.base_project_url, self.webapp_settings
450 self.base_project_url, self.webapp_settings
433 )
451 )
434 if self.certfile:
452 if self.certfile:
435 ssl_options = dict(certfile=self.certfile)
453 ssl_options = dict(certfile=self.certfile)
436 if self.keyfile:
454 if self.keyfile:
437 ssl_options['keyfile'] = self.keyfile
455 ssl_options['keyfile'] = self.keyfile
438 else:
456 else:
439 ssl_options = None
457 ssl_options = None
440 self.web_app.password = self.password
458 self.web_app.password = self.password
441 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options)
459 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options)
442 if ssl_options is None and not self.ip and not (self.read_only and not self.password):
460 if ssl_options is None and not self.ip and not (self.read_only and not self.password):
443 self.log.critical('WARNING: the notebook server is listening on all IP addresses '
461 self.log.critical('WARNING: the notebook server is listening on all IP addresses '
444 'but not using any encryption or authentication. This is highly '
462 'but not using any encryption or authentication. This is highly '
445 'insecure and not recommended.')
463 'insecure and not recommended.')
446
464
447 success = None
465 success = None
448 for port in random_ports(self.port, self.port_retries+1):
466 for port in random_ports(self.port, self.port_retries+1):
449 try:
467 try:
450 self.http_server.listen(port, self.ip)
468 self.http_server.listen(port, self.ip)
451 except socket.error as e:
469 except socket.error as e:
452 if e.errno != errno.EADDRINUSE:
470 if e.errno != errno.EADDRINUSE:
453 raise
471 raise
454 self.log.info('The port %i is already in use, trying another random port.' % port)
472 self.log.info('The port %i is already in use, trying another random port.' % port)
455 else:
473 else:
456 self.port = port
474 self.port = port
457 success = True
475 success = True
458 break
476 break
459 if not success:
477 if not success:
460 self.log.critical('ERROR: the notebook server could not be started because '
478 self.log.critical('ERROR: the notebook server could not be started because '
461 'no available port could be found.')
479 'no available port could be found.')
462 self.exit(1)
480 self.exit(1)
463
481
464 def init_signal(self):
482 def init_signal(self):
465 # FIXME: remove this check when pyzmq dependency is >= 2.1.11
483 # FIXME: remove this check when pyzmq dependency is >= 2.1.11
466 # safely extract zmq version info:
484 # safely extract zmq version info:
467 try:
485 try:
468 zmq_v = zmq.pyzmq_version_info()
486 zmq_v = zmq.pyzmq_version_info()
469 except AttributeError:
487 except AttributeError:
470 zmq_v = [ int(n) for n in re.findall(r'\d+', zmq.__version__) ]
488 zmq_v = [ int(n) for n in re.findall(r'\d+', zmq.__version__) ]
471 if 'dev' in zmq.__version__:
489 if 'dev' in zmq.__version__:
472 zmq_v.append(999)
490 zmq_v.append(999)
473 zmq_v = tuple(zmq_v)
491 zmq_v = tuple(zmq_v)
474 if zmq_v >= (2,1,9) and not sys.platform.startswith('win'):
492 if zmq_v >= (2,1,9) and not sys.platform.startswith('win'):
475 # This won't work with 2.1.7 and
493 # This won't work with 2.1.7 and
476 # 2.1.9-10 will log ugly 'Interrupted system call' messages,
494 # 2.1.9-10 will log ugly 'Interrupted system call' messages,
477 # but it will work
495 # but it will work
478 signal.signal(signal.SIGINT, self._handle_sigint)
496 signal.signal(signal.SIGINT, self._handle_sigint)
479 signal.signal(signal.SIGTERM, self._signal_stop)
497 signal.signal(signal.SIGTERM, self._signal_stop)
480
498
481 def _handle_sigint(self, sig, frame):
499 def _handle_sigint(self, sig, frame):
482 """SIGINT handler spawns confirmation dialog"""
500 """SIGINT handler spawns confirmation dialog"""
483 # register more forceful signal handler for ^C^C case
501 # register more forceful signal handler for ^C^C case
484 signal.signal(signal.SIGINT, self._signal_stop)
502 signal.signal(signal.SIGINT, self._signal_stop)
485 # request confirmation dialog in bg thread, to avoid
503 # request confirmation dialog in bg thread, to avoid
486 # blocking the App
504 # blocking the App
487 thread = threading.Thread(target=self._confirm_exit)
505 thread = threading.Thread(target=self._confirm_exit)
488 thread.daemon = True
506 thread.daemon = True
489 thread.start()
507 thread.start()
490
508
491 def _restore_sigint_handler(self):
509 def _restore_sigint_handler(self):
492 """callback for restoring original SIGINT handler"""
510 """callback for restoring original SIGINT handler"""
493 signal.signal(signal.SIGINT, self._handle_sigint)
511 signal.signal(signal.SIGINT, self._handle_sigint)
494
512
495 def _confirm_exit(self):
513 def _confirm_exit(self):
496 """confirm shutdown on ^C
514 """confirm shutdown on ^C
497
515
498 A second ^C, or answering 'y' within 5s will cause shutdown,
516 A second ^C, or answering 'y' within 5s will cause shutdown,
499 otherwise original SIGINT handler will be restored.
517 otherwise original SIGINT handler will be restored.
500
518
501 This doesn't work on Windows.
519 This doesn't work on Windows.
502 """
520 """
503 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
521 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
504 time.sleep(0.1)
522 time.sleep(0.1)
505 sys.stdout.write("Shutdown Notebook Server (y/[n])? ")
523 sys.stdout.write("Shutdown Notebook Server (y/[n])? ")
506 sys.stdout.flush()
524 sys.stdout.flush()
507 r,w,x = select.select([sys.stdin], [], [], 5)
525 r,w,x = select.select([sys.stdin], [], [], 5)
508 if r:
526 if r:
509 line = sys.stdin.readline()
527 line = sys.stdin.readline()
510 if line.lower().startswith('y'):
528 if line.lower().startswith('y'):
511 self.log.critical("Shutdown confirmed")
529 self.log.critical("Shutdown confirmed")
512 ioloop.IOLoop.instance().stop()
530 ioloop.IOLoop.instance().stop()
513 return
531 return
514 else:
532 else:
515 print "No answer for 5s:",
533 print "No answer for 5s:",
516 print "resuming operation..."
534 print "resuming operation..."
517 # no answer, or answer is no:
535 # no answer, or answer is no:
518 # set it back to original SIGINT handler
536 # set it back to original SIGINT handler
519 # use IOLoop.add_callback because signal.signal must be called
537 # use IOLoop.add_callback because signal.signal must be called
520 # from main thread
538 # from main thread
521 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
539 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
522
540
523 def _signal_stop(self, sig, frame):
541 def _signal_stop(self, sig, frame):
524 self.log.critical("received signal %s, stopping", sig)
542 self.log.critical("received signal %s, stopping", sig)
525 ioloop.IOLoop.instance().stop()
543 ioloop.IOLoop.instance().stop()
526
544
527 @catch_config_error
545 @catch_config_error
528 def initialize(self, argv=None):
546 def initialize(self, argv=None):
529 self.init_logging()
547 self.init_logging()
530 super(NotebookApp, self).initialize(argv)
548 super(NotebookApp, self).initialize(argv)
531 self.init_configurables()
549 self.init_configurables()
532 self.init_webapp()
550 self.init_webapp()
533 self.init_signal()
551 self.init_signal()
534
552
535 def cleanup_kernels(self):
553 def cleanup_kernels(self):
536 """shutdown all kernels
554 """shutdown all kernels
537
555
538 The kernels will shutdown themselves when this process no longer exists,
556 The kernels will shutdown themselves when this process no longer exists,
539 but explicit shutdown allows the KernelManagers to cleanup the connection files.
557 but explicit shutdown allows the KernelManagers to cleanup the connection files.
540 """
558 """
541 self.log.info('Shutting down kernels')
559 self.log.info('Shutting down kernels')
542 km = self.kernel_manager
560 km = self.kernel_manager
543 # copy list, since shutdown_kernel deletes keys
561 # copy list, since shutdown_kernel deletes keys
544 for kid in list(km.kernel_ids):
562 for kid in list(km.kernel_ids):
545 km.shutdown_kernel(kid)
563 km.shutdown_kernel(kid)
546
564
547 def start(self):
565 def start(self):
548 ip = self.ip if self.ip else '[all ip addresses on your system]'
566 ip = self.ip if self.ip else '[all ip addresses on your system]'
549 proto = 'https' if self.certfile else 'http'
567 proto = 'https' if self.certfile else 'http'
550 info = self.log.info
568 info = self.log.info
551 info("The IPython Notebook is running at: %s://%s:%i%s" %
569 info("The IPython Notebook is running at: %s://%s:%i%s" %
552 (proto, ip, self.port,self.base_project_url) )
570 (proto, ip, self.port,self.base_project_url) )
553 info("Use Control-C to stop this server and shut down all kernels.")
571 info("Use Control-C to stop this server and shut down all kernels.")
554
572
555 if self.open_browser or self.file_to_run:
573 if self.open_browser or self.file_to_run:
556 ip = self.ip or '127.0.0.1'
574 ip = self.ip or '127.0.0.1'
557 try:
575 try:
558 browser = webbrowser.get(self.browser or None)
576 browser = webbrowser.get(self.browser or None)
559 except webbrowser.Error as e:
577 except webbrowser.Error as e:
560 self.log.warn('No web browser found: %s.' % e)
578 self.log.warn('No web browser found: %s.' % e)
561 browser = None
579 browser = None
562
580
563 if self.file_to_run:
581 if self.file_to_run:
564 filename, _ = os.path.splitext(os.path.basename(self.file_to_run))
582 filename, _ = os.path.splitext(os.path.basename(self.file_to_run))
565 for nb in self.notebook_manager.list_notebooks():
583 for nb in self.notebook_manager.list_notebooks():
566 if filename == nb['name']:
584 if filename == nb['name']:
567 url = nb['notebook_id']
585 url = nb['notebook_id']
568 break
586 break
569 else:
587 else:
570 url = ''
588 url = ''
571 else:
589 else:
572 url = ''
590 url = ''
573 if browser:
591 if browser:
574 b = lambda : browser.open("%s://%s:%i%s%s" % (proto, ip,
592 b = lambda : browser.open("%s://%s:%i%s%s" % (proto, ip,
575 self.port, self.base_project_url, url), new=2)
593 self.port, self.base_project_url, url), new=2)
576 threading.Thread(target=b).start()
594 threading.Thread(target=b).start()
577 try:
595 try:
578 ioloop.IOLoop.instance().start()
596 ioloop.IOLoop.instance().start()
579 except KeyboardInterrupt:
597 except KeyboardInterrupt:
580 info("Interrupted...")
598 info("Interrupted...")
581 finally:
599 finally:
582 self.cleanup_kernels()
600 self.cleanup_kernels()
583
601
584
602
585 #-----------------------------------------------------------------------------
603 #-----------------------------------------------------------------------------
586 # Main entry point
604 # Main entry point
587 #-----------------------------------------------------------------------------
605 #-----------------------------------------------------------------------------
588
606
589 def launch_new_instance():
607 def launch_new_instance():
590 app = NotebookApp.instance()
608 app = NotebookApp.instance()
591 app.initialize()
609 app.initialize()
592 app.start()
610 app.start()
593
611
@@ -1,250 +1,250
1 {% extends page.html %}
1 {% extends page.html %}
2 {% block stylesheet %}
2 {% block stylesheet %}
3
3
4 {% if mathjax_url %}
4 {% if mathjax_url %}
5 <script type="text/javascript" src="{{mathjax_url}}?config=TeX-AMS_HTML" charset="utf-8"></script>
5 <script type="text/javascript" src="{{mathjax_url}}?config=TeX-AMS_HTML" charset="utf-8"></script>
6 {% end %}
6 {% end %}
7 <script type="text/javascript">
7 <script type="text/javascript">
8 // MathJax disabled, set as null to distingish from *missing* MathJax,
8 // MathJax disabled, set as null to distingish from *missing* MathJax,
9 // where it will be undefined, and should prompt a dialog later.
9 // where it will be undefined, and should prompt a dialog later.
10 window.mathjax_url = "{{mathjax_url}}";
10 window.mathjax_url = "{{mathjax_url}}";
11 </script>
11 </script>
12
12
13 <link rel="stylesheet" href="{{ static_url("codemirror/lib/codemirror.css") }}">
13 <link rel="stylesheet" href="{{ static_url("codemirror/lib/codemirror.css") }}">
14 <link rel="stylesheet" href="{{ static_url("codemirror/theme/ipython.css") }}">
14 <link rel="stylesheet" href="{{ static_url("codemirror/theme/ipython.css") }}">
15
15
16 <link rel="stylesheet" href="{{ static_url("prettify/prettify.css") }}"/>
16 <link rel="stylesheet" href="{{ static_url("prettify/prettify.css") }}"/>
17
17
18 <link rel="stylesheet" href="{{ static_url("css/notebook.css") }}" type="text/css" />
18 <link rel="stylesheet" href="{{ static_url("css/notebook.css") }}" type="text/css" />
19 <link rel="stylesheet" href="{{ static_url("css/tooltip.css") }}" type="text/css" />
19 <link rel="stylesheet" href="{{ static_url("css/tooltip.css") }}" type="text/css" />
20 <link rel="stylesheet" href="{{ static_url("css/renderedhtml.css") }}" type="text/css" />
20 <link rel="stylesheet" href="{{ static_url("css/renderedhtml.css") }}" type="text/css" />
21
21
22 <link rel="stylesheet" href="{{ static_url("css/printnotebook.css") }}" type="text/css" media="print"/>
22 <link rel="stylesheet" href="{{ static_url("css/printnotebook.css") }}" type="text/css" media="print"/>
23
23
24 {% end %}
24 {% end %}
25
25
26
26
27 {% block params %}
27 {% block params %}
28
28
29 data-project={{project}}
29 data-project={{project}}
30 data-base-project-url={{base_project_url}}
30 data-base-project-url={{base_project_url}}
31 data-base-kernel-url={{base_kernel_url}}
31 data-base-kernel-url={{base_kernel_url}}
32 data-read-only={{read_only and not logged_in}}
32 data-read-only={{read_only and not logged_in}}
33 data-notebook-id={{notebook_id}}
33 data-notebook-id={{notebook_id}}
34
34
35 {% end %}
35 {% end %}
36
36
37
37
38 {% block header %}
38 {% block header %}
39
39
40 <span id="save_widget">
40 <span id="save_widget">
41 <span id="notebook_name"></span>
41 <span id="notebook_name"></span>
42 <span id="save_status"></span>
42 <span id="save_status"></span>
43 </span>
43 </span>
44
44
45 {% end %}
45 {% end %}
46
46
47
47
48 {% block site %}
48 {% block site %}
49
49
50 <div id="menubar_container">
50 <div id="menubar_container">
51 <div id="menubar">
51 <div id="menubar">
52 <ul id="menus">
52 <ul id="menus">
53 <li><a href="#">File</a>
53 <li><a href="#">File</a>
54 <ul>
54 <ul>
55 <li id="new_notebook"><a href="#">New</a></li>
55 <li id="new_notebook"><a href="#">New</a></li>
56 <li id="open_notebook"><a href="#">Open...</a></li>
56 <li id="open_notebook"><a href="#">Open...</a></li>
57 <hr/>
57 <hr/>
58 <li id="copy_notebook"><a href="#">Make a Copy...</a></li>
58 <li id="copy_notebook"><a href="#">Make a Copy...</a></li>
59 <li id="rename_notebook"><a href="#">Rename...</a></li>
59 <li id="rename_notebook"><a href="#">Rename...</a></li>
60 <li id="save_notebook"><a href="#">Save</a></li>
60 <li id="save_notebook"><a href="#">Save</a></li>
61 <hr/>
61 <hr/>
62 <li><a href="#">Download as</a>
62 <li><a href="#">Download as</a>
63 <ul>
63 <ul>
64 <li id="download_ipynb"><a href="#">IPython (.ipynb)</a></li>
64 <li id="download_ipynb"><a href="#">IPython (.ipynb)</a></li>
65 <li id="download_py"><a href="#">Python (.py)</a></li>
65 <li id="download_py"><a href="#">Python (.py)</a></li>
66 </ul>
66 </ul>
67 </li>
67 </li>
68 <hr/>
68 <hr/>
69 <li id="print_notebook"><a href="/{{notebook_id}}/print" target="_blank">Print View</a></li>
69 <li id="print_notebook"><a href="/{{notebook_id}}/print" target="_blank">Print View</a></li>
70 <hr/>
70 <hr/>
71 <li id="kill_and_exit"><a href="#" >Close and halt</a></li>
71 <li id="kill_and_exit"><a href="#" >Close and halt</a></li>
72 </ul>
72 </ul>
73 </li>
73 </li>
74 <li><a href="#">Edit</a>
74 <li><a href="#">Edit</a>
75 <ul>
75 <ul>
76 <li id="cut_cell"><a href="#">Cut Cell</a></li>
76 <li id="cut_cell"><a href="#">Cut Cell</a></li>
77 <li id="copy_cell"><a href="#">Copy Cell</a></li>
77 <li id="copy_cell"><a href="#">Copy Cell</a></li>
78 <li id="paste_cell" class="ui-state-disabled"><a href="#">Paste Cell</a></li>
78 <li id="paste_cell" class="ui-state-disabled"><a href="#">Paste Cell</a></li>
79 <li id="paste_cell_above" class="ui-state-disabled"><a href="#">Paste Cell Above</a></li>
79 <li id="paste_cell_above" class="ui-state-disabled"><a href="#">Paste Cell Above</a></li>
80 <li id="paste_cell_below" class="ui-state-disabled"><a href="#">Paste Cell Below</a></li>
80 <li id="paste_cell_below" class="ui-state-disabled"><a href="#">Paste Cell Below</a></li>
81 <li id="delete_cell"><a href="#">Delete</a></li>
81 <li id="delete_cell"><a href="#">Delete</a></li>
82 <hr/>
82 <hr/>
83 <li id="split_cell"><a href="#">Split Cell</a></li>
83 <li id="split_cell"><a href="#">Split Cell</a></li>
84 <li id="merge_cell_above"><a href="#">Merge Cell Above</a></li>
84 <li id="merge_cell_above"><a href="#">Merge Cell Above</a></li>
85 <li id="merge_cell_below"><a href="#">Merge Cell Below</a></li>
85 <li id="merge_cell_below"><a href="#">Merge Cell Below</a></li>
86 <hr/>
86 <hr/>
87 <li id="move_cell_up"><a href="#">Move Cell Up</a></li>
87 <li id="move_cell_up"><a href="#">Move Cell Up</a></li>
88 <li id="move_cell_down"><a href="#">Move Cell Down</a></li>
88 <li id="move_cell_down"><a href="#">Move Cell Down</a></li>
89 <hr/>
89 <hr/>
90 <li id="select_previous"><a href="#">Select Previous Cell</a></li>
90 <li id="select_previous"><a href="#">Select Previous Cell</a></li>
91 <li id="select_next"><a href="#">Select Next Cell</a></li>
91 <li id="select_next"><a href="#">Select Next Cell</a></li>
92 </ul>
92 </ul>
93 </li>
93 </li>
94 <li><a href="#">View</a>
94 <li><a href="#">View</a>
95 <ul>
95 <ul>
96 <li id="toggle_header"><a href="#">Toggle Header</a></li>
96 <li id="toggle_header"><a href="#">Toggle Header</a></li>
97 <li id="toggle_toolbar"><a href="#">Toggle Toolbar</a></li>
97 <li id="toggle_toolbar"><a href="#">Toggle Toolbar</a></li>
98 </ul>
98 </ul>
99 </li>
99 </li>
100 <li><a href="#">Insert</a>
100 <li><a href="#">Insert</a>
101 <ul>
101 <ul>
102 <li id="insert_cell_above"><a href="#">Insert Cell Above</a></li>
102 <li id="insert_cell_above"><a href="#">Insert Cell Above</a></li>
103 <li id="insert_cell_below"><a href="#">Insert Cell Below</a></li>
103 <li id="insert_cell_below"><a href="#">Insert Cell Below</a></li>
104 </ul>
104 </ul>
105 </li>
105 </li>
106 <li><a href="#">Cell</a>
106 <li><a href="#">Cell</a>
107 <ul>
107 <ul>
108 <li id="run_cell"><a href="#">Run</a></li>
108 <li id="run_cell"><a href="#">Run</a></li>
109 <li id="run_cell_in_place"><a href="#">Run in Place</a></li>
109 <li id="run_cell_in_place"><a href="#">Run in Place</a></li>
110 <li id="run_all_cells"><a href="#">Run All</a></li>
110 <li id="run_all_cells"><a href="#">Run All</a></li>
111 <hr/>
111 <hr/>
112 <li id="to_code"><a href="#">Code</a></li>
112 <li id="to_code"><a href="#">Code</a></li>
113 <li id="to_markdown"><a href="#">Markdown </a></li>
113 <li id="to_markdown"><a href="#">Markdown </a></li>
114 <li id="to_raw"><a href="#">Raw Text</a></li>
114 <li id="to_raw"><a href="#">Raw Text</a></li>
115 <li id="to_heading1"><a href="#">Heading 1</a></li>
115 <li id="to_heading1"><a href="#">Heading 1</a></li>
116 <li id="to_heading2"><a href="#">Heading 2</a></li>
116 <li id="to_heading2"><a href="#">Heading 2</a></li>
117 <li id="to_heading3"><a href="#">Heading 3</a></li>
117 <li id="to_heading3"><a href="#">Heading 3</a></li>
118 <li id="to_heading4"><a href="#">Heading 4</a></li>
118 <li id="to_heading4"><a href="#">Heading 4</a></li>
119 <li id="to_heading5"><a href="#">Heading 5</a></li>
119 <li id="to_heading5"><a href="#">Heading 5</a></li>
120 <li id="to_heading6"><a href="#">Heading 6</a></li>
120 <li id="to_heading6"><a href="#">Heading 6</a></li>
121 <hr/>
121 <hr/>
122 <li id="toggle_output"><a href="#">Toggle Current Output</a></li>
122 <li id="toggle_output"><a href="#">Toggle Current Output</a></li>
123 <li id="all_outputs"><a href="#">All Output</a>
123 <li id="all_outputs"><a href="#">All Output</a>
124 <ul>
124 <ul>
125 <li id="expand_all_output"><a href="#">Expand</a></li>
125 <li id="expand_all_output"><a href="#">Expand</a></li>
126 <li id="scroll_all_output"><a href="#">Scroll Long</a></li>
126 <li id="scroll_all_output"><a href="#">Scroll Long</a></li>
127 <li id="collapse_all_output"><a href="#">Collapse</a></li>
127 <li id="collapse_all_output"><a href="#">Collapse</a></li>
128 <li id="clear_all_output"><a href="#">Clear</a></li>
128 <li id="clear_all_output"><a href="#">Clear</a></li>
129 </ul>
129 </ul>
130 </li>
130 </li>
131 </ul>
131 </ul>
132 </li>
132 </li>
133 <li><a href="#">Kernel</a>
133 <li><a href="#">Kernel</a>
134 <ul>
134 <ul>
135 <li id="int_kernel"><a href="#">Interrupt</a></li>
135 <li id="int_kernel"><a href="#">Interrupt</a></li>
136 <li id="restart_kernel"><a href="#">Restart</a></li>
136 <li id="restart_kernel"><a href="#">Restart</a></li>
137 </ul>
137 </ul>
138 </li>
138 </li>
139 <li><a href="#">Help</a>
139 <li><a href="#">Help</a>
140 <ul>
140 <ul>
141 <li><a href="http://ipython.org/documentation.html" target="_blank">IPython Help</a></li>
141 <li><a href="http://ipython.org/documentation.html" target="_blank">IPython Help</a></li>
142 <li><a href="http://ipython.org/ipython-doc/stable/interactive/htmlnotebook.html" target="_blank">Notebook Help</a></li>
142 <li><a href="http://ipython.org/ipython-doc/stable/interactive/htmlnotebook.html" target="_blank">Notebook Help</a></li>
143 <li id="keyboard_shortcuts"><a href="#">Keyboard Shortcuts</a></li>
143 <li id="keyboard_shortcuts"><a href="#">Keyboard Shortcuts</a></li>
144 <hr/>
144 <hr/>
145 <li><a href="http://docs.python.org" target="_blank">Python</a></li>
145 <li><a href="http://docs.python.org" target="_blank">Python</a></li>
146 <li><a href="http://docs.scipy.org/doc/numpy/reference/" target="_blank">NumPy</a></li>
146 <li><a href="http://docs.scipy.org/doc/numpy/reference/" target="_blank">NumPy</a></li>
147 <li><a href="http://docs.scipy.org/doc/scipy/reference/" target="_blank">SciPy</a></li>
147 <li><a href="http://docs.scipy.org/doc/scipy/reference/" target="_blank">SciPy</a></li>
148 <li><a href="http://docs.sympy.org/dev/index.html" target="_blank">SymPy</a></li>
148 <li><a href="http://docs.sympy.org/dev/index.html" target="_blank">SymPy</a></li>
149 <li><a href="http://matplotlib.sourceforge.net/" target="_blank">Matplotlib</a></li>
149 <li><a href="http://matplotlib.sourceforge.net/" target="_blank">Matplotlib</a></li>
150 </ul>
150 </ul>
151 </li>
151 </li>
152 </ul>
152 </ul>
153
153
154 </div>
154 </div>
155 <div id="notification"></div>
155 <div id="notification"></div>
156 </div>
156 </div>
157
157
158
158
159 <div id="toolbar">
159 <div id="toolbar">
160
160
161 <span>
161 <span>
162 <button id="save_b">Save</button>
162 <button id="save_b">Save</button>
163 </span>
163 </span>
164 <span id="cut_copy_paste">
164 <span id="cut_copy_paste">
165 <button id="cut_b" title="Cut Cell">Cut Cell</button>
165 <button id="cut_b" title="Cut Cell">Cut Cell</button>
166 <button id="copy_b" title="Copy Cell">Copy Cell</button>
166 <button id="copy_b" title="Copy Cell">Copy Cell</button>
167 <button id="paste_b" title="Paste Cell">Paste Cell</button>
167 <button id="paste_b" title="Paste Cell">Paste Cell</button>
168 </span>
168 </span>
169 <span id="move_up_down">
169 <span id="move_up_down">
170 <button id="move_up_b" title="Move Cell Up">Move Cell Up</button>
170 <button id="move_up_b" title="Move Cell Up">Move Cell Up</button>
171 <button id="move_down_b" title="Move Cell Down">Move Down</button>
171 <button id="move_down_b" title="Move Cell Down">Move Down</button>
172 </span>
172 </span>
173 <span id="insert_above_below">
173 <span id="insert_above_below">
174 <button id="insert_above_b" title="Insert Cell Above">Insert Cell Above</button>
174 <button id="insert_above_b" title="Insert Cell Above">Insert Cell Above</button>
175 <button id="insert_below_b" title="Insert Cell Below">Insert Cell Below</button>
175 <button id="insert_below_b" title="Insert Cell Below">Insert Cell Below</button>
176 </span>
176 </span>
177 <span id="run_int">
177 <span id="run_int">
178 <button id="run_b" title="Run Cell">Run Cell</button>
178 <button id="run_b" title="Run Cell">Run Cell</button>
179 <button id="interrupt_b" title="Interrupt">Interrupt</button>
179 <button id="interrupt_b" title="Interrupt">Interrupt</button>
180 </span>
180 </span>
181 <span>
181 <span>
182 <select id="cell_type">
182 <select id="cell_type">
183 <option value="code">Code</option>
183 <option value="code">Code</option>
184 <option value="markdown">Markdown</option>
184 <option value="markdown">Markdown</option>
185 <option value="raw">Raw Text</option>
185 <option value="raw">Raw Text</option>
186 <option value="heading1">Heading 1</option>
186 <option value="heading1">Heading 1</option>
187 <option value="heading2">Heading 2</option>
187 <option value="heading2">Heading 2</option>
188 <option value="heading3">Heading 3</option>
188 <option value="heading3">Heading 3</option>
189 <option value="heading4">Heading 4</option>
189 <option value="heading4">Heading 4</option>
190 <option value="heading5">Heading 5</option>
190 <option value="heading5">Heading 5</option>
191 <option value="heading6">Heading 6</option>
191 <option value="heading6">Heading 6</option>
192 </select>
192 </select>
193 </span>
193 </span>
194
194
195 </div>
195 </div>
196
196
197 <div id="main_app">
197 <div id="main_app">
198
198
199 <div id="notebook_panel">
199 <div id="notebook_panel">
200 <div id="notebook"></div>
200 <div id="notebook"></div>
201 <div id="pager_splitter"></div>
201 <div id="pager_splitter"></div>
202 <div id="pager"></div>
202 <div id="pager"></div>
203 </div>
203 </div>
204
204
205 </div>
205 </div>
206 <div id='tooltip' class='tooltip ui-corner-all' style='display:none'></div>
206 <div id='tooltip' class='tooltip ui-corner-all' style='display:none'></div>
207
207
208
208
209 {% end %}
209 {% end %}
210
210
211
211
212 {% block script %}
212 {% block script %}
213
213
214 <script src="{{ static_url("codemirror/lib/codemirror.js") }}" charset="utf-8"></script>
214 <script src="{{ static_url("codemirror/lib/codemirror.js") }}" charset="utf-8"></script>
215 <script src="{{ static_url("codemirror/mode/python/python.js") }}" charset="utf-8"></script>
215 <script src="{{ static_url("codemirror/mode/python/python.js") }}" charset="utf-8"></script>
216 <script src="{{ static_url("codemirror/mode/htmlmixed/htmlmixed.js") }}" charset="utf-8"></script>
216 <script src="{{ static_url("codemirror/mode/htmlmixed/htmlmixed.js") }}" charset="utf-8"></script>
217 <script src="{{ static_url("codemirror/mode/xml/xml.js") }}" charset="utf-8"></script>
217 <script src="{{ static_url("codemirror/mode/xml/xml.js") }}" charset="utf-8"></script>
218 <script src="{{ static_url("codemirror/mode/javascript/javascript.js") }}" charset="utf-8"></script>
218 <script src="{{ static_url("codemirror/mode/javascript/javascript.js") }}" charset="utf-8"></script>
219 <script src="{{ static_url("codemirror/mode/css/css.js") }}" charset="utf-8"></script>
219 <script src="{{ static_url("codemirror/mode/css/css.js") }}" charset="utf-8"></script>
220 <script src="{{ static_url("codemirror/mode/rst/rst.js") }}" charset="utf-8"></script>
220 <script src="{{ static_url("codemirror/mode/rst/rst.js") }}" charset="utf-8"></script>
221 <script src="{{ static_url("codemirror/mode/markdown/markdown.js") }}" charset="utf-8"></script>
221 <script src="{{ static_url("codemirror/mode/markdown/markdown.js") }}" charset="utf-8"></script>
222
222
223 <script src="{{ static_url("pagedown/Markdown.Converter.js") }}" charset="utf-8"></script>
223 <script src="{{ static_url("pagedown/Markdown.Converter.js") }}" charset="utf-8"></script>
224
224
225 <script src="{{ static_url("prettify/prettify.js") }}" charset="utf-8"></script>
225 <script src="{{ static_url("prettify/prettify.js") }}" charset="utf-8"></script>
226 <script src="{{ static_url("dateformat/date.format.js") }}" charset="utf-8"></script>
226 <script src="{{ static_url("dateformat/date.format.js") }}" charset="utf-8"></script>
227
227
228 <script src="{{ static_url("js/events.js") }}" type="text/javascript" charset="utf-8"></script>
228 <script src="{{ static_url("js/events.js") }}" type="text/javascript" charset="utf-8"></script>
229 <script src="{{ static_url("js/utils.js") }}" type="text/javascript" charset="utf-8"></script>
229 <script src="{{ static_url("js/utils.js") }}" type="text/javascript" charset="utf-8"></script>
230 <script src="{{ static_url("js/layoutmanager.js") }}" type="text/javascript" charset="utf-8"></script>
230 <script src="{{ static_url("js/layoutmanager.js") }}" type="text/javascript" charset="utf-8"></script>
231 <script src="{{ static_url("js/initmathjax.js") }}" type="text/javascript" charset="utf-8"></script>
231 <script src="{{ static_url("js/initmathjax.js") }}" type="text/javascript" charset="utf-8"></script>
232 <script src="{{ static_url("js/outputarea.js") }}" type="text/javascript" charset="utf-8"></script>
232 <script src="{{ static_url("js/outputarea.js") }}" type="text/javascript" charset="utf-8"></script>
233 <script src="{{ static_url("js/cell.js") }}" type="text/javascript" charset="utf-8"></script>
233 <script src="{{ static_url("js/cell.js") }}" type="text/javascript" charset="utf-8"></script>
234 <script src="{{ static_url("js/codecell.js") }}" type="text/javascript" charset="utf-8"></script>
234 <script src="{{ static_url("js/codecell.js") }}" type="text/javascript" charset="utf-8"></script>
235 <script src="{{ static_url("js/completer.js") }}" type="text/javascript" charset="utf-8"></script>
235 <script src="{{ static_url("js/completer.js") }}" type="text/javascript" charset="utf-8"></script>
236 <script src="{{ static_url("js/textcell.js") }}" type="text/javascript" charset="utf-8"></script>
236 <script src="{{ static_url("js/textcell.js") }}" type="text/javascript" charset="utf-8"></script>
237 <script src="{{ static_url("js/kernel.js") }}" type="text/javascript" charset="utf-8"></script>
237 <script src="{{ static_url("js/kernel.js") }}" type="text/javascript" charset="utf-8"></script>
238 <script src="{{ static_url("js/savewidget.js") }}" type="text/javascript" charset="utf-8"></script>
238 <script src="{{ static_url("js/savewidget.js") }}" type="text/javascript" charset="utf-8"></script>
239 <script src="{{ static_url("js/quickhelp.js") }}" type="text/javascript" charset="utf-8"></script>
239 <script src="{{ static_url("js/quickhelp.js") }}" type="text/javascript" charset="utf-8"></script>
240 <script src="{{ static_url("js/pager.js") }}" type="text/javascript" charset="utf-8"></script>
240 <script src="{{ static_url("js/pager.js") }}" type="text/javascript" charset="utf-8"></script>
241 <script src="{{ static_url("js/menubar.js") }}" type="text/javascript" charset="utf-8"></script>
241 <script src="{{ static_url("js/menubar.js") }}" type="text/javascript" charset="utf-8"></script>
242 <script src="{{ static_url("js/toolbar.js") }}" type="text/javascript" charset="utf-8"></script>
242 <script src="{{ static_url("js/toolbar.js") }}" type="text/javascript" charset="utf-8"></script>
243 <script src="{{ static_url("js/notebook.js") }}" type="text/javascript" charset="utf-8"></script>
243 <script src="{{ static_url("js/notebook.js") }}" type="text/javascript" charset="utf-8"></script>
244 <script src="{{ static_url("js/notificationwidget.js") }}" type="text/javascript" charset="utf-8"></script>
244 <script src="{{ static_url("js/notificationwidget.js") }}" type="text/javascript" charset="utf-8"></script>
245 <script src="{{ static_url("js/tooltip.js") }}" type="text/javascript" charset="utf-8"></script>
245 <script src="{{ static_url("js/tooltip.js") }}" type="text/javascript" charset="utf-8"></script>
246 <script src="{{ static_url("js/notebookmain.js") }}" type="text/javascript" charset="utf-8"></script>
246 <script src="{{ static_url("js/notebookmain.js") }}" type="text/javascript" charset="utf-8"></script>
247
247
248 <script src="{{ static_url("js/contexthint.js") }} charset="utf-8"></script>
248 <script src="{{ static_url("js/contexthint.js") }}" charset="utf-8"></script>
249
249
250 {% end %}
250 {% end %}
@@ -1,58 +1,62
1 <!DOCTYPE HTML>
1 <!DOCTYPE HTML>
2 <html>
2 <html>
3
3
4 <head>
4 <head>
5 <meta charset="utf-8">
5 <meta charset="utf-8">
6
6
7 <title>{% block title %}IPython Notebook{% end %}</title>
7 <title>{% block title %}IPython Notebook{% end %}</title>
8
8
9 <link rel="stylesheet" href="{{static_url("jquery/css/themes/base/jquery-ui.min.css") }}" type="text/css" />
9 <link rel="stylesheet" href="{{static_url("jquery/css/themes/base/jquery-ui.min.css") }}" type="text/css" />
10 <link rel="stylesheet" href="{{static_url("css/boilerplate.css") }}" type="text/css" />
10 <link rel="stylesheet" href="{{static_url("css/boilerplate.css") }}" type="text/css" />
11 <link rel="stylesheet" href="{{static_url("css/fbm.css") }}" type="text/css" />
11 <link rel="stylesheet" href="{{static_url("css/fbm.css") }}" type="text/css" />
12 <link rel="stylesheet" href="{{static_url("css/page.css") }}" type="text/css"/>
12 <link rel="stylesheet" href="{{static_url("css/page.css") }}" type="text/css"/>
13 {% block stylesheet %}
13 {% block stylesheet %}
14 {% end %}
14 {% end %}
15 <link rel="stylesheet" href="{{ static_url("css/custom.css") }}" type="text/css" />
16
15
17
16 {% block meta %}
18 {% block meta %}
17 {% end %}
19 {% end %}
18
20
19 </head>
21 </head>
20
22
21 <body {% block params %}{% end %}>
23 <body {% block params %}{% end %}>
22
24
23 <div id="header">
25 <div id="header">
24 <span id="ipython_notebook"><h1><a href={{base_project_url}} alt='dashboard'><img src='{{static_url("ipynblogo.png") }}' alt='IPython Notebook'/></a></h1></span>
26 <span id="ipython_notebook"><h1><a href={{base_project_url}} alt='dashboard'><img src='{{static_url("ipynblogo.png") }}' alt='IPython Notebook'/></a></h1></span>
25
27
26 {% block login_widget %}
28 {% block login_widget %}
27
29
28 <span id="login_widget">
30 <span id="login_widget">
29 {% if logged_in %}
31 {% if logged_in %}
30 <button id="logout">Logout</button>
32 <button id="logout">Logout</button>
31 {% elif login_available and not logged_in %}
33 {% elif login_available and not logged_in %}
32 <button id="login">Login</button>
34 <button id="login">Login</button>
33 {% end %}
35 {% end %}
34 </span>
36 </span>
35
37
36 {% end %}
38 {% end %}
37
39
38 {% block header %}
40 {% block header %}
39 {% end %}
41 {% end %}
40 </div>
42 </div>
41
43
42 <div id="site">
44 <div id="site">
43 {% block site %}
45 {% block site %}
44 {% end %}
46 {% end %}
45 </div>
47 </div>
46
48
47 <script src="{{static_url("jquery/js/jquery-1.7.1.min.js") }}" type="text/javascript" charset="utf-8"></script>
49 <script src="{{static_url("jquery/js/jquery-1.7.1.min.js") }}" type="text/javascript" charset="utf-8"></script>
48 <script src="{{static_url("jquery/js/jquery-ui.min.js") }}" type="text/javascript" charset="utf-8"></script>
50 <script src="{{static_url("jquery/js/jquery-ui.min.js") }}" type="text/javascript" charset="utf-8"></script>
49 <script src="{{static_url("js/namespace.js") }}" type="text/javascript" charset="utf-8"></script>
51 <script src="{{static_url("js/namespace.js") }}" type="text/javascript" charset="utf-8"></script>
50 <script src="{{static_url("js/page.js") }}" type="text/javascript" charset="utf-8"></script>
52 <script src="{{static_url("js/page.js") }}" type="text/javascript" charset="utf-8"></script>
51 <script src="{{static_url("js/loginwidget.js") }}" type="text/javascript" charset="utf-8"></script>
53 <script src="{{static_url("js/loginwidget.js") }}" type="text/javascript" charset="utf-8"></script>
52
54
53 {% block script %}
55 {% block script %}
54 {% end %}
56 {% end %}
55
57
58 <script src="{{static_url("js/custom.js") }}" type="text/javascript" charset="utf-8"></script>
59
56 </body>
60 </body>
57
61
58 </html>
62 </html>
General Comments 0
You need to be logged in to leave comments. Login now