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
dist
share/jupyter/static/components
share/jupyter/static/css/style.min.css
share/jupyter/static/css/style.min.css.map
*.egg-info
MANIFEST

View File

@@ -23,7 +23,11 @@ Basic principles:
# install the Python pargs (-e for editable/development install)
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
to get node executables to work:

View File

@@ -9,10 +9,12 @@ import logging
import os
from subprocess import Popen
from jinja2 import Environment, FileSystemLoader
import tornado.httpserver
import tornado.options
from tornado.ioloop import IOLoop
from tornado.log import LogFormatter
from tornado.log import LogFormatter, app_log
from tornado import gen, web
from IPython.utils.traitlets import (
@@ -25,6 +27,7 @@ from IPython.utils.importstring import import_item
here = os.path.dirname(__file__)
from .handlers import (
Template404,
RootHandler,
LoginHandler,
LogoutHandler,
@@ -36,6 +39,13 @@ from . import orm
from ._data import DATA_FILES_PATH
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):
"""An Application for starting a Multi-User Notebook server."""
data_files_path = Unicode(DATA_FILES_PATH, config=True,
@@ -64,6 +74,10 @@ class JupyterHubApp(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,
help="""The command to start the http proxy.
@@ -157,7 +171,7 @@ class JupyterHubApp(Application):
def _log_format_default(self):
"""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):
# 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.extend([
(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):
# TODO: load state from db for resume
@@ -240,20 +257,28 @@ class JupyterHubApp(Application):
'--api-ip', self.proxy.api_server.ip,
'--api-port', str(self.proxy.api_server.port),
'--default-target', self.hub.server.host,
'--log-level=debug',
]
if self.ssl_key:
cmd.extend(['--ssl-key', self.ssl_key])
if self.ssl_cert:
cmd.extend(['--ssl-cert', self.ssl_cert])
self.proxy = Popen(cmd, env=env)
def init_tornado_settings(self):
"""Set up the tornado settings dict."""
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(
config=self.config,
# log=self.log,
log=self.log,
db=self.db,
hub=self.hub,
authenticator=import_item(self.authenticator)(config=self.config),
@@ -261,8 +286,10 @@ class JupyterHubApp(Application):
base_url=base_url,
cookie_secret=self.cookie_secret,
login_url=url_path_join(self.hub.server.base_url, 'login'),
template_path=os.path.join(self.data_files_path, 'templates'),
static_files_path=os.path.join(self.data_files_path, 'static'),
static_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
settings.update(self.tornado_settings)

View File

@@ -5,9 +5,16 @@
import json
import re
try:
# py3
from http.client import responses
except ImportError:
from httplib import responses
import requests
from jinja2 import TemplateNotFound
from tornado.log import app_log
from tornado.escape import url_escape
from tornado.httputil import url_concat
@@ -54,26 +61,97 @@ class BaseHandler(RequestHandler):
return cookie_token.user.name
else:
# 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):
username = self.get_current_user()
if username is not None:
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(self.cookie_name, path=self.hub.base_url)
self.clear_cookie(self.hub.server.cookie_name, path=self.hub.server.base_url)
@property
def spawner_class(self):
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):
"""Redirect from / to /user/foo/ after logging in."""
@web.authenticated
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):
@@ -98,24 +176,25 @@ class LogoutHandler(BaseHandler):
"""Log a user out by clearing their login cookie."""
def get(self):
self.clear_login_cookie()
self.write("logged out")
html = self.render_template('logout.html')
self.finish(html)
class LoginHandler(BaseHandler):
"""Render the login page."""
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='')),
username=username,
message=message,
)
def get(self):
if False and self.get_current_user():
self.redirect(self.get_argument('next', default='/'))
if self.get_argument('next', False) and self.get_current_user():
self.redirect(self.get_argument('next'))
else:
username = self.get_argument('username', default='')
self._render(username=username)
self.finish(self._render(username=username))
@gen.coroutine
def notify_proxy(self, user):
@@ -211,13 +290,15 @@ class LoginHandler(BaseHandler):
user = yield self.spawn_single_user(username)
self.set_login_cookies(user)
next_url = self.get_argument('next', default='') or '/user/%s/' % username
print('next', next_url)
self.redirect(next_url)
else:
self.log.debug("Failed login for %s", username)
self._render(
html = self._render(
message={'error': 'Invalid username or password'},
username=username,
)
self.finish(html)
#------------------------------------------------------------------------------

View File

@@ -42,9 +42,12 @@ except NameError:
locs = locs or globs
exec(compile(open(fname).read(), fname, "exec"), globs, locs)
here = os.path.abspath(os.path.dirname(__file__))
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.
#---------------------------------------------------------------------------
@@ -53,7 +56,6 @@ def get_data_files():
"""Get data files in share/jupyter"""
data_files = []
share_jupyter = pjoin(here, 'share', 'jupyter')
ntrim = len(here) + 1
for (d, dirs, filenames) in os.walk(share_jupyter):
@@ -117,19 +119,41 @@ class Bower(Command):
def run(self):
check_call(['bower', 'install', '--allow-root'])
# update data-files in case this created new files
self.distribution.data_files = get_data_files()
class CSS(Command):
description = "compile CSS from LESS"
def get_outputs(self):
return []
user_options = []
def get_inputs(self):
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
install.sub_commands.insert(0, ('bower', None))
install.sub_commands.insert(1, ('css', None))
setup_args['cmdclass'] = {
'bower': Bower,
'css': CSS,
}
# 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>
<head>
</head>
<body>
{% if message %}
<div id="message">{{message}}</div>
{% end if %}
<form action="?next={{next}}" method="post">
<input type="text" name="username" id="user_input" value="{{username}}">
<input type="password" name="password" id="password_input">
<button type="submit" id="login_submit">Log in</button>
</form>
{% extends "page.html" %}
<script type="text/javascript">
</script>
</body>
</html>
{% block login_widget %}
{% endblock %}
{% block site %}
<div id="login-main" class="container">
<div class="row">
<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>
</div>
{% if message %}
<div class="row">
<div class="message">
{{message}}
</div>
</div>
{% endif %}
<div/>
{% endblock %}
{% 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>