add some basic HTML pages

and LESS

closes #6
This commit is contained in:
MinRK
2014-09-09 15:20:21 -07:00
parent c40dd367e7
commit 7f93ea2325
16 changed files with 384 additions and 40 deletions

2
.gitignore vendored
View File

@@ -5,5 +5,7 @@ node_modules
build build
dist dist
share/jupyter/static/components share/jupyter/static/components
share/jupyter/static/css/style.min.css
share/jupyter/static/css/style.min.css.map
*.egg-info *.egg-info
MANIFEST MANIFEST

View File

@@ -24,6 +24,10 @@ Basic principles:
# install the Python pargs (-e for editable/development install) # install the Python pargs (-e for editable/development install)
pip install [-e] . pip install [-e] .
You will also need `bower` to fetch frontend-javascript and `less` to compile CSS:
npm install -g bower less
Note on debian/ubuntu machines, you may need to install the `nodejs-legacy` package Note on debian/ubuntu machines, you may need to install the `nodejs-legacy` package
to get node executables to work: to get node executables to work:

View File

@@ -9,10 +9,12 @@ import logging
import os import os
from subprocess import Popen from subprocess import Popen
from jinja2 import Environment, FileSystemLoader
import tornado.httpserver import tornado.httpserver
import tornado.options import tornado.options
from tornado.ioloop import IOLoop from tornado.ioloop import IOLoop
from tornado.log import LogFormatter from tornado.log import LogFormatter, app_log
from tornado import gen, web from tornado import gen, web
from IPython.utils.traitlets import ( from IPython.utils.traitlets import (
@@ -25,6 +27,7 @@ from IPython.utils.importstring import import_item
here = os.path.dirname(__file__) here = os.path.dirname(__file__)
from .handlers import ( from .handlers import (
Template404,
RootHandler, RootHandler,
LoginHandler, LoginHandler,
LogoutHandler, LogoutHandler,
@@ -36,6 +39,13 @@ from . import orm
from ._data import DATA_FILES_PATH from ._data import DATA_FILES_PATH
from .utils import url_path_join from .utils import url_path_join
class RedirectHandler(web.RedirectHandler):
def get(self, *a, **kw):
self.set_header("mu-redirect", True)
app_log.warn("mu redirect: %s -> %s", self.request.path, self._url)
return super(RedirectHandler, self).get(*a, **kw)
class JupyterHubApp(Application): class JupyterHubApp(Application):
"""An Application for starting a Multi-User Notebook server.""" """An Application for starting a Multi-User Notebook server."""
data_files_path = Unicode(DATA_FILES_PATH, config=True, data_files_path = Unicode(DATA_FILES_PATH, config=True,
@@ -64,6 +74,10 @@ class JupyterHubApp(Application):
help="The base URL of the entire application" help="The base URL of the entire application"
) )
jinja_environment_options = Dict(config=True,
help="Supply extra arguments that will be passed to Jinja environment."
)
proxy_cmd = Unicode('configurable-http-proxy', config=True, proxy_cmd = Unicode('configurable-http-proxy', config=True,
help="""The command to start the http proxy. help="""The command to start the http proxy.
@@ -157,7 +171,7 @@ class JupyterHubApp(Application):
def _log_format_default(self): def _log_format_default(self):
"""override default log format to include time""" """override default log format to include time"""
return u"%(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s]%(end_color)s %(message)s" return u"H %(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s]%(end_color)s %(message)s"
def init_logging(self): def init_logging(self):
# This prevents double log messages because tornado use a root logger that # This prevents double log messages because tornado use a root logger that
@@ -191,8 +205,11 @@ class JupyterHubApp(Application):
self.handlers = self.add_url_prefix(self.hub_prefix, handlers) self.handlers = self.add_url_prefix(self.hub_prefix, handlers)
self.handlers.extend([ self.handlers.extend([
(r"/user/([^/]+)/?.*", UserHandler), (r"/user/([^/]+)/?.*", UserHandler),
(r"/", web.RedirectHandler, {"url" : self.hub_prefix}), (r"/?", RedirectHandler, {"url" : self.hub_prefix}),
]) ])
self.handlers.append(
(r'(.*)', Template404)
)
def init_db(self): def init_db(self):
# TODO: load state from db for resume # TODO: load state from db for resume
@@ -240,20 +257,28 @@ class JupyterHubApp(Application):
'--api-ip', self.proxy.api_server.ip, '--api-ip', self.proxy.api_server.ip,
'--api-port', str(self.proxy.api_server.port), '--api-port', str(self.proxy.api_server.port),
'--default-target', self.hub.server.host, '--default-target', self.hub.server.host,
'--log-level=debug',
] ]
if self.ssl_key: if self.ssl_key:
cmd.extend(['--ssl-key', self.ssl_key]) cmd.extend(['--ssl-key', self.ssl_key])
if self.ssl_cert: if self.ssl_cert:
cmd.extend(['--ssl-cert', self.ssl_cert]) cmd.extend(['--ssl-cert', self.ssl_cert])
self.proxy = Popen(cmd, env=env) self.proxy = Popen(cmd, env=env)
def init_tornado_settings(self): def init_tornado_settings(self):
"""Set up the tornado settings dict.""" """Set up the tornado settings dict."""
base_url = self.base_url base_url = self.base_url
template_path = os.path.join(self.data_files_path, 'templates'),
jinja_env = Environment(
loader=FileSystemLoader(template_path),
**self.jinja_environment_options
)
settings = dict( settings = dict(
config=self.config, config=self.config,
# log=self.log, log=self.log,
db=self.db, db=self.db,
hub=self.hub, hub=self.hub,
authenticator=import_item(self.authenticator)(config=self.config), authenticator=import_item(self.authenticator)(config=self.config),
@@ -261,8 +286,10 @@ class JupyterHubApp(Application):
base_url=base_url, base_url=base_url,
cookie_secret=self.cookie_secret, cookie_secret=self.cookie_secret,
login_url=url_path_join(self.hub.server.base_url, 'login'), login_url=url_path_join(self.hub.server.base_url, 'login'),
template_path=os.path.join(self.data_files_path, 'templates'), static_path=os.path.join(self.data_files_path, 'static'),
static_files_path=os.path.join(self.data_files_path, 'static'), static_url_prefix=url_path_join(self.hub.server.base_url, 'static/'),
template_path=template_path,
jinja2_env=jinja_env,
) )
# allow configured settings to have priority # allow configured settings to have priority
settings.update(self.tornado_settings) settings.update(self.tornado_settings)

View File

@@ -5,9 +5,16 @@
import json import json
import re import re
try:
# py3
from http.client import responses
except ImportError:
from httplib import responses
import requests import requests
from jinja2 import TemplateNotFound
from tornado.log import app_log from tornado.log import app_log
from tornado.escape import url_escape from tornado.escape import url_escape
from tornado.httputil import url_concat from tornado.httputil import url_concat
@@ -54,26 +61,97 @@ class BaseHandler(RequestHandler):
return cookie_token.user.name return cookie_token.user.name
else: else:
# have cookie, but it's not valid. Clear it and start over. # have cookie, but it's not valid. Clear it and start over.
self.clear_cookie(self.hub.server.cookie_name) self.clear_cookie(self.hub.server.cookie_name, path=self.hub.server.base_url)
def clear_login_cookie(self): def clear_login_cookie(self):
username = self.get_current_user() username = self.get_current_user()
if username is not None: if username is not None:
user = self.db.query(orm.User).filter(name=username).first() user = self.db.query(orm.User).filter(name=username).first()
if user is not None: if user is not None and user.server is not None:
self.clear_cookie(user.server.cookie_name, path=user.server.base_url) self.clear_cookie(user.server.cookie_name, path=user.server.base_url)
self.clear_cookie(self.cookie_name, path=self.hub.base_url) self.clear_cookie(self.hub.server.cookie_name, path=self.hub.server.base_url)
@property @property
def spawner_class(self): def spawner_class(self):
return self.settings.get('spawner_class', LocalProcessSpawner) return self.settings.get('spawner_class', LocalProcessSpawner)
#---------------------------------------------------------------
# template rendering
#---------------------------------------------------------------
def get_template(self, name):
"""Return the jinja template object for a given name"""
return self.settings['jinja2_env'].get_template(name)
def render_template(self, name, **ns):
ns.update(self.template_namespace)
template = self.get_template(name)
return template.render(**ns)
@property
def logged_in(self):
"""Is a user currently logged in?"""
user = self.get_current_user()
return (user and not user == 'anonymous')
@property
def template_namespace(self):
return dict(
base_url=self.base_url,
logged_in=self.logged_in,
login_url=self.settings['login_url'],
static_url=self.static_url,
)
def write_error(self, status_code, **kwargs):
"""render custom error pages"""
exc_info = kwargs.get('exc_info')
message = ''
status_message = responses.get(status_code, 'Unknown HTTP Error')
if exc_info:
exception = exc_info[1]
# get the custom message, if defined
try:
message = exception.log_message % exception.args
except Exception:
pass
# construct the custom reason, if defined
reason = getattr(exception, 'reason', '')
if reason:
status_message = reason
# build template namespace
ns = dict(
status_code=status_code,
status_message=status_message,
message=message,
exception=exception,
)
self.set_header('Content-Type', 'text/html')
# render the template
try:
html = self.render_template('%s.html' % status_code, **ns)
except TemplateNotFound:
self.log.debug("No template for %d", status_code)
html = self.render_template('error.html', **ns)
self.write(html)
class Template404(BaseHandler):
"""Render our 404 template"""
def prepare(self):
raise web.HTTPError(404)
class RootHandler(BaseHandler): class RootHandler(BaseHandler):
"""Redirect from / to /user/foo/ after logging in.""" """Redirect from / to /user/foo/ after logging in."""
@web.authenticated @web.authenticated
def get(self): def get(self):
self.redirect("/user/%s/" % self.get_current_user()) uri = "/user/%s/" % self.get_current_user()
self.redirect(uri, permanent=False)
class UserHandler(BaseHandler): class UserHandler(BaseHandler):
@@ -98,24 +176,25 @@ class LogoutHandler(BaseHandler):
"""Log a user out by clearing their login cookie.""" """Log a user out by clearing their login cookie."""
def get(self): def get(self):
self.clear_login_cookie() self.clear_login_cookie()
self.write("logged out") html = self.render_template('logout.html')
self.finish(html)
class LoginHandler(BaseHandler): class LoginHandler(BaseHandler):
"""Render the login page.""" """Render the login page."""
def _render(self, message=None, username=None): def _render(self, message=None, username=None):
self.render('login.html', return self.render_template('login.html',
next=url_escape(self.get_argument('next', default='')), next=url_escape(self.get_argument('next', default='')),
username=username, username=username,
message=message, message=message,
) )
def get(self): def get(self):
if False and self.get_current_user(): if self.get_argument('next', False) and self.get_current_user():
self.redirect(self.get_argument('next', default='/')) self.redirect(self.get_argument('next'))
else: else:
username = self.get_argument('username', default='') username = self.get_argument('username', default='')
self._render(username=username) self.finish(self._render(username=username))
@gen.coroutine @gen.coroutine
def notify_proxy(self, user): def notify_proxy(self, user):
@@ -211,13 +290,15 @@ class LoginHandler(BaseHandler):
user = yield self.spawn_single_user(username) user = yield self.spawn_single_user(username)
self.set_login_cookies(user) self.set_login_cookies(user)
next_url = self.get_argument('next', default='') or '/user/%s/' % username next_url = self.get_argument('next', default='') or '/user/%s/' % username
print('next', next_url)
self.redirect(next_url) self.redirect(next_url)
else: else:
self.log.debug("Failed login for %s", username) self.log.debug("Failed login for %s", username)
self._render( html = self._render(
message={'error': 'Invalid username or password'}, message={'error': 'Invalid username or password'},
username=username, username=username,
) )
self.finish(html)
#------------------------------------------------------------------------------ #------------------------------------------------------------------------------

View File

@@ -42,9 +42,12 @@ except NameError:
locs = locs or globs locs = locs or globs
exec(compile(open(fname).read(), fname, "exec"), globs, locs) exec(compile(open(fname).read(), fname, "exec"), globs, locs)
here = os.path.abspath(os.path.dirname(__file__))
pjoin = os.path.join pjoin = os.path.join
here = os.path.abspath(os.path.dirname(__file__))
share_jupyter = pjoin(here, 'share', 'jupyter')
static = pjoin(share_jupyter, 'static')
#--------------------------------------------------------------------------- #---------------------------------------------------------------------------
# Build basic package data, etc. # Build basic package data, etc.
#--------------------------------------------------------------------------- #---------------------------------------------------------------------------
@@ -53,7 +56,6 @@ def get_data_files():
"""Get data files in share/jupyter""" """Get data files in share/jupyter"""
data_files = [] data_files = []
share_jupyter = pjoin(here, 'share', 'jupyter')
ntrim = len(here) + 1 ntrim = len(here) + 1
for (d, dirs, filenames) in os.walk(share_jupyter): for (d, dirs, filenames) in os.walk(share_jupyter):
@@ -117,19 +119,41 @@ class Bower(Command):
def run(self): def run(self):
check_call(['bower', 'install', '--allow-root']) check_call(['bower', 'install', '--allow-root'])
# update data-files in case this created new files
self.distribution.data_files = get_data_files() self.distribution.data_files = get_data_files()
def get_outputs(self): class CSS(Command):
return [] description = "compile CSS from LESS"
def get_inputs(self): user_options = []
return []
def initialize_options(self):
pass
def finalize_options(self):
pass
def run(self):
style_less = pjoin(static, 'less', 'style.less')
style_css = pjoin(static, 'css', 'style.min.css')
sourcemap = style_css + '.map'
check_call([
'lessc', '-x', '--verbose',
'--source-map-basepath={}'.format(static),
'--source-map={}'.format(sourcemap),
'--source-map-rootpath=../',
style_less, style_css,
])
# update data-files in case this created new files
self.distribution.data_files = get_data_files()
# ensure bower is run as part of install # ensure bower is run as part of install
install.sub_commands.insert(0, ('bower', None)) install.sub_commands.insert(0, ('bower', None))
install.sub_commands.insert(1, ('css', None))
setup_args['cmdclass'] = { setup_args['cmdclass'] = {
'bower': Bower, 'bower': Bower,
'css': CSS,
} }
# setuptools requirements # setuptools requirements

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -0,0 +1,20 @@
div.error {
margin: 2em;
text-align: center;
}
div.error > h1 {
font-size: 500%;
line-height: normal;
}
div.error > p {
font-size: 200%;
line-height: normal;
}
div.traceback-wrapper {
text-align: left;
max-width: 800px;
margin: auto;
}

View File

@@ -0,0 +1,14 @@
div.logout-main {
margin: 2em;
text-align: center;
}
div.logout-main > h1 {
font-size: 400%;
line-height: normal;
}
div.logout-main > p {
font-size: 200%;
line-height: normal;
}

View File

@@ -0,0 +1,3 @@
.jpy-logo {
height: 48px;
}

View File

@@ -0,0 +1,26 @@
/*!
*
* Twitter Bootstrap
*
*/
@import "../components/bootstrap/less/bootstrap.less";
@import "../components/bootstrap/less/responsive-utilities.less";
/*!
*
* Font Awesome
*
*/
@import "../components/font-awesome/less/font-awesome.less";
@fa-font-path: "../components/font-awesome/fonts";
/*!
*
* Jupyter
*
*/
@import "./variables.less";
@import "./page.less";
@import "./error.less";
@import "./logout.less";

View File

View File

@@ -0,0 +1,6 @@
{% extends "error.html" %}
{% block error_detail %}
<p>Jupyter has lots of moons, but this is not one...</p>
{% endblock %}

View File

@@ -0,0 +1,22 @@
{% extends "page.html" %}
{% block login_widget %}
{% endblock %}
{% block main %}
<div class="error">
{% block h1_error %}
<h1>{{status_code}} : {{status_message}}</h1>
{% endblock h1_error %}
{% block error_detail %}
{% if message %}
<p>The error was:</p>
<div class="traceback-wrapper">
<pre class="traceback">{{message}}</pre>
</div>
{% endif %}
{% endblock %}
</header>
{% endblock %}

View File

@@ -1,18 +1,44 @@
<html> {% extends "page.html" %}
<head>
</head>
<body> {% block login_widget %}
{% if message %} {% endblock %}
<div id="message">{{message}}</div>
{% end if %} {% block site %}
<form action="?next={{next}}" method="post">
<input type="text" name="username" id="user_input" value="{{username}}"> <div id="login-main" class="container">
<input type="password" name="password" id="password_input"> <div class="row">
<button type="submit" id="login_submit">Log in</button> <form action="{{login_url}}?next={{next}}" method="post" role="form">
<div class="col-lg-6">
<div class="input-group">
<span class="input-group-addon">Username:</span>
<input type="username" class="form-control" name="username" id="username_input" val="{{username}}">
</div>
</div>
<div class="col-lg-6">
<div class="input-group">
<span class="input-group-addon">Password:</span>
<input type="password" class="form-control" name="password" id="password_input">
<span class="input-group-btn">
<button type="submit" id="login_submit" class="btn btn-default">Log in</button>
</span>
</div>
</div>
</form> </form>
</div>
{% if message %}
<div class="row">
<div class="message">
{{message}}
</div>
</div>
{% endif %}
<div/>
<script type="text/javascript"> {% endblock %}
</script>
</body>
</html> {% block script %}
{{super()}}
{% endblock %}

View File

@@ -0,0 +1,13 @@
{% extends "page.html" %}
{% block login_widget %}
{% endblock %}
{% block main %}
<div class="container logout-main">
<h1>You have been logged out</h1>
<p><a href="{base_url}login">Log in again...</a></p>
<div/>
{% endblock %}

View File

@@ -0,0 +1,76 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>{% block title %}Jupyter Hub{% endblock %}</title>
<link rel="shortcut icon" type="image/x-icon" href="{{static_url("images/favicon.ico") }}">
<meta http-equiv="X-UA-Compatible" content="chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{% block stylesheet %}
<link rel="stylesheet" href="{{ static_url("css/style.min.css") }}" type="text/css"/>
{% endblock %}
<script src="{{static_url("components/requirejs/require.js") }}" type="text/javascript" charset="utf-8"></script>
<script>
require.config({
baseUrl: '{{static_url("", include_version=False)}}',
paths: {
jquery: 'components/jquery/jquery.min',
bootstrap: 'components/bootstrap/js/bootstrap.min',
moment: "components/moment/moment",
},
shim: {
bootstrap: {
deps: ["jquery"],
exports: "bootstrap"
},
}
});
</script>
{% block meta %}
{% endblock %}
</head>
<body {% block params %}{% endblock %}>
<noscript>
<div id='noscript'>
Jupyter Hub requires JavaScript.<br>
Please enable it to proceed.
</div>
</noscript>
<div id="header" class="navbar navbar-static-top">
<div class="container">
<span id="jupyterlogo" class="pull-left"><a href="{{base_url}}home/" alt='dashboard'><img src='{{static_url("images/jupyterlogo.png") }}' alt='Jupyter Hub' class='jpy-logo'/></a></span>
{% block login_widget %}
<span id="login_widget">
{% if logged_in %}
<button id="logout">Logout</button>
{% else %}
<button id="login">Login</button>
{% endif %}
</span>
{% endblock %}
{% block header %}
{% endblock %}
</div>
</div>
{% block main %}
{% endblock %}
{% block script %}
{% endblock %}
</body>
</html>