mirror of
https://github.com/jupyter/notebook.git
synced 2024-12-27 04:20:22 +08:00
Merge pull request #5001 from ellisonbg/dashboard-dirs
Add directory navigation to dashboard
This commit is contained in:
commit
cd7c1e6fae
@ -22,7 +22,6 @@ import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import stat
|
||||
import sys
|
||||
import traceback
|
||||
try:
|
||||
@ -42,10 +41,7 @@ except ImportError:
|
||||
from IPython.config import Application
|
||||
from IPython.utils.path import filefind
|
||||
from IPython.utils.py3compat import string_types
|
||||
|
||||
# UF_HIDDEN is a stat flag not defined in the stat module.
|
||||
# It is used by BSD to indicate hidden files.
|
||||
UF_HIDDEN = getattr(stat, 'UF_HIDDEN', 32768)
|
||||
from IPython.html.utils import is_hidden
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Top-level handlers
|
||||
@ -269,28 +265,9 @@ class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
|
||||
"""
|
||||
abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
|
||||
abs_root = os.path.abspath(root)
|
||||
self.forbid_hidden(abs_root, abs_path)
|
||||
if is_hidden(abs_path, abs_root):
|
||||
raise web.HTTPError(404)
|
||||
return abs_path
|
||||
|
||||
def forbid_hidden(self, absolute_root, absolute_path):
|
||||
"""Raise 403 if a file is hidden or contained in a hidden directory.
|
||||
|
||||
Hidden is determined by either name starting with '.'
|
||||
or the UF_HIDDEN flag as reported by stat
|
||||
"""
|
||||
inside_root = absolute_path[len(absolute_root):]
|
||||
if any(part.startswith('.') for part in inside_root.split(os.sep)):
|
||||
raise web.HTTPError(403)
|
||||
|
||||
# check UF_HIDDEN on any location up to root
|
||||
path = absolute_path
|
||||
while path and path.startswith(absolute_root) and path != absolute_root:
|
||||
st = os.stat(path)
|
||||
if getattr(st, 'st_flags', 0) & UF_HIDDEN:
|
||||
raise web.HTTPError(403)
|
||||
path = os.path.dirname(path)
|
||||
|
||||
return absolute_path
|
||||
|
||||
|
||||
def json_errors(method):
|
||||
|
@ -29,6 +29,7 @@ from .nbmanager import NotebookManager
|
||||
from IPython.nbformat import current
|
||||
from IPython.utils.traitlets import Unicode, Dict, Bool, TraitError
|
||||
from IPython.utils import tz
|
||||
from IPython.html.utils import is_hidden
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Classes
|
||||
@ -108,7 +109,26 @@ class FileNotebookManager(NotebookManager):
|
||||
path = path.strip('/')
|
||||
os_path = self.get_os_path(path=path)
|
||||
return os.path.isdir(os_path)
|
||||
|
||||
|
||||
def is_hidden(self, path):
|
||||
"""Does the API style path correspond to a hidden directory or file?
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path : string
|
||||
The path to check. This is an API path (`/` separated,
|
||||
relative to base notebook-dir).
|
||||
|
||||
Returns
|
||||
-------
|
||||
exists : bool
|
||||
Whether the path is hidden.
|
||||
|
||||
"""
|
||||
path = path.strip('/')
|
||||
os_path = self.get_os_path(path=path)
|
||||
return is_hidden(os_path, self.notebook_dir)
|
||||
|
||||
def get_os_path(self, name=None, path=''):
|
||||
"""Given a notebook name and a URL path, return its file system
|
||||
path.
|
||||
@ -153,6 +173,47 @@ class FileNotebookManager(NotebookManager):
|
||||
nbpath = self.get_os_path(name, path=path)
|
||||
return os.path.isfile(nbpath)
|
||||
|
||||
# TODO: Remove this after we create the contents web service and directories are
|
||||
# no longer listed by the notebook web service.
|
||||
def list_dirs(self, path):
|
||||
"""List the directories for a given API style path."""
|
||||
path = path.strip('/')
|
||||
os_path = self.get_os_path('', path)
|
||||
if not os.path.isdir(os_path) or is_hidden(os_path, self.notebook_dir):
|
||||
raise web.HTTPError(404, u'directory does not exist: %r' % os_path)
|
||||
dir_names = os.listdir(os_path)
|
||||
dirs = []
|
||||
for name in dir_names:
|
||||
os_path = self.get_os_path(name, path)
|
||||
if os.path.isdir(os_path) and not is_hidden(os_path, self.notebook_dir):
|
||||
try:
|
||||
model = self.get_dir_model(name, path)
|
||||
except IOError:
|
||||
pass
|
||||
dirs.append(model)
|
||||
dirs = sorted(dirs, key=lambda item: item['name'])
|
||||
return dirs
|
||||
|
||||
# TODO: Remove this after we create the contents web service and directories are
|
||||
# no longer listed by the notebook web service.
|
||||
def get_dir_model(self, name, path=''):
|
||||
"""Get the directory model given a directory name and its API style path"""
|
||||
path = path.strip('/')
|
||||
os_path = self.get_os_path(name, path)
|
||||
if not os.path.isdir(os_path):
|
||||
raise IOError('directory does not exist: %r' % os_path)
|
||||
info = os.stat(os_path)
|
||||
last_modified = tz.utcfromtimestamp(info.st_mtime)
|
||||
created = tz.utcfromtimestamp(info.st_ctime)
|
||||
# Create the notebook model.
|
||||
model ={}
|
||||
model['name'] = name
|
||||
model['path'] = path
|
||||
model['last_modified'] = last_modified
|
||||
model['created'] = created
|
||||
model['type'] = 'directory'
|
||||
return model
|
||||
|
||||
def list_notebooks(self, path):
|
||||
"""Returns a list of dictionaries that are the standard model
|
||||
for all notebooks in the relative 'path'.
|
||||
@ -170,10 +231,7 @@ class FileNotebookManager(NotebookManager):
|
||||
"""
|
||||
path = path.strip('/')
|
||||
notebook_names = self.get_notebook_names(path)
|
||||
notebooks = []
|
||||
for name in notebook_names:
|
||||
model = self.get_notebook_model(name, path, content=False)
|
||||
notebooks.append(model)
|
||||
notebooks = [self.get_notebook_model(name, path, content=False) for name in notebook_names]
|
||||
notebooks = sorted(notebooks, key=lambda item: item['name'])
|
||||
return notebooks
|
||||
|
||||
@ -207,6 +265,7 @@ class FileNotebookManager(NotebookManager):
|
||||
model['path'] = path
|
||||
model['last_modified'] = last_modified
|
||||
model['created'] = created
|
||||
model['type'] = 'notebook'
|
||||
if content:
|
||||
with io.open(os_path, 'r', encoding='utf-8') as f:
|
||||
try:
|
||||
@ -223,7 +282,7 @@ class FileNotebookManager(NotebookManager):
|
||||
|
||||
if 'content' not in model:
|
||||
raise web.HTTPError(400, u'No notebook JSON data provided')
|
||||
|
||||
|
||||
# One checkpoint should always exist
|
||||
if self.notebook_exists(name, path) and not self.list_checkpoints(name, path):
|
||||
self.create_checkpoint(name, path)
|
||||
|
@ -69,8 +69,18 @@ class NotebookHandler(IPythonHandler):
|
||||
nbm = self.notebook_manager
|
||||
# Check to see if a notebook name was given
|
||||
if name is None:
|
||||
# List notebooks in 'path'
|
||||
notebooks = nbm.list_notebooks(path)
|
||||
# TODO: Remove this after we create the contents web service and directories are
|
||||
# no longer listed by the notebook web service. This should only handle notebooks
|
||||
# and not directories.
|
||||
dirs = nbm.list_dirs(path)
|
||||
notebooks = []
|
||||
index = []
|
||||
for nb in nbm.list_notebooks(path):
|
||||
if nb['name'].lower() == 'index.ipynb':
|
||||
index.append(nb)
|
||||
else:
|
||||
notebooks.append(nb)
|
||||
notebooks = index + dirs + notebooks
|
||||
self.finish(json.dumps(notebooks, default=date_default))
|
||||
return
|
||||
# get and return notebook representation
|
||||
|
@ -82,7 +82,24 @@ class NotebookManager(LoggingConfigurable):
|
||||
Whether the path does indeed exist.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def is_hidden(self, path):
|
||||
"""Does the API style path correspond to a hidden directory or file?
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path : string
|
||||
The path to check. This is an API path (`/` separated,
|
||||
relative to base notebook-dir).
|
||||
|
||||
Returns
|
||||
-------
|
||||
exists : bool
|
||||
Whether the path is hidden.
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def _notebook_dir_changed(self, name, old, new):
|
||||
"""Do a bit of validation of the notebook dir."""
|
||||
if not os.path.isabs(new):
|
||||
@ -112,6 +129,26 @@ class NotebookManager(LoggingConfigurable):
|
||||
"""
|
||||
return basename
|
||||
|
||||
# TODO: Remove this after we create the contents web service and directories are
|
||||
# no longer listed by the notebook web service.
|
||||
def list_dirs(self, path):
|
||||
"""List the directory models for a given API style path."""
|
||||
raise NotImplementedError('must be implemented in a subclass')
|
||||
|
||||
# TODO: Remove this after we create the contents web service and directories are
|
||||
# no longer listed by the notebook web service.
|
||||
def get_dir_model(self, name, path=''):
|
||||
"""Get the directory model given a directory name and its API style path.
|
||||
|
||||
The keys in the model should be:
|
||||
* name
|
||||
* path
|
||||
* last_modified
|
||||
* created
|
||||
* type='directory'
|
||||
"""
|
||||
raise NotImplementedError('must be implemented in a subclass')
|
||||
|
||||
def list_notebooks(self, path=''):
|
||||
"""Return a list of notebook dicts without content.
|
||||
|
||||
|
@ -15,6 +15,7 @@ from IPython.html.utils import url_path_join
|
||||
from ..filenbmanager import FileNotebookManager
|
||||
from ..nbmanager import NotebookManager
|
||||
|
||||
|
||||
class TestFileNotebookManager(TestCase):
|
||||
|
||||
def test_nb_dir(self):
|
||||
@ -67,7 +68,7 @@ class TestNotebookManager(TestCase):
|
||||
try:
|
||||
os.makedirs(os_path)
|
||||
except OSError:
|
||||
print("Directory already exists.")
|
||||
print("Directory already exists: %r" % os_path)
|
||||
|
||||
def test_create_notebook_model(self):
|
||||
with TemporaryDirectory() as td:
|
||||
|
@ -21,6 +21,12 @@ from IPython.utils import py3compat
|
||||
from IPython.utils.data import uniq_stable
|
||||
|
||||
|
||||
# TODO: Remove this after we create the contents web service and directories are
|
||||
# no longer listed by the notebook web service.
|
||||
def notebooks_only(nb_list):
|
||||
return [nb for nb in nb_list if nb['type']=='notebook']
|
||||
|
||||
|
||||
class NBAPI(object):
|
||||
"""Wrapper for notebook API calls."""
|
||||
def __init__(self, base_url):
|
||||
@ -125,25 +131,25 @@ class APITest(NotebookTestBase):
|
||||
os.unlink(pjoin(nbdir, 'inroot.ipynb'))
|
||||
|
||||
def test_list_notebooks(self):
|
||||
nbs = self.nb_api.list().json()
|
||||
nbs = notebooks_only(self.nb_api.list().json())
|
||||
self.assertEqual(len(nbs), 1)
|
||||
self.assertEqual(nbs[0]['name'], 'inroot.ipynb')
|
||||
|
||||
nbs = self.nb_api.list('/Directory with spaces in/').json()
|
||||
nbs = notebooks_only(self.nb_api.list('/Directory with spaces in/').json())
|
||||
self.assertEqual(len(nbs), 1)
|
||||
self.assertEqual(nbs[0]['name'], 'inspace.ipynb')
|
||||
|
||||
nbs = self.nb_api.list(u'/unicodé/').json()
|
||||
nbs = notebooks_only(self.nb_api.list(u'/unicodé/').json())
|
||||
self.assertEqual(len(nbs), 1)
|
||||
self.assertEqual(nbs[0]['name'], 'innonascii.ipynb')
|
||||
self.assertEqual(nbs[0]['path'], u'unicodé')
|
||||
|
||||
nbs = self.nb_api.list('/foo/bar/').json()
|
||||
nbs = notebooks_only(self.nb_api.list('/foo/bar/').json())
|
||||
self.assertEqual(len(nbs), 1)
|
||||
self.assertEqual(nbs[0]['name'], 'baz.ipynb')
|
||||
self.assertEqual(nbs[0]['path'], 'foo/bar')
|
||||
|
||||
nbs = self.nb_api.list('foo').json()
|
||||
nbs = notebooks_only(self.nb_api.list('foo').json())
|
||||
self.assertEqual(len(nbs), 4)
|
||||
nbnames = { normalize('NFC', n['name']) for n in nbs }
|
||||
expected = [ u'a.ipynb', u'b.ipynb', u'name with spaces.ipynb', u'unicodé.ipynb']
|
||||
@ -231,7 +237,7 @@ class APITest(NotebookTestBase):
|
||||
self.assertEqual(resp.status_code, 204)
|
||||
|
||||
for d in self.dirs + ['/']:
|
||||
nbs = self.nb_api.list(d).json()
|
||||
nbs = notebooks_only(self.nb_api.list(d).json())
|
||||
self.assertEqual(len(nbs), 0)
|
||||
|
||||
def test_rename(self):
|
||||
@ -240,7 +246,7 @@ class APITest(NotebookTestBase):
|
||||
self.assertEqual(resp.json()['name'], 'z.ipynb')
|
||||
assert os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'z.ipynb'))
|
||||
|
||||
nbs = self.nb_api.list('foo').json()
|
||||
nbs = notebooks_only(self.nb_api.list('foo').json())
|
||||
nbnames = set(n['name'] for n in nbs)
|
||||
self.assertIn('z.ipynb', nbnames)
|
||||
self.assertNotIn('a.ipynb', nbnames)
|
||||
|
25
IPython/html/static/style/ipython.min.css
vendored
25
IPython/html/static/style/ipython.min.css
vendored
@ -28,22 +28,29 @@ div.traceback-wrapper{text-align:left;max-width:800px;margin:auto}
|
||||
.center-nav{display:inline-block;margin-bottom:-4px}
|
||||
.alternate_upload{background-color:none;display:inline}
|
||||
.alternate_upload.form{padding:0;margin:0}
|
||||
.alternate_upload input.fileinput{background-color:#f00;position:relative;opacity:0;z-index:2;width:295px;margin-left:163px;cursor:pointer}
|
||||
.list_toolbar{padding:5px;height:25px;line-height:25px}
|
||||
.toolbar_info{float:left}
|
||||
.toolbar_buttons{float:right}
|
||||
.alternate_upload input.fileinput{background-color:#f00;position:relative;opacity:0;z-index:2;width:295px;margin-left:163px;cursor:pointer;height:26px}
|
||||
ul#tabs{margin-bottom:4px}
|
||||
ul#tabs a{padding-top:4px;padding-bottom:4px}
|
||||
ul.breadcrumb a:focus,ul.breadcrumb a:hover{text-decoration:none}
|
||||
ul.breadcrumb i.icon-home{font-size:16px;margin-right:4px}
|
||||
ul.breadcrumb span{color:#5e5e5e}
|
||||
.list_toolbar{padding:4px 0 4px 0}
|
||||
.list_toolbar [class*="span"]{min-height:26px}
|
||||
.list_header{font-weight:bold}
|
||||
.list_container{margin-top:16px;margin-bottom:16px;border:1px solid #ababab;border-radius:4px}
|
||||
.list_container{margin-top:4px;margin-bottom:20px;border:1px solid #ababab;border-radius:4px}
|
||||
.list_container>div{border-bottom:1px solid #ababab}.list_container>div:hover .list-item{background-color:#f00}
|
||||
.list_container>div:last-child{border:none}
|
||||
.list_item:hover .list_item{background-color:#ddd}
|
||||
.item_name{line-height:24px}
|
||||
.list_container>div>span,.list_container>div>div{padding:8px}
|
||||
.list_item a{text-decoration:none}
|
||||
input.nbname_input{height:15px}
|
||||
.list_header>div,.list_item>div{padding-top:4px;padding-bottom:4px;padding-left:7px;padding-right:7px;height:22px;line-height:22px}
|
||||
.item_name{line-height:22px;height:26px}
|
||||
.item_icon{font-size:14px;color:#5e5e5e;margin-right:7px}
|
||||
.item_buttons{line-height:1em}
|
||||
.toolbar_info{height:26px;line-height:26px}
|
||||
input.nbname_input,input.engine_num_input{padding-top:3px;padding-bottom:3px;height:14px;line-height:14px;margin:0}
|
||||
input.engine_num_input{width:60px}
|
||||
.highlight_text{color:#00f}
|
||||
#project_name>.breadcrumb{padding:0;margin-bottom:0;background-color:transparent;font-weight:bold}
|
||||
input.engine_num_input{height:20px;margin-bottom:2px;padding-top:0;padding-bottom:0;width:60px}
|
||||
.ansibold{font-weight:bold}
|
||||
.ansiblack{color:#000}
|
||||
.ansired{color:#8b0000}
|
||||
|
25
IPython/html/static/style/style.min.css
vendored
25
IPython/html/static/style/style.min.css
vendored
@ -1305,22 +1305,29 @@ span#login_widget{float:right}
|
||||
.center-nav{display:inline-block;margin-bottom:-4px}
|
||||
.alternate_upload{background-color:none;display:inline}
|
||||
.alternate_upload.form{padding:0;margin:0}
|
||||
.alternate_upload input.fileinput{background-color:#f00;position:relative;opacity:0;z-index:2;width:295px;margin-left:163px;cursor:pointer}
|
||||
.list_toolbar{padding:5px;height:25px;line-height:25px}
|
||||
.toolbar_info{float:left}
|
||||
.toolbar_buttons{float:right}
|
||||
.alternate_upload input.fileinput{background-color:#f00;position:relative;opacity:0;z-index:2;width:295px;margin-left:163px;cursor:pointer;height:26px}
|
||||
ul#tabs{margin-bottom:4px}
|
||||
ul#tabs a{padding-top:4px;padding-bottom:4px}
|
||||
ul.breadcrumb a:focus,ul.breadcrumb a:hover{text-decoration:none}
|
||||
ul.breadcrumb i.icon-home{font-size:16px;margin-right:4px}
|
||||
ul.breadcrumb span{color:#5e5e5e}
|
||||
.list_toolbar{padding:4px 0 4px 0}
|
||||
.list_toolbar [class*="span"]{min-height:26px}
|
||||
.list_header{font-weight:bold}
|
||||
.list_container{margin-top:16px;margin-bottom:16px;border:1px solid #ababab;border-radius:4px}
|
||||
.list_container{margin-top:4px;margin-bottom:20px;border:1px solid #ababab;border-radius:4px}
|
||||
.list_container>div{border-bottom:1px solid #ababab}.list_container>div:hover .list-item{background-color:#f00}
|
||||
.list_container>div:last-child{border:none}
|
||||
.list_item:hover .list_item{background-color:#ddd}
|
||||
.item_name{line-height:24px}
|
||||
.list_container>div>span,.list_container>div>div{padding:8px}
|
||||
.list_item a{text-decoration:none}
|
||||
input.nbname_input{height:15px}
|
||||
.list_header>div,.list_item>div{padding-top:4px;padding-bottom:4px;padding-left:7px;padding-right:7px;height:22px;line-height:22px}
|
||||
.item_name{line-height:22px;height:26px}
|
||||
.item_icon{font-size:14px;color:#5e5e5e;margin-right:7px}
|
||||
.item_buttons{line-height:1em}
|
||||
.toolbar_info{height:26px;line-height:26px}
|
||||
input.nbname_input,input.engine_num_input{padding-top:3px;padding-bottom:3px;height:14px;line-height:14px;margin:0}
|
||||
input.engine_num_input{width:60px}
|
||||
.highlight_text{color:#00f}
|
||||
#project_name>.breadcrumb{padding:0;margin-bottom:0;background-color:transparent;font-weight:bold}
|
||||
input.engine_num_input{height:20px;margin-bottom:2px;padding-top:0;padding-bottom:0;width:60px}
|
||||
.ansibold{font-weight:bold}
|
||||
.ansiblack{color:#000}
|
||||
.ansired{color:#8b0000}
|
||||
|
@ -100,16 +100,16 @@ var IPython = (function (IPython) {
|
||||
|
||||
ClusterItem.prototype.state_stopped = function () {
|
||||
var that = this;
|
||||
var profile_col = $('<span/>').addClass('profile_col span4').text(this.data.profile);
|
||||
var status_col = $('<span/>').addClass('status_col span3').text('stopped');
|
||||
var engines_col = $('<span/>').addClass('engine_col span3');
|
||||
var profile_col = $('<div/>').addClass('profile_col span4').text(this.data.profile);
|
||||
var status_col = $('<div/>').addClass('status_col span3').text('stopped');
|
||||
var engines_col = $('<div/>').addClass('engine_col span3');
|
||||
var input = $('<input/>').attr('type','number')
|
||||
.attr('min',1)
|
||||
.attr('size',3)
|
||||
.addClass('engine_num_input');
|
||||
engines_col.append(input);
|
||||
var start_button = $('<button/>').addClass("btn btn-mini").text("Start");
|
||||
var action_col = $('<span/>').addClass('action_col span2').append(
|
||||
var action_col = $('<div/>').addClass('action_col span2').append(
|
||||
$("<span/>").addClass("item_buttons btn-group").append(
|
||||
start_button
|
||||
)
|
||||
@ -151,11 +151,11 @@ var IPython = (function (IPython) {
|
||||
|
||||
ClusterItem.prototype.state_running = function () {
|
||||
var that = this;
|
||||
var profile_col = $('<span/>').addClass('profile_col span4').text(this.data.profile);
|
||||
var status_col = $('<span/>').addClass('status_col span3').text('running');
|
||||
var engines_col = $('<span/>').addClass('engines_col span3').text(this.data.n);
|
||||
var profile_col = $('<div/>').addClass('profile_col span4').text(this.data.profile);
|
||||
var status_col = $('<div/>').addClass('status_col span3').text('running');
|
||||
var engines_col = $('<div/>').addClass('engines_col span3').text(this.data.n);
|
||||
var stop_button = $('<button/>').addClass("btn btn-mini").text("Stop");
|
||||
var action_col = $('<span/>').addClass('action_col span2').append(
|
||||
var action_col = $('<div/>').addClass('action_col span2').append(
|
||||
$("<span/>").addClass("item_buttons btn-group").append(
|
||||
stop_button
|
||||
)
|
||||
|
@ -70,11 +70,10 @@ var IPython = (function (IPython) {
|
||||
var reader = new FileReader();
|
||||
reader.readAsText(f);
|
||||
var name_and_ext = utils.splitext(f.name);
|
||||
var nbname = name_and_ext[0];
|
||||
var file_ext = name_and_ext[1];
|
||||
if (file_ext === '.ipynb') {
|
||||
var item = that.new_notebook_item(0);
|
||||
that.add_name_input(nbname, item);
|
||||
that.add_name_input(f.name, item);
|
||||
// Store the notebook item in the reader so we can use it later
|
||||
// to know which item it belongs to.
|
||||
$(reader).data('item', item);
|
||||
@ -167,26 +166,37 @@ var IPython = (function (IPython) {
|
||||
if (param !== undefined && param.msg) {
|
||||
message = param.msg;
|
||||
}
|
||||
var item = null;
|
||||
var len = data.length;
|
||||
this.clear_list();
|
||||
if (len === 0) {
|
||||
$(this.new_notebook_item(0))
|
||||
.append(
|
||||
$('<div style="margin:auto;text-align:center;color:grey"/>')
|
||||
.text(message)
|
||||
);
|
||||
item = this.new_notebook_item(0);
|
||||
var span12 = item.children().first();
|
||||
span12.empty();
|
||||
span12.append($('<div style="margin:auto;text-align:center;color:grey"/>').text(message));
|
||||
}
|
||||
var path = this.notebookPath();
|
||||
var offset = 0;
|
||||
if (path !== '') {
|
||||
item = this.new_notebook_item(0);
|
||||
this.add_dir(path, '..', item);
|
||||
offset = 1;
|
||||
}
|
||||
for (var i=0; i<len; i++) {
|
||||
var name = data[i].name;
|
||||
var path = this.notebookPath();
|
||||
var nbname = utils.splitext(name)[0];
|
||||
var item = this.new_notebook_item(i);
|
||||
this.add_link(path, nbname, item);
|
||||
name = utils.url_path_join(path, name);
|
||||
if(this.sessions[name] === undefined){
|
||||
this.add_delete_button(item);
|
||||
if (data[i].type === 'directory') {
|
||||
var name = data[i].name;
|
||||
item = this.new_notebook_item(i+offset);
|
||||
this.add_dir(path, name, item);
|
||||
} else {
|
||||
this.add_shutdown_button(item,this.sessions[name]);
|
||||
var name = data[i].name;
|
||||
item = this.new_notebook_item(i+offset);
|
||||
this.add_link(path, name, item);
|
||||
name = utils.url_path_join(path, name);
|
||||
if(this.sessions[name] === undefined){
|
||||
this.add_delete_button(item);
|
||||
} else {
|
||||
this.add_shutdown_button(item,this.sessions[name]);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -197,6 +207,8 @@ var IPython = (function (IPython) {
|
||||
// item.addClass('list_item ui-widget ui-widget-content ui-helper-clearfix');
|
||||
// item.css('border-top-style','none');
|
||||
item.append($("<div/>").addClass("span12").append(
|
||||
$('<i/>').addClass('item_icon')
|
||||
).append(
|
||||
$("<a/>").addClass("item_link").append(
|
||||
$("<span/>").addClass("item_name")
|
||||
)
|
||||
@ -213,17 +225,35 @@ var IPython = (function (IPython) {
|
||||
};
|
||||
|
||||
|
||||
NotebookList.prototype.add_dir = function (path, name, item) {
|
||||
item.data('name', name);
|
||||
item.data('path', path);
|
||||
item.find(".item_name").text(name);
|
||||
item.find(".item_icon").addClass('icon-folder-open');
|
||||
item.find("a.item_link")
|
||||
.attr('href',
|
||||
utils.url_join_encode(
|
||||
this.baseProjectUrl(),
|
||||
"tree",
|
||||
path,
|
||||
name
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
NotebookList.prototype.add_link = function (path, nbname, item) {
|
||||
item.data('nbname', nbname);
|
||||
item.data('path', path);
|
||||
item.find(".item_name").text(nbname);
|
||||
item.find(".item_icon").addClass('icon-book');
|
||||
item.find("a.item_link")
|
||||
.attr('href',
|
||||
utils.url_join_encode(
|
||||
this.baseProjectUrl(),
|
||||
"notebooks",
|
||||
path,
|
||||
nbname + ".ipynb"
|
||||
nbname
|
||||
)
|
||||
).attr('target','_blank');
|
||||
};
|
||||
@ -231,10 +261,11 @@ var IPython = (function (IPython) {
|
||||
|
||||
NotebookList.prototype.add_name_input = function (nbname, item) {
|
||||
item.data('nbname', nbname);
|
||||
item.find(".item_icon").addClass('icon-book');
|
||||
item.find(".item_name").empty().append(
|
||||
$('<input/>')
|
||||
.addClass("nbname_input")
|
||||
.attr('value', nbname)
|
||||
.attr('value', utils.splitext(nbname)[0])
|
||||
.attr('size', '30')
|
||||
.attr('type', 'text')
|
||||
);
|
||||
@ -248,7 +279,7 @@ var IPython = (function (IPython) {
|
||||
|
||||
NotebookList.prototype.add_shutdown_button = function (item, session) {
|
||||
var that = this;
|
||||
var shutdown_button = $("<button/>").text("Shutdown").addClass("btn btn-mini").
|
||||
var shutdown_button = $("<button/>").text("Shutdown").addClass("btn btn-mini btn-danger").
|
||||
click(function (e) {
|
||||
var settings = {
|
||||
processData : false,
|
||||
@ -303,7 +334,7 @@ var IPython = (function (IPython) {
|
||||
notebooklist.baseProjectUrl(),
|
||||
'api/notebooks',
|
||||
notebooklist.notebookPath(),
|
||||
nbname + '.ipynb'
|
||||
nbname
|
||||
);
|
||||
$.ajax(url, settings);
|
||||
}
|
||||
@ -323,6 +354,9 @@ var IPython = (function (IPython) {
|
||||
.addClass('btn btn-primary btn-mini upload_button')
|
||||
.click(function (e) {
|
||||
var nbname = item.find('.item_name > input').val();
|
||||
if (nbname.slice(nbname.length-6, nbname.length) != ".ipynb") {
|
||||
nbname = nbname + ".ipynb";
|
||||
}
|
||||
var path = that.notebookPath();
|
||||
var nbdata = item.data('nbdata');
|
||||
var content_type = 'application/json';
|
||||
@ -349,7 +383,7 @@ var IPython = (function (IPython) {
|
||||
that.baseProjectUrl(),
|
||||
'api/notebooks',
|
||||
that.notebookPath(),
|
||||
nbname + '.ipynb'
|
||||
nbname
|
||||
);
|
||||
$.ajax(url, settings);
|
||||
return false;
|
||||
|
@ -22,4 +22,5 @@
|
||||
width: 295px;
|
||||
margin-left:163px;
|
||||
cursor: pointer;
|
||||
height: 26px;
|
||||
}
|
||||
|
@ -5,21 +5,43 @@
|
||||
* Author: IPython Development Team
|
||||
*/
|
||||
|
||||
#tabs {
|
||||
@dashboard_tb_pad: 4px;
|
||||
@dashboard_lr_pad: 7px;
|
||||
// These are the total heights of the Bootstrap small and mini buttons. These values
|
||||
// are not less variables so we have to track them statically.
|
||||
@btn_small_height: 26px;
|
||||
@btn_mini_height: 22px;
|
||||
@dark_dashboard_color: darken(@border_color, 30%);
|
||||
|
||||
ul#tabs {
|
||||
margin-bottom: @dashboard_tb_pad;
|
||||
}
|
||||
|
||||
ul#tabs a {
|
||||
padding-top: @dashboard_tb_pad;
|
||||
padding-bottom: @dashboard_tb_pad;
|
||||
}
|
||||
|
||||
ul.breadcrumb {
|
||||
a:focus, a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
i.icon-home {
|
||||
font-size: 16px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
span {
|
||||
color: @dark_dashboard_color;
|
||||
}
|
||||
}
|
||||
|
||||
.list_toolbar {
|
||||
padding: 5px;
|
||||
height: 25px;
|
||||
line-height: 25px;
|
||||
padding: @dashboard_tb_pad 0 @dashboard_tb_pad 0;
|
||||
}
|
||||
|
||||
.toolbar_info {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.toolbar_buttons {
|
||||
float: right;
|
||||
.list_toolbar [class*="span"] {
|
||||
min-height: @btn_small_height;
|
||||
}
|
||||
|
||||
.list_header {
|
||||
@ -27,8 +49,8 @@
|
||||
}
|
||||
|
||||
.list_container {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 16px;
|
||||
margin-top: @dashboard_tb_pad;
|
||||
margin-bottom: 5*@dashboard_tb_pad;
|
||||
border: 1px solid @border_color;
|
||||
border-radius: 4px;
|
||||
}
|
||||
@ -48,42 +70,55 @@
|
||||
&:hover .list_item {
|
||||
background-color: #ddd;
|
||||
};
|
||||
a {text-decoration: none;}
|
||||
}
|
||||
|
||||
.list_header>div, .list_item>div {
|
||||
padding-top: @dashboard_tb_pad;
|
||||
padding-bottom: @dashboard_tb_pad;
|
||||
padding-left: @dashboard_lr_pad;
|
||||
padding-right: @dashboard_lr_pad;
|
||||
height: @btn_mini_height;
|
||||
line-height: @btn_mini_height;
|
||||
}
|
||||
|
||||
.item_name {
|
||||
line-height: 24px;
|
||||
line-height: @btn_mini_height;
|
||||
height: @btn_small_height;
|
||||
}
|
||||
|
||||
.list_container > div > span, .list_container > div > div {
|
||||
padding: 8px;
|
||||
.item_icon {
|
||||
font-size: 14px;
|
||||
color: @dark_dashboard_color;
|
||||
margin-right: @dashboard_lr_pad;
|
||||
}
|
||||
|
||||
|
||||
.list_item a {
|
||||
text-decoration: none;
|
||||
.item_buttons {
|
||||
line-height: 1em;
|
||||
}
|
||||
|
||||
.profile_col {
|
||||
.toolbar_info {
|
||||
height: @btn_small_height;
|
||||
line-height: @btn_small_height;
|
||||
}
|
||||
|
||||
.status_col {
|
||||
input.nbname_input, input.engine_num_input {
|
||||
// These settings give these inputs a height that matches @btn_mini_height = 22
|
||||
padding-top: 3px;
|
||||
padding-bottom: 3px;
|
||||
height: 14px;
|
||||
line-height: 14px;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.engines_col {
|
||||
}
|
||||
|
||||
.action_col {
|
||||
}
|
||||
|
||||
input.nbname_input {
|
||||
height: 15px;
|
||||
input.engine_num_input {
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
.highlight_text {
|
||||
color: blue;
|
||||
}
|
||||
|
||||
|
||||
#project_name > .breadcrumb {
|
||||
padding: 0px;
|
||||
margin-bottom: 0px;
|
||||
@ -92,10 +127,4 @@ input.nbname_input {
|
||||
|
||||
}
|
||||
|
||||
input.engine_num_input {
|
||||
height: 20px;
|
||||
margin-bottom:2px;
|
||||
padding-top:0;
|
||||
padding-bottom:0;
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
{% extends "page.html" %}
|
||||
|
||||
{% block title %}IPython Dashboard{% endblock %}
|
||||
{% block title %}{{page_title}}{% endblock %}
|
||||
|
||||
|
||||
{% block stylesheet %}
|
||||
@ -22,33 +22,38 @@ data-base-kernel-url="{{base_kernel_url}}"
|
||||
|
||||
<div id="ipython-main-app" class="container">
|
||||
|
||||
<div id="tabs" class="tabbable">
|
||||
<ul class="nav nav-tabs" id="tabs">
|
||||
<div id="tab_content" class="tabbable">
|
||||
<ul id="tabs" class="nav nav-tabs">
|
||||
<li class="active"><a href="#notebooks" data-toggle="tab">Notebooks</a></li>
|
||||
<li><a href="#clusters" data-toggle="tab">Clusters</a></li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
<div id="notebooks" class="tab-pane active">
|
||||
<div id="notebook_toolbar">
|
||||
<form id='alternate_upload' class='alternate_upload' >
|
||||
<span id="drag_info" style="position:absolute" >
|
||||
To import a notebook, drag the file onto the listing below or <strong>click here</strong>.
|
||||
</span>
|
||||
<input type="file" name="datafile" class="fileinput" multiple='multiple'>
|
||||
</form>
|
||||
<span id="notebook_buttons">
|
||||
<button id="refresh_notebook_list" title="Refresh notebook list" class="btn btn-small">Refresh</button>
|
||||
<button id="new_notebook" title="Create new notebook" class="btn btn-small">New Notebook</button>
|
||||
</span>
|
||||
<div id="notebook_toolbar" class="row-fluid">
|
||||
<div class="span8">
|
||||
<form id='alternate_upload' class='alternate_upload' >
|
||||
<span id="drag_info" style="position:absolute" >
|
||||
To import a notebook, drag the file onto the listing below or <strong>click here</strong>.
|
||||
</span>
|
||||
<input type="file" name="datafile" class="fileinput" multiple='multiple'>
|
||||
</form>
|
||||
</div>
|
||||
<div class="span4 clearfix">
|
||||
<span id="notebook_buttons" class="pull-right">
|
||||
<button id="new_notebook" title="Create new notebook" class="btn btn-small">New Notebook</button>
|
||||
<button id="refresh_notebook_list" title="Refresh notebook list" class="btn btn-small"><i class="icon-refresh"></i></button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="notebook_list">
|
||||
<div id="notebook_list_header" class="row-fluid list_header">
|
||||
<div id="project_name">
|
||||
<ul class="breadcrumb">
|
||||
{% for component in tree_url_path.strip('/').split('/') %}
|
||||
<li>{{component}} <span>/</span></li>
|
||||
<li><a href="{{breadcrumbs[0][0]}}"><i class="icon-home"></i></a><span>/</span></li>
|
||||
{% for crumb in breadcrumbs[1:] %}
|
||||
<li><a href="{{crumb[0]}}">{{crumb[1]}}</a> <span>/</span></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
@ -58,20 +63,23 @@ data-base-kernel-url="{{base_kernel_url}}"
|
||||
|
||||
<div id="clusters" class="tab-pane">
|
||||
|
||||
<div id="cluster_toolbar">
|
||||
<span id="cluster_list_info">IPython parallel computing clusters</span>
|
||||
|
||||
<span id="cluster_buttons">
|
||||
<button id="refresh_cluster_list" title="Refresh cluster list" class="btn btn-small">Refresh</button>
|
||||
</span>
|
||||
<div id="cluster_toolbar" class="row-fluid">
|
||||
<div class="span8">
|
||||
<span id="cluster_list_info">IPython parallel computing clusters</span>
|
||||
</div>
|
||||
<div class="span4" class="clearfix">
|
||||
<span id="cluster_buttons" class="pull-right">
|
||||
<button id="refresh_cluster_list" title="Refresh cluster list" class="btn btn-small"><i class="icon-refresh"></i></button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="cluster_list">
|
||||
<div id="cluster_list_header" class="row-fluid list_header">
|
||||
<span class="profile_col span4">profile</span>
|
||||
<span class="status_col span3">status</span>
|
||||
<span class="engines_col span3" title="Enter the number of engines to start or empty for default"># of engines</span>
|
||||
<span class="action_col span2">action</span>
|
||||
<div class="profile_col span4">profile</div>
|
||||
<div class="status_col span3">status</div>
|
||||
<div class="engines_col span3" title="Enter the number of engines to start or empty for default"># of engines</div>
|
||||
<div class="action_col span2">action</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
37
IPython/html/tests/casperjs/test_cases/dashboard_nav.js
Normal file
37
IPython/html/tests/casperjs/test_cases/dashboard_nav.js
Normal file
@ -0,0 +1,37 @@
|
||||
|
||||
|
||||
casper.get_list_items = function () {
|
||||
return this.evaluate(function () {
|
||||
return $.makeArray($('.item_link').map(function () {
|
||||
return {
|
||||
link: $(this).attr('href'),
|
||||
label: $(this).find('.item_name').text()
|
||||
}
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
casper.test_items = function (baseUrl) {
|
||||
casper.then(function () {
|
||||
var items = casper.get_list_items();
|
||||
casper.each(items, function (self, item) {
|
||||
if (!item.label.match('.ipynb$')) {
|
||||
var followed_url = baseUrl+item.link;
|
||||
if (!followed_url.match('/\.\.$')) {
|
||||
casper.thenOpen(baseUrl+item.link, function () {
|
||||
casper.wait_for_dashboard();
|
||||
this.test.assertEquals(this.getCurrentUrl(), followed_url, 'Testing dashboard link: '+followed_url);
|
||||
casper.test_items(baseUrl);
|
||||
this.back();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
casper.dashboard_test(function () {
|
||||
baseUrl = this.get_notebook_server()
|
||||
casper.test_items(baseUrl);
|
||||
})
|
||||
|
@ -250,6 +250,34 @@ casper.notebook_test = function(test) {
|
||||
});
|
||||
};
|
||||
|
||||
casper.wait_for_dashboard = function () {
|
||||
// Wait for the dashboard list to load.
|
||||
casper.waitForSelector('.list_item');
|
||||
}
|
||||
|
||||
casper.open_dashboard = function () {
|
||||
// Start casper by opening the dashboard page.
|
||||
var baseUrl = this.get_notebook_server();
|
||||
this.start(baseUrl);
|
||||
this.wait_for_dashboard();
|
||||
}
|
||||
|
||||
casper.dashboard_test = function (test) {
|
||||
// Open the dashboard page and run a test.
|
||||
this.open_dashboard();
|
||||
this.then(test);
|
||||
|
||||
this.then(function () {
|
||||
this.page.close();
|
||||
this.page = null;
|
||||
});
|
||||
|
||||
// Run the browser automation.
|
||||
this.run(function() {
|
||||
this.test.done();
|
||||
});
|
||||
}
|
||||
|
||||
casper.options.waitTimeout=10000
|
||||
casper.on('waitFor.timeout', function onWaitForTimeout(timeout) {
|
||||
this.echo("Timeout for " + casper.get_notebook_server());
|
||||
|
@ -1,5 +1,7 @@
|
||||
"""Base class for notebook tests."""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import sys
|
||||
import time
|
||||
import requests
|
||||
|
@ -42,13 +42,13 @@ class FilesTest(NotebookTestBase):
|
||||
r.raise_for_status()
|
||||
self.assertEqual(r.text, 'foo')
|
||||
r = requests.get(url_path_join(url, 'files', d, '.foo'))
|
||||
self.assertEqual(r.status_code, 403)
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
for d in hidden:
|
||||
path = pjoin(nbdir, d.replace('/', os.sep))
|
||||
for foo in ('foo', '.foo'):
|
||||
r = requests.get(url_path_join(url, 'files', d, foo))
|
||||
self.assertEqual(r.status_code, 403)
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
def test_old_files_redirect(self):
|
||||
"""pre-2.0 'files/' prefixed links are properly redirected"""
|
||||
|
@ -11,10 +11,13 @@
|
||||
# Imports
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
import os
|
||||
|
||||
import nose.tools as nt
|
||||
|
||||
import IPython.testing.tools as tt
|
||||
from IPython.html.utils import url_escape, url_unescape
|
||||
from IPython.html.utils import url_escape, url_unescape, is_hidden
|
||||
from IPython.utils.tempdir import TemporaryDirectory
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Test functions
|
||||
@ -59,3 +62,15 @@ def test_url_unescape():
|
||||
'/%20%21%40%24%23%25%5E%26%2A%20/%20test%20%25%5E%20notebook%20%40%23%24%20name.ipynb')
|
||||
nt.assert_equal(path, '/ !@$#%^&* / test %^ notebook @#$ name.ipynb')
|
||||
|
||||
def test_is_hidden():
|
||||
with TemporaryDirectory() as root:
|
||||
subdir1 = os.path.join(root, 'subdir')
|
||||
os.makedirs(subdir1)
|
||||
nt.assert_equal(is_hidden(subdir1, root), False)
|
||||
subdir2 = os.path.join(root, '.subdir2')
|
||||
os.makedirs(subdir2)
|
||||
nt.assert_equal(is_hidden(subdir2, root), True)
|
||||
subdir34 = os.path.join(root, 'subdir3', '.subdir4')
|
||||
os.makedirs(subdir34)
|
||||
nt.assert_equal(is_hidden(subdir34, root), True)
|
||||
nt.assert_equal(is_hidden(subdir34), True)
|
||||
|
@ -19,7 +19,7 @@ import os
|
||||
|
||||
from tornado import web
|
||||
from ..base.handlers import IPythonHandler, notebook_path_regex, path_regex
|
||||
from ..utils import url_path_join, path2url, url2path, url_escape
|
||||
from ..utils import url_path_join, path2url, url2path, url_escape, is_hidden
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Handlers
|
||||
@ -29,6 +29,27 @@ from ..utils import url_path_join, path2url, url2path, url_escape
|
||||
class TreeHandler(IPythonHandler):
|
||||
"""Render the tree view, listing notebooks, clusters, etc."""
|
||||
|
||||
def generate_breadcrumbs(self, path):
|
||||
breadcrumbs = [(url_escape(url_path_join(self.base_project_url, 'tree')), '')]
|
||||
comps = path.split('/')
|
||||
ncomps = len(comps)
|
||||
for i in range(ncomps):
|
||||
if comps[i]:
|
||||
link = url_escape(url_path_join(self.base_project_url, 'tree', *comps[0:i+1]))
|
||||
breadcrumbs.append((link, comps[i]))
|
||||
return breadcrumbs
|
||||
|
||||
def generate_page_title(self, path):
|
||||
comps = path.split('/')
|
||||
if len(comps) > 3:
|
||||
for i in range(len(comps)-2):
|
||||
comps.pop(0)
|
||||
page_title = url_escape(url_path_join(*comps))
|
||||
if page_title:
|
||||
return page_title+'/'
|
||||
else:
|
||||
return 'Home'
|
||||
|
||||
@web.authenticated
|
||||
def get(self, path='', name=None):
|
||||
path = path.strip('/')
|
||||
@ -41,13 +62,16 @@ class TreeHandler(IPythonHandler):
|
||||
self.log.debug("Redirecting %s to %s", self.request.path, url)
|
||||
self.redirect(url)
|
||||
else:
|
||||
if not nbm.path_exists(path=path):
|
||||
# no such directory, 404
|
||||
if not nbm.path_exists(path=path) or nbm.is_hidden(path):
|
||||
# Directory is hidden or does not exist.
|
||||
raise web.HTTPError(404)
|
||||
breadcrumbs = self.generate_breadcrumbs(path)
|
||||
page_title = self.generate_page_title(path)
|
||||
self.write(self.render_template('tree.html',
|
||||
project=self.project_dir,
|
||||
tree_url_path=path,
|
||||
page_title=page_title,
|
||||
notebook_path=path,
|
||||
breadcrumbs=breadcrumbs
|
||||
))
|
||||
|
||||
|
||||
|
@ -12,7 +12,11 @@ Authors:
|
||||
# the file COPYING, distributed as part of this software.
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import os
|
||||
import stat
|
||||
|
||||
try:
|
||||
from urllib.parse import quote, unquote
|
||||
except ImportError:
|
||||
@ -20,6 +24,10 @@ except ImportError:
|
||||
|
||||
from IPython.utils import py3compat
|
||||
|
||||
# UF_HIDDEN is a stat flag not defined in the stat module.
|
||||
# It is used by BSD to indicate hidden files.
|
||||
UF_HIDDEN = getattr(stat, 'UF_HIDDEN', 32768)
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Imports
|
||||
#-----------------------------------------------------------------------------
|
||||
@ -72,3 +80,35 @@ def url_unescape(path):
|
||||
for p in py3compat.unicode_to_str(path).split('/')
|
||||
])
|
||||
|
||||
def is_hidden(abs_path, abs_root=''):
|
||||
"""Is a file is hidden or contained in a hidden directory.
|
||||
|
||||
This will start with the rightmost path element and work backwards to the
|
||||
given root to see if a path is hidden or in a hidden directory. Hidden is
|
||||
determined by either name starting with '.' or the UF_HIDDEN flag as
|
||||
reported by stat.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
abs_path : unicode
|
||||
The absolute path to check for hidden directories.
|
||||
abs_root : unicode
|
||||
The absolute path of the root directory in which hidden directories
|
||||
should be checked for.
|
||||
"""
|
||||
if not abs_root:
|
||||
abs_root = abs_path.split(os.sep, 1)[0] + os.sep
|
||||
inside_root = abs_path[len(abs_root):]
|
||||
if any(part.startswith('.') for part in inside_root.split(os.sep)):
|
||||
return True
|
||||
|
||||
# check UF_HIDDEN on any location up to root
|
||||
path = abs_path
|
||||
while path and path.startswith(abs_root) and path != abs_root:
|
||||
st = os.stat(path)
|
||||
if getattr(st, 'st_flags', 0) & UF_HIDDEN:
|
||||
return True
|
||||
path = os.path.dirname(path)
|
||||
|
||||
return False
|
||||
|
||||
|
@ -167,9 +167,12 @@ class JSController(TestController):
|
||||
self.section = section
|
||||
|
||||
self.ipydir = TemporaryDirectory()
|
||||
# print(self.ipydir.name)
|
||||
self.nbdir = TemporaryDirectory()
|
||||
print("Running notebook tests in directory: %r" % self.nbdir.name)
|
||||
os.makedirs(os.path.join(self.nbdir.name, os.path.join('subdir1', 'subdir1a')))
|
||||
os.makedirs(os.path.join(self.nbdir.name, os.path.join('subdir2', 'subdir2a')))
|
||||
self.dirs.append(self.ipydir)
|
||||
self.env['IPYTHONDIR'] = self.ipydir.name
|
||||
self.dirs.append(self.nbdir)
|
||||
|
||||
def launch(self):
|
||||
# start the ipython notebook, so we get the port number
|
||||
@ -191,7 +194,7 @@ class JSController(TestController):
|
||||
def _init_server(self):
|
||||
"Start the notebook server in a separate process"
|
||||
self.queue = q = Queue()
|
||||
self.server = Process(target=run_webapp, args=(q, self.ipydir.name))
|
||||
self.server = Process(target=run_webapp, args=(q, self.ipydir.name, self.nbdir.name))
|
||||
self.server.start()
|
||||
self.server_port = q.get()
|
||||
|
||||
@ -202,18 +205,17 @@ class JSController(TestController):
|
||||
|
||||
js_test_group_names = {'js'}
|
||||
|
||||
def run_webapp(q, nbdir, loglevel=0):
|
||||
def run_webapp(q, ipydir, nbdir, loglevel=0):
|
||||
"""start the IPython Notebook, and pass port back to the queue"""
|
||||
import os
|
||||
import IPython.html.notebookapp as nbapp
|
||||
import sys
|
||||
sys.stderr = open(os.devnull, 'w')
|
||||
os.environ["IPYTHONDIR"] = nbdir
|
||||
server = nbapp.NotebookApp()
|
||||
args = ['--no-browser']
|
||||
args.append('--notebook-dir='+nbdir)
|
||||
args.append('--profile-dir='+nbdir)
|
||||
args.append('--log-level='+str(loglevel))
|
||||
args.extend(['--ipython-dir', ipydir])
|
||||
args.extend(['--notebook-dir', nbdir])
|
||||
args.extend(['--log-level', str(loglevel)])
|
||||
server.initialize(args)
|
||||
# communicate the port number to the parent process
|
||||
q.put(server.port)
|
||||
|
Loading…
Reference in New Issue
Block a user