##// END OF EJS Templates
Merge pull request #4715 from minrk/tornado-static-url...
Min RK -
r13985:0fc2a30e merge
parent child Browse files
Show More
@@ -1,372 +1,376 b''
1 """Base Tornado handlers for the notebook.
1 """Base Tornado handlers for the notebook.
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2011 The IPython Development Team
9 # Copyright (C) 2011 The IPython Development Team
10 #
10 #
11 # Distributed under the terms of the BSD License. The full license is in
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19
19
20 import functools
20 import functools
21 import json
21 import json
22 import logging
22 import logging
23 import os
23 import os
24 import stat
24 import stat
25 import sys
25 import sys
26 import traceback
26 import traceback
27
27
28 from tornado import web
28 from tornado import web
29
29
30 try:
30 try:
31 from tornado.log import app_log
31 from tornado.log import app_log
32 except ImportError:
32 except ImportError:
33 app_log = logging.getLogger()
33 app_log = logging.getLogger()
34
34
35 from IPython.config import Application
35 from IPython.config import Application
36 from IPython.utils.path import filefind
36 from IPython.utils.path import filefind
37 from IPython.utils.py3compat import string_types
37 from IPython.utils.py3compat import string_types
38
38
39 # UF_HIDDEN is a stat flag not defined in the stat module.
39 # UF_HIDDEN is a stat flag not defined in the stat module.
40 # It is used by BSD to indicate hidden files.
40 # It is used by BSD to indicate hidden files.
41 UF_HIDDEN = getattr(stat, 'UF_HIDDEN', 32768)
41 UF_HIDDEN = getattr(stat, 'UF_HIDDEN', 32768)
42
42
43 #-----------------------------------------------------------------------------
43 #-----------------------------------------------------------------------------
44 # Top-level handlers
44 # Top-level handlers
45 #-----------------------------------------------------------------------------
45 #-----------------------------------------------------------------------------
46
46
47 class RequestHandler(web.RequestHandler):
47 class RequestHandler(web.RequestHandler):
48 """RequestHandler with default variable setting."""
48 """RequestHandler with default variable setting."""
49
49
50 def render(*args, **kwargs):
50 def render(*args, **kwargs):
51 kwargs.setdefault('message', '')
51 kwargs.setdefault('message', '')
52 return web.RequestHandler.render(*args, **kwargs)
52 return web.RequestHandler.render(*args, **kwargs)
53
53
54 class AuthenticatedHandler(RequestHandler):
54 class AuthenticatedHandler(RequestHandler):
55 """A RequestHandler with an authenticated user."""
55 """A RequestHandler with an authenticated user."""
56
56
57 def clear_login_cookie(self):
57 def clear_login_cookie(self):
58 self.clear_cookie(self.cookie_name)
58 self.clear_cookie(self.cookie_name)
59
59
60 def get_current_user(self):
60 def get_current_user(self):
61 user_id = self.get_secure_cookie(self.cookie_name)
61 user_id = self.get_secure_cookie(self.cookie_name)
62 # For now the user_id should not return empty, but it could eventually
62 # For now the user_id should not return empty, but it could eventually
63 if user_id == '':
63 if user_id == '':
64 user_id = 'anonymous'
64 user_id = 'anonymous'
65 if user_id is None:
65 if user_id is None:
66 # prevent extra Invalid cookie sig warnings:
66 # prevent extra Invalid cookie sig warnings:
67 self.clear_login_cookie()
67 self.clear_login_cookie()
68 if not self.login_available:
68 if not self.login_available:
69 user_id = 'anonymous'
69 user_id = 'anonymous'
70 return user_id
70 return user_id
71
71
72 @property
72 @property
73 def cookie_name(self):
73 def cookie_name(self):
74 default_cookie_name = 'username-{host}'.format(
74 default_cookie_name = 'username-{host}'.format(
75 host=self.request.host,
75 host=self.request.host,
76 ).replace(':', '-')
76 ).replace(':', '-')
77 return self.settings.get('cookie_name', default_cookie_name)
77 return self.settings.get('cookie_name', default_cookie_name)
78
78
79 @property
79 @property
80 def password(self):
80 def password(self):
81 """our password"""
81 """our password"""
82 return self.settings.get('password', '')
82 return self.settings.get('password', '')
83
83
84 @property
84 @property
85 def logged_in(self):
85 def logged_in(self):
86 """Is a user currently logged in?
86 """Is a user currently logged in?
87
87
88 """
88 """
89 user = self.get_current_user()
89 user = self.get_current_user()
90 return (user and not user == 'anonymous')
90 return (user and not user == 'anonymous')
91
91
92 @property
92 @property
93 def login_available(self):
93 def login_available(self):
94 """May a user proceed to log in?
94 """May a user proceed to log in?
95
95
96 This returns True if login capability is available, irrespective of
96 This returns True if login capability is available, irrespective of
97 whether the user is already logged in or not.
97 whether the user is already logged in or not.
98
98
99 """
99 """
100 return bool(self.settings.get('password', ''))
100 return bool(self.settings.get('password', ''))
101
101
102
102
103 class IPythonHandler(AuthenticatedHandler):
103 class IPythonHandler(AuthenticatedHandler):
104 """IPython-specific extensions to authenticated handling
104 """IPython-specific extensions to authenticated handling
105
105
106 Mostly property shortcuts to IPython-specific settings.
106 Mostly property shortcuts to IPython-specific settings.
107 """
107 """
108
108
109 @property
109 @property
110 def config(self):
110 def config(self):
111 return self.settings.get('config', None)
111 return self.settings.get('config', None)
112
112
113 @property
113 @property
114 def log(self):
114 def log(self):
115 """use the IPython log by default, falling back on tornado's logger"""
115 """use the IPython log by default, falling back on tornado's logger"""
116 if Application.initialized():
116 if Application.initialized():
117 return Application.instance().log
117 return Application.instance().log
118 else:
118 else:
119 return app_log
119 return app_log
120
120
121 @property
121 @property
122 def use_less(self):
122 def use_less(self):
123 """Use less instead of css in templates"""
123 """Use less instead of css in templates"""
124 return self.settings.get('use_less', False)
124 return self.settings.get('use_less', False)
125
125
126 #---------------------------------------------------------------
126 #---------------------------------------------------------------
127 # URLs
127 # URLs
128 #---------------------------------------------------------------
128 #---------------------------------------------------------------
129
129
130 @property
130 @property
131 def ws_url(self):
131 def ws_url(self):
132 """websocket url matching the current request
132 """websocket url matching the current request
133
133
134 By default, this is just `''`, indicating that it should match
134 By default, this is just `''`, indicating that it should match
135 the same host, protocol, port, etc.
135 the same host, protocol, port, etc.
136 """
136 """
137 return self.settings.get('websocket_url', '')
137 return self.settings.get('websocket_url', '')
138
138
139 @property
139 @property
140 def mathjax_url(self):
140 def mathjax_url(self):
141 return self.settings.get('mathjax_url', '')
141 return self.settings.get('mathjax_url', '')
142
142
143 @property
143 @property
144 def base_project_url(self):
144 def base_project_url(self):
145 return self.settings.get('base_project_url', '/')
145 return self.settings.get('base_project_url', '/')
146
146
147 @property
147 @property
148 def base_kernel_url(self):
148 def base_kernel_url(self):
149 return self.settings.get('base_kernel_url', '/')
149 return self.settings.get('base_kernel_url', '/')
150
150
151 #---------------------------------------------------------------
151 #---------------------------------------------------------------
152 # Manager objects
152 # Manager objects
153 #---------------------------------------------------------------
153 #---------------------------------------------------------------
154
154
155 @property
155 @property
156 def kernel_manager(self):
156 def kernel_manager(self):
157 return self.settings['kernel_manager']
157 return self.settings['kernel_manager']
158
158
159 @property
159 @property
160 def notebook_manager(self):
160 def notebook_manager(self):
161 return self.settings['notebook_manager']
161 return self.settings['notebook_manager']
162
162
163 @property
163 @property
164 def cluster_manager(self):
164 def cluster_manager(self):
165 return self.settings['cluster_manager']
165 return self.settings['cluster_manager']
166
166
167 @property
167 @property
168 def session_manager(self):
168 def session_manager(self):
169 return self.settings['session_manager']
169 return self.settings['session_manager']
170
170
171 @property
171 @property
172 def project_dir(self):
172 def project_dir(self):
173 return self.notebook_manager.notebook_dir
173 return self.notebook_manager.notebook_dir
174
174
175 #---------------------------------------------------------------
175 #---------------------------------------------------------------
176 # template rendering
176 # template rendering
177 #---------------------------------------------------------------
177 #---------------------------------------------------------------
178
178
179 def get_template(self, name):
179 def get_template(self, name):
180 """Return the jinja template object for a given name"""
180 """Return the jinja template object for a given name"""
181 return self.settings['jinja2_env'].get_template(name)
181 return self.settings['jinja2_env'].get_template(name)
182
182
183 def render_template(self, name, **ns):
183 def render_template(self, name, **ns):
184 ns.update(self.template_namespace)
184 ns.update(self.template_namespace)
185 template = self.get_template(name)
185 template = self.get_template(name)
186 return template.render(**ns)
186 return template.render(**ns)
187
187
188 @property
188 @property
189 def template_namespace(self):
189 def template_namespace(self):
190 return dict(
190 return dict(
191 base_project_url=self.base_project_url,
191 base_project_url=self.base_project_url,
192 base_kernel_url=self.base_kernel_url,
192 base_kernel_url=self.base_kernel_url,
193 logged_in=self.logged_in,
193 logged_in=self.logged_in,
194 login_available=self.login_available,
194 login_available=self.login_available,
195 use_less=self.use_less,
195 use_less=self.use_less,
196 static_url=self.static_url,
196 )
197 )
197
198
198 def get_json_body(self):
199 def get_json_body(self):
199 """Return the body of the request as JSON data."""
200 """Return the body of the request as JSON data."""
200 if not self.request.body:
201 if not self.request.body:
201 return None
202 return None
202 # Do we need to call body.decode('utf-8') here?
203 # Do we need to call body.decode('utf-8') here?
203 body = self.request.body.strip().decode(u'utf-8')
204 body = self.request.body.strip().decode(u'utf-8')
204 try:
205 try:
205 model = json.loads(body)
206 model = json.loads(body)
206 except Exception:
207 except Exception:
207 self.log.debug("Bad JSON: %r", body)
208 self.log.debug("Bad JSON: %r", body)
208 self.log.error("Couldn't parse JSON", exc_info=True)
209 self.log.error("Couldn't parse JSON", exc_info=True)
209 raise web.HTTPError(400, u'Invalid JSON in body of request')
210 raise web.HTTPError(400, u'Invalid JSON in body of request')
210 return model
211 return model
211
212
212
213
213 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
214 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
214 """static files should only be accessible when logged in"""
215 """static files should only be accessible when logged in"""
215
216
216 @web.authenticated
217 @web.authenticated
217 def get(self, path):
218 def get(self, path):
218 if os.path.splitext(path)[1] == '.ipynb':
219 if os.path.splitext(path)[1] == '.ipynb':
219 name = os.path.basename(path)
220 name = os.path.basename(path)
220 self.set_header('Content-Type', 'application/json')
221 self.set_header('Content-Type', 'application/json')
221 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
222 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
222
223
223 return web.StaticFileHandler.get(self, path)
224 return web.StaticFileHandler.get(self, path)
224
225
225 def compute_etag(self):
226 def compute_etag(self):
226 return None
227 return None
227
228
228 def validate_absolute_path(self, root, absolute_path):
229 def validate_absolute_path(self, root, absolute_path):
229 """Validate and return the absolute path.
230 """Validate and return the absolute path.
230
231
231 Requires tornado 3.1
232 Requires tornado 3.1
232
233
233 Adding to tornado's own handling, forbids the serving of hidden files.
234 Adding to tornado's own handling, forbids the serving of hidden files.
234 """
235 """
235 abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
236 abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
236 abs_root = os.path.abspath(root)
237 abs_root = os.path.abspath(root)
237 self.forbid_hidden(abs_root, abs_path)
238 self.forbid_hidden(abs_root, abs_path)
238 return abs_path
239 return abs_path
239
240
240 def forbid_hidden(self, absolute_root, absolute_path):
241 def forbid_hidden(self, absolute_root, absolute_path):
241 """Raise 403 if a file is hidden or contained in a hidden directory.
242 """Raise 403 if a file is hidden or contained in a hidden directory.
242
243
243 Hidden is determined by either name starting with '.'
244 Hidden is determined by either name starting with '.'
244 or the UF_HIDDEN flag as reported by stat
245 or the UF_HIDDEN flag as reported by stat
245 """
246 """
246 inside_root = absolute_path[len(absolute_root):]
247 inside_root = absolute_path[len(absolute_root):]
247 if any(part.startswith('.') for part in inside_root.split(os.sep)):
248 if any(part.startswith('.') for part in inside_root.split(os.sep)):
248 raise web.HTTPError(403)
249 raise web.HTTPError(403)
249
250
250 # check UF_HIDDEN on any location up to root
251 # check UF_HIDDEN on any location up to root
251 path = absolute_path
252 path = absolute_path
252 while path and path.startswith(absolute_root) and path != absolute_root:
253 while path and path.startswith(absolute_root) and path != absolute_root:
253 st = os.stat(path)
254 st = os.stat(path)
254 if getattr(st, 'st_flags', 0) & UF_HIDDEN:
255 if getattr(st, 'st_flags', 0) & UF_HIDDEN:
255 raise web.HTTPError(403)
256 raise web.HTTPError(403)
256 path = os.path.dirname(path)
257 path = os.path.dirname(path)
257
258
258 return absolute_path
259 return absolute_path
259
260
260
261
261 def json_errors(method):
262 def json_errors(method):
262 """Decorate methods with this to return GitHub style JSON errors.
263 """Decorate methods with this to return GitHub style JSON errors.
263
264
264 This should be used on any JSON API on any handler method that can raise HTTPErrors.
265 This should be used on any JSON API on any handler method that can raise HTTPErrors.
265
266
266 This will grab the latest HTTPError exception using sys.exc_info
267 This will grab the latest HTTPError exception using sys.exc_info
267 and then:
268 and then:
268
269
269 1. Set the HTTP status code based on the HTTPError
270 1. Set the HTTP status code based on the HTTPError
270 2. Create and return a JSON body with a message field describing
271 2. Create and return a JSON body with a message field describing
271 the error in a human readable form.
272 the error in a human readable form.
272 """
273 """
273 @functools.wraps(method)
274 @functools.wraps(method)
274 def wrapper(self, *args, **kwargs):
275 def wrapper(self, *args, **kwargs):
275 try:
276 try:
276 result = method(self, *args, **kwargs)
277 result = method(self, *args, **kwargs)
277 except web.HTTPError as e:
278 except web.HTTPError as e:
278 status = e.status_code
279 status = e.status_code
279 message = e.log_message
280 message = e.log_message
280 self.set_status(e.status_code)
281 self.set_status(e.status_code)
281 self.finish(json.dumps(dict(message=message)))
282 self.finish(json.dumps(dict(message=message)))
282 except Exception:
283 except Exception:
283 self.log.error("Unhandled error in API request", exc_info=True)
284 self.log.error("Unhandled error in API request", exc_info=True)
284 status = 500
285 status = 500
285 message = "Unknown server error"
286 message = "Unknown server error"
286 t, value, tb = sys.exc_info()
287 t, value, tb = sys.exc_info()
287 self.set_status(status)
288 self.set_status(status)
288 tb_text = ''.join(traceback.format_exception(t, value, tb))
289 tb_text = ''.join(traceback.format_exception(t, value, tb))
289 reply = dict(message=message, traceback=tb_text)
290 reply = dict(message=message, traceback=tb_text)
290 self.finish(json.dumps(reply))
291 self.finish(json.dumps(reply))
291 else:
292 else:
292 return result
293 return result
293 return wrapper
294 return wrapper
294
295
295
296
296
297
297 #-----------------------------------------------------------------------------
298 #-----------------------------------------------------------------------------
298 # File handler
299 # File handler
299 #-----------------------------------------------------------------------------
300 #-----------------------------------------------------------------------------
300
301
301 # to minimize subclass changes:
302 # to minimize subclass changes:
302 HTTPError = web.HTTPError
303 HTTPError = web.HTTPError
303
304
304 class FileFindHandler(web.StaticFileHandler):
305 class FileFindHandler(web.StaticFileHandler):
305 """subclass of StaticFileHandler for serving files from a search path"""
306 """subclass of StaticFileHandler for serving files from a search path"""
306
307
307 # cache search results, don't search for files more than once
308 # cache search results, don't search for files more than once
308 _static_paths = {}
309 _static_paths = {}
309
310
310 def initialize(self, path, default_filename=None):
311 def initialize(self, path, default_filename=None):
311 if isinstance(path, string_types):
312 if isinstance(path, string_types):
312 path = [path]
313 path = [path]
313
314
314 self.root = tuple(
315 self.root = tuple(
315 os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
316 os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
316 )
317 )
317 self.default_filename = default_filename
318 self.default_filename = default_filename
318
319
319 def compute_etag(self):
320 def compute_etag(self):
320 return None
321 return None
321
322
322 @classmethod
323 @classmethod
323 def get_absolute_path(cls, roots, path):
324 def get_absolute_path(cls, roots, path):
324 """locate a file to serve on our static file search path"""
325 """locate a file to serve on our static file search path"""
325 with cls._lock:
326 with cls._lock:
326 if path in cls._static_paths:
327 if path in cls._static_paths:
327 return cls._static_paths[path]
328 return cls._static_paths[path]
328 try:
329 try:
329 abspath = os.path.abspath(filefind(path, roots))
330 abspath = os.path.abspath(filefind(path, roots))
330 except IOError:
331 except IOError:
331 # IOError means not found
332 # IOError means not found
332 raise web.HTTPError(404)
333 return ''
333
334
334 cls._static_paths[path] = abspath
335 cls._static_paths[path] = abspath
335 return abspath
336 return abspath
336
337
337 def validate_absolute_path(self, root, absolute_path):
338 def validate_absolute_path(self, root, absolute_path):
338 """check if the file should be served (raises 404, 403, etc.)"""
339 """check if the file should be served (raises 404, 403, etc.)"""
340 if absolute_path == '':
341 raise web.HTTPError(404)
342
339 for root in self.root:
343 for root in self.root:
340 if (absolute_path + os.sep).startswith(root):
344 if (absolute_path + os.sep).startswith(root):
341 break
345 break
342
346
343 return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
347 return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
344
348
345
349
346 class TrailingSlashHandler(web.RequestHandler):
350 class TrailingSlashHandler(web.RequestHandler):
347 """Simple redirect handler that strips trailing slashes
351 """Simple redirect handler that strips trailing slashes
348
352
349 This should be the first, highest priority handler.
353 This should be the first, highest priority handler.
350 """
354 """
351
355
352 SUPPORTED_METHODS = ['GET']
356 SUPPORTED_METHODS = ['GET']
353
357
354 def get(self):
358 def get(self):
355 self.redirect(self.request.uri.rstrip('/'))
359 self.redirect(self.request.uri.rstrip('/'))
356
360
357 #-----------------------------------------------------------------------------
361 #-----------------------------------------------------------------------------
358 # URL pattern fragments for re-use
362 # URL pattern fragments for re-use
359 #-----------------------------------------------------------------------------
363 #-----------------------------------------------------------------------------
360
364
361 path_regex = r"(?P<path>(?:/.*)*)"
365 path_regex = r"(?P<path>(?:/.*)*)"
362 notebook_name_regex = r"(?P<name>[^/]+\.ipynb)"
366 notebook_name_regex = r"(?P<name>[^/]+\.ipynb)"
363 notebook_path_regex = "%s/%s" % (path_regex, notebook_name_regex)
367 notebook_path_regex = "%s/%s" % (path_regex, notebook_name_regex)
364
368
365 #-----------------------------------------------------------------------------
369 #-----------------------------------------------------------------------------
366 # URL to handler mappings
370 # URL to handler mappings
367 #-----------------------------------------------------------------------------
371 #-----------------------------------------------------------------------------
368
372
369
373
370 default_handlers = [
374 default_handlers = [
371 (r".*/", TrailingSlashHandler)
375 (r".*/", TrailingSlashHandler)
372 ]
376 ]
@@ -1,97 +1,94 b''
1
1
2
2
3 <!DOCTYPE HTML>
3 <!DOCTYPE HTML>
4 {% macro static_url(name) -%}
5 {{ base_project_url }}static/{{ name }}
6 {%- endmacro %}
7 <html>
4 <html>
8
5
9 <head>
6 <head>
10 <meta charset="utf-8">
7 <meta charset="utf-8">
11
8
12 <title>{% block title %}IPython Notebook{% endblock %}</title>
9 <title>{% block title %}IPython Notebook{% endblock %}</title>
13 <link rel="shortcut icon" type="image/x-icon" href="{{static_url("base/images/favicon.ico") }}">
10 <link rel="shortcut icon" type="image/x-icon" href="{{static_url("base/images/favicon.ico") }}">
14 <meta http-equiv="X-UA-Compatible" content="chrome=1">
11 <meta http-equiv="X-UA-Compatible" content="chrome=1">
15 <link rel="stylesheet" href="{{static_url("components/jquery-ui/themes/smoothness/jquery-ui.min.css") }}" type="text/css" />
12 <link rel="stylesheet" href="{{static_url("components/jquery-ui/themes/smoothness/jquery-ui.min.css") }}" type="text/css" />
16 <meta name="viewport" content="width=device-width, initial-scale=1.0">
13 <meta name="viewport" content="width=device-width, initial-scale=1.0">
17
14
18 {% block stylesheet %}
15 {% block stylesheet %}
19 {% block lesscss %}
16 {% block lesscss %}
20 {% if use_less %}
17 {% if use_less %}
21 <link rel="stylesheet/less" href="{{ static_url("style/style.less") }}" type="text/css" />
18 <link rel="stylesheet/less" href="{{ static_url("style/style.less") }}" type="text/css" />
22 {% else %}
19 {% else %}
23 <link rel="stylesheet" href="{{ static_url("style/style.min.css") }}" type="text/css"/>
20 <link rel="stylesheet" href="{{ static_url("style/style.min.css") }}" type="text/css"/>
24 {% endif %}
21 {% endif %}
25 {% endblock %}
22 {% endblock %}
26 {% endblock %}
23 {% endblock %}
27 <link rel="stylesheet" href="{{ static_url("custom/custom.css") }}" type="text/css" />
24 <link rel="stylesheet" href="{{ static_url("custom/custom.css") }}" type="text/css" />
28 <script src="{{static_url("components/requirejs/require.js") }}" type="text/javascript" charset="utf-8"></script>
25 <script src="{{static_url("components/requirejs/require.js") }}" type="text/javascript" charset="utf-8"></script>
29 <script>
26 <script>
30 require.config({
27 require.config({
31 baseUrl: '{{static_url()}}',
28 baseUrl: '{{static_url("")}}',
32 paths: {
29 paths: {
33 nbextensions : '{{ base_project_url }}nbextensions'
30 nbextensions : '{{ base_project_url }}nbextensions'
34 }
31 }
35 });
32 });
36 </script>
33 </script>
37
34
38 {% block meta %}
35 {% block meta %}
39 {% endblock %}
36 {% endblock %}
40
37
41 </head>
38 </head>
42
39
43 <body {% block params %}{% endblock %}>
40 <body {% block params %}{% endblock %}>
44
41
45 <noscript>
42 <noscript>
46 <div id='noscript'>
43 <div id='noscript'>
47 IPython Notebook requires JavaScript.<br>
44 IPython Notebook requires JavaScript.<br>
48 Please enable it to proceed.
45 Please enable it to proceed.
49 </div>
46 </div>
50 </noscript>
47 </noscript>
51
48
52 <div id="header" class="navbar navbar-static-top">
49 <div id="header" class="navbar navbar-static-top">
53 <div class="navbar-inner navbar-nobg">
50 <div class="navbar-inner navbar-nobg">
54 <div class="container">
51 <div class="container">
55 <div id="ipython_notebook" class="nav brand pull-left"><a href="{{base_project_url}}tree/{{notebook_path}}" alt='dashboard'><img src='{{static_url("base/images/ipynblogo.png") }}' alt='IPython Notebook'/></a></div>
52 <div id="ipython_notebook" class="nav brand pull-left"><a href="{{base_project_url}}tree/{{notebook_path}}" alt='dashboard'><img src='{{static_url("base/images/ipynblogo.png") }}' alt='IPython Notebook'/></a></div>
56
53
57 {% block login_widget %}
54 {% block login_widget %}
58
55
59 <span id="login_widget">
56 <span id="login_widget">
60 {% if logged_in %}
57 {% if logged_in %}
61 <button id="logout">Logout</button>
58 <button id="logout">Logout</button>
62 {% elif login_available and not logged_in %}
59 {% elif login_available and not logged_in %}
63 <button id="login">Login</button>
60 <button id="login">Login</button>
64 {% endif %}
61 {% endif %}
65 </span>
62 </span>
66
63
67 {% endblock %}
64 {% endblock %}
68
65
69 {% block header %}
66 {% block header %}
70 {% endblock %}
67 {% endblock %}
71 </div>
68 </div>
72 </div>
69 </div>
73 </div>
70 </div>
74
71
75 <div id="site">
72 <div id="site">
76 {% block site %}
73 {% block site %}
77 {% endblock %}
74 {% endblock %}
78 </div>
75 </div>
79
76
80 <script src="{{static_url("components/jquery/jquery.min.js") }}" type="text/javascript" charset="utf-8"></script>
77 <script src="{{static_url("components/jquery/jquery.min.js") }}" type="text/javascript" charset="utf-8"></script>
81 <script src="{{static_url("components/jquery-ui/ui/minified/jquery-ui.min.js") }}" type="text/javascript" charset="utf-8"></script>
78 <script src="{{static_url("components/jquery-ui/ui/minified/jquery-ui.min.js") }}" type="text/javascript" charset="utf-8"></script>
82 <script src="{{static_url("components/bootstrap/bootstrap/js/bootstrap.min.js") }}" type="text/javascript" charset="utf-8"></script>
79 <script src="{{static_url("components/bootstrap/bootstrap/js/bootstrap.min.js") }}" type="text/javascript" charset="utf-8"></script>
83 <script src="{{static_url("base/js/namespace.js") }}" type="text/javascript" charset="utf-8"></script>
80 <script src="{{static_url("base/js/namespace.js") }}" type="text/javascript" charset="utf-8"></script>
84 <script src="{{static_url("base/js/page.js") }}" type="text/javascript" charset="utf-8"></script>
81 <script src="{{static_url("base/js/page.js") }}" type="text/javascript" charset="utf-8"></script>
85 <script src="{{static_url("auth/js/loginwidget.js") }}" type="text/javascript" charset="utf-8"></script>
82 <script src="{{static_url("auth/js/loginwidget.js") }}" type="text/javascript" charset="utf-8"></script>
86
83
87 {% block script %}
84 {% block script %}
88 {% if use_less %}
85 {% if use_less %}
89 <script src="{{ static_url("components/less.js/dist/less-1.3.3.min.js") }}" charset="utf-8"></script>
86 <script src="{{ static_url("components/less.js/dist/less-1.3.3.min.js") }}" charset="utf-8"></script>
90 {% endif %}
87 {% endif %}
91 {% endblock %}
88 {% endblock %}
92
89
93 <script src="{{static_url("custom/custom.js") }}" type="text/javascript" charset="utf-8"></script>
90 <script src="{{static_url("custom/custom.js") }}" type="text/javascript" charset="utf-8"></script>
94
91
95 </body>
92 </body>
96
93
97 </html>
94 </html>
General Comments 0
You need to be logged in to leave comments. Login now