From a3ef3109db415dce51094cd930585e51c06e99ac Mon Sep 17 00:00:00 2001 From: "Brian E. Granger" Date: Sun, 2 Feb 2014 15:50:57 -0800 Subject: [PATCH 01/24] Server side logic for directories. --- .../html/services/notebooks/filenbmanager.py | 33 +++++++++++++++++++ IPython/html/static/tree/js/notebooklist.js | 1 + 2 files changed, 34 insertions(+) diff --git a/IPython/html/services/notebooks/filenbmanager.py b/IPython/html/services/notebooks/filenbmanager.py index bac4de245..f0ce2a14d 100644 --- a/IPython/html/services/notebooks/filenbmanager.py +++ b/IPython/html/services/notebooks/filenbmanager.py @@ -153,6 +153,38 @@ class FileNotebookManager(NotebookManager): nbpath = self.get_os_path(name, path=path) return os.path.isfile(nbpath) + def list_dirs(self, path): + """List the directories for a given API style path.""" + path = path.strip('/') + os_path = self.get_os_path('', 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 name.startswith('.'): + model = self.get_dir_model(name, path) + dirs.append(model) + dirs = sorted(dirs, key=lambda item: item['name']) + return dirs + + 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'. @@ -175,6 +207,7 @@ class FileNotebookManager(NotebookManager): model = self.get_notebook_model(name, path, content=False) notebooks.append(model) notebooks = sorted(notebooks, key=lambda item: item['name']) + notebooks = self.list_dirs(path) + notebooks return notebooks def get_notebook_model(self, name, path='', content=True): diff --git a/IPython/html/static/tree/js/notebooklist.js b/IPython/html/static/tree/js/notebooklist.js index cc2f7fba8..8fc5462c6 100644 --- a/IPython/html/static/tree/js/notebooklist.js +++ b/IPython/html/static/tree/js/notebooklist.js @@ -167,6 +167,7 @@ var IPython = (function (IPython) { if (param !== undefined && param.msg) { message = param.msg; } + console.log(data); var len = data.length; this.clear_list(); if (len === 0) { From 45a8d046c857c7445aa0ec59dd3463f60d8e8e61 Mon Sep 17 00:00:00 2001 From: "Brian E. Granger" Date: Sun, 2 Feb 2014 18:20:27 -0800 Subject: [PATCH 02/24] Add support for index.ipynb --- IPython/html/services/notebooks/filenbmanager.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/IPython/html/services/notebooks/filenbmanager.py b/IPython/html/services/notebooks/filenbmanager.py index f0ce2a14d..2276d637f 100644 --- a/IPython/html/services/notebooks/filenbmanager.py +++ b/IPython/html/services/notebooks/filenbmanager.py @@ -202,12 +202,16 @@ class FileNotebookManager(NotebookManager): """ path = path.strip('/') notebook_names = self.get_notebook_names(path) + index = [] notebooks = [] for name in notebook_names: model = self.get_notebook_model(name, path, content=False) - notebooks.append(model) + if name.lower() == 'index.ipynb': + index.append(model) + else: + notebooks.append(model) notebooks = sorted(notebooks, key=lambda item: item['name']) - notebooks = self.list_dirs(path) + notebooks + notebooks = index + self.list_dirs(path) + notebooks return notebooks def get_notebook_model(self, name, path='', content=True): From 86ba08213f7a90cdacaa6b193505a3c7b6ee0758 Mon Sep 17 00:00:00 2001 From: "Brian E. Granger" Date: Sun, 2 Feb 2014 18:20:50 -0800 Subject: [PATCH 03/24] Add directory browsing to the dashboard. --- IPython/html/static/tree/js/notebooklist.js | 54 ++++++++++++++++----- 1 file changed, 43 insertions(+), 11 deletions(-) diff --git a/IPython/html/static/tree/js/notebooklist.js b/IPython/html/static/tree/js/notebooklist.js index 8fc5462c6..a1e3c6c78 100644 --- a/IPython/html/static/tree/js/notebooklist.js +++ b/IPython/html/static/tree/js/notebooklist.js @@ -167,7 +167,6 @@ var IPython = (function (IPython) { if (param !== undefined && param.msg) { message = param.msg; } - console.log(data); var len = data.length; this.clear_list(); if (len === 0) { @@ -177,17 +176,29 @@ var IPython = (function (IPython) { .text(message) ); } + var path = this.notebookPath(); + var offset = 0; + if (path !== '') { + var item = this.new_notebook_item(0); + this.add_dir(path, '..', item); + offset = 1; + } for (var i=0; i").addClass("span12").append( + $('').addClass('item_icon') + ).append( $("").addClass("item_link").append( $("").addClass("item_name") ) @@ -214,10 +227,28 @@ 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_name").text(nbname + '.ipynb'); + item.find(".item_icon").addClass('icon-book'); item.find("a.item_link") .attr('href', utils.url_join_encode( @@ -232,6 +263,7 @@ 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( $('') .addClass("nbname_input") From 775ebc72e04a231b722bf7bbddb6f8b289ed2f3b Mon Sep 17 00:00:00 2001 From: "Brian E. Granger" Date: Sun, 2 Feb 2014 18:21:06 -0800 Subject: [PATCH 04/24] Update styling of dashboard. --- IPython/html/static/style/ipython.min.css | 6 ++++-- IPython/html/static/style/style.min.css | 6 ++++-- IPython/html/static/tree/less/tree.less | 15 ++++++++++----- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/IPython/html/static/style/ipython.min.css b/IPython/html/static/style/ipython.min.css index c1af57105..41347d95a 100644 --- a/IPython/html/static/style/ipython.min.css +++ b/IPython/html/static/style/ipython.min.css @@ -37,10 +37,12 @@ div.traceback-wrapper{text-align:left;max-width:800px;margin:auto} .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} +.list_item a{text-decoration:none} +.list_item .span12{padding:5px} .item_name{line-height:24px} +.item_icon{font-size:14px;color:#5e5e5e;margin-left:5px;margin-right:8px} .list_container>div>span,.list_container>div>div{padding:8px} -.list_item a{text-decoration:none} -input.nbname_input{height:15px} +input.nbname_input{height:15px;margin:0} .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} diff --git a/IPython/html/static/style/style.min.css b/IPython/html/static/style/style.min.css index f36939d94..dd92dc081 100644 --- a/IPython/html/static/style/style.min.css +++ b/IPython/html/static/style/style.min.css @@ -1314,10 +1314,12 @@ span#login_widget{float:right} .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} +.list_item a{text-decoration:none} +.list_item .span12{padding:5px} .item_name{line-height:24px} +.item_icon{font-size:14px;color:#5e5e5e;margin-left:5px;margin-right:8px} .list_container>div>span,.list_container>div>div{padding:8px} -.list_item a{text-decoration:none} -input.nbname_input{height:15px} +input.nbname_input{height:15px;margin:0} .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} diff --git a/IPython/html/static/tree/less/tree.less b/IPython/html/static/tree/less/tree.less index b5154b5c7..ddf2b3863 100644 --- a/IPython/html/static/tree/less/tree.less +++ b/IPython/html/static/tree/less/tree.less @@ -48,19 +48,23 @@ &:hover .list_item { background-color: #ddd; }; + a {text-decoration: none;} + .span12 {padding: 5px;} } .item_name { line-height: 24px; } -.list_container > div > span, .list_container > div > div { - padding: 8px; +.item_icon { + font-size: 14px; + color: darken(@border_color, 30%); + margin-left: 5px; + margin-right: 8px; } - -.list_item a { - text-decoration: none; +.list_container > div > span, .list_container > div > div { + padding: 8px; } .profile_col { @@ -77,6 +81,7 @@ input.nbname_input { height: 15px; + margin: 0px; } .highlight_text { From 2207a4f0dc40687840d1367672d3c5458d6676b4 Mon Sep 17 00:00:00 2001 From: "Brian E. Granger" Date: Sun, 2 Feb 2014 19:41:53 -0800 Subject: [PATCH 05/24] Adding proper breadcrumb support. --- IPython/html/templates/tree.html | 5 +++-- IPython/html/tree/handlers.py | 12 ++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/IPython/html/templates/tree.html b/IPython/html/templates/tree.html index 1ff6fb536..37d8cea38 100644 --- a/IPython/html/templates/tree.html +++ b/IPython/html/templates/tree.html @@ -47,8 +47,9 @@ data-base-kernel-url="{{base_kernel_url}}"
diff --git a/IPython/html/tree/handlers.py b/IPython/html/tree/handlers.py index 03820ca5a..98241dea5 100644 --- a/IPython/html/tree/handlers.py +++ b/IPython/html/tree/handlers.py @@ -29,6 +29,16 @@ 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 + @web.authenticated def get(self, path='', name=None): path = path.strip('/') @@ -44,10 +54,12 @@ class TreeHandler(IPythonHandler): if not nbm.path_exists(path=path): # no such directory, 404 raise web.HTTPError(404) + breadcrumbs = self.generate_breadcrumbs(path) self.write(self.render_template('tree.html', project=self.project_dir, tree_url_path=path, notebook_path=path, + breadcrumbs=breadcrumbs )) From 58a3daba5cb7ef9c652c51dd603b6db45cae77aa Mon Sep 17 00:00:00 2001 From: "Brian E. Granger" Date: Sun, 2 Feb 2014 19:45:04 -0800 Subject: [PATCH 06/24] Tighten up vertical spacing of notebook list. --- IPython/html/static/style/ipython.min.css | 2 +- IPython/html/static/style/style.min.css | 2 +- IPython/html/static/tree/less/tree.less | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/IPython/html/static/style/ipython.min.css b/IPython/html/static/style/ipython.min.css index 41347d95a..53a074062 100644 --- a/IPython/html/static/style/ipython.min.css +++ b/IPython/html/static/style/ipython.min.css @@ -38,7 +38,7 @@ div.traceback-wrapper{text-align:left;max-width:800px;margin:auto} .list_container>div:last-child{border:none} .list_item:hover .list_item{background-color:#ddd} .list_item a{text-decoration:none} -.list_item .span12{padding:5px} +.list_item .span12{padding:4px} .item_name{line-height:24px} .item_icon{font-size:14px;color:#5e5e5e;margin-left:5px;margin-right:8px} .list_container>div>span,.list_container>div>div{padding:8px} diff --git a/IPython/html/static/style/style.min.css b/IPython/html/static/style/style.min.css index dd92dc081..7f2a3c20e 100644 --- a/IPython/html/static/style/style.min.css +++ b/IPython/html/static/style/style.min.css @@ -1315,7 +1315,7 @@ span#login_widget{float:right} .list_container>div:last-child{border:none} .list_item:hover .list_item{background-color:#ddd} .list_item a{text-decoration:none} -.list_item .span12{padding:5px} +.list_item .span12{padding:4px} .item_name{line-height:24px} .item_icon{font-size:14px;color:#5e5e5e;margin-left:5px;margin-right:8px} .list_container>div>span,.list_container>div>div{padding:8px} diff --git a/IPython/html/static/tree/less/tree.less b/IPython/html/static/tree/less/tree.less index ddf2b3863..e4eeeed18 100644 --- a/IPython/html/static/tree/less/tree.less +++ b/IPython/html/static/tree/less/tree.less @@ -49,7 +49,7 @@ background-color: #ddd; }; a {text-decoration: none;} - .span12 {padding: 5px;} + .span12 {padding: 4px;} } .item_name { From 2df5c0b7d1ddc7e9e1ca60da6a2c2d698ffe4ee5 Mon Sep 17 00:00:00 2001 From: "Brian E. Granger" Date: Sun, 2 Feb 2014 19:52:33 -0800 Subject: [PATCH 07/24] Adding btn-danger to Shutdown button. --- IPython/html/static/tree/js/notebooklist.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IPython/html/static/tree/js/notebooklist.js b/IPython/html/static/tree/js/notebooklist.js index a1e3c6c78..e208fe029 100644 --- a/IPython/html/static/tree/js/notebooklist.js +++ b/IPython/html/static/tree/js/notebooklist.js @@ -281,7 +281,7 @@ var IPython = (function (IPython) { NotebookList.prototype.add_shutdown_button = function (item, session) { var that = this; - var shutdown_button = $(" - - +
+
+
+ + To import a notebook, drag the file onto the listing below or click here. + + +
+
+
+ + + + +
@@ -59,20 +63,23 @@ data-base-kernel-url="{{base_kernel_url}}"
-
- IPython parallel computing clusters - - - - +
+
+ IPython parallel computing clusters +
+
+ + + +
- profile - status - # of engines - action +
profile
+
status
+
# of engines
+
action
From 9f9fe08f0f4bb128e8b382fdb52b799dccc718b8 Mon Sep 17 00:00:00 2001 From: "Brian E. Granger" Date: Mon, 3 Feb 2014 10:24:24 -0800 Subject: [PATCH 10/24] Cleaning up the dashboard CSS and fixing small visual problems. --- IPython/html/static/style/ipython.min.css | 10 +++--- IPython/html/static/style/style.min.css | 10 +++--- IPython/html/static/tree/js/notebooklist.js | 16 +++++----- IPython/html/static/tree/less/tree.less | 34 ++++++++++++--------- 4 files changed, 37 insertions(+), 33 deletions(-) diff --git a/IPython/html/static/style/ipython.min.css b/IPython/html/static/style/ipython.min.css index 04b518f44..f715120d3 100644 --- a/IPython/html/static/style/ipython.min.css +++ b/IPython/html/static/style/ipython.min.css @@ -34,20 +34,20 @@ ul#tabs a{padding-top:4px;padding-bottom:4px} .list_toolbar{padding:4px 0 4px 0} .list_toolbar [class*="span"]{min-height:26px} .list_header{font-weight:bold} -.list_container{margin-top:4px;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} .list_item a{text-decoration:none} -.list_header>div,.list_item>div{padding-top:4px;padding-bottom:4px;padding-left:7px;padding-right:7px} -.item_name{line-height:22px} +.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{padding-top:3px;padding-bottom:3px;height:14px;line-height:14px;margin:0} +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} diff --git a/IPython/html/static/style/style.min.css b/IPython/html/static/style/style.min.css index c17e62feb..f55c0b48b 100644 --- a/IPython/html/static/style/style.min.css +++ b/IPython/html/static/style/style.min.css @@ -1311,20 +1311,20 @@ ul#tabs a{padding-top:4px;padding-bottom:4px} .list_toolbar{padding:4px 0 4px 0} .list_toolbar [class*="span"]{min-height:26px} .list_header{font-weight:bold} -.list_container{margin-top:4px;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} .list_item a{text-decoration:none} -.list_header>div,.list_item>div{padding-top:4px;padding-bottom:4px;padding-left:7px;padding-right:7px} -.item_name{line-height:22px} +.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{padding-top:3px;padding-bottom:3px;height:14px;line-height:14px;margin:0} +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} diff --git a/IPython/html/static/tree/js/notebooklist.js b/IPython/html/static/tree/js/notebooklist.js index e208fe029..9ba4790f1 100644 --- a/IPython/html/static/tree/js/notebooklist.js +++ b/IPython/html/static/tree/js/notebooklist.js @@ -167,31 +167,31 @@ 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( - $('
') - .text(message) - ); + var item = this.new_notebook_item(0); + var span12 = item.children().first(); + span12.empty(); + span12.append($('
').text(message)) } var path = this.notebookPath(); var offset = 0; if (path !== '') { - var item = this.new_notebook_item(0); + item = this.new_notebook_item(0); this.add_dir(path, '..', item); offset = 1; } for (var i=0; i .breadcrumb { padding: 0px; margin-bottom: 0px; @@ -102,10 +112,4 @@ input.nbname_input { } -input.engine_num_input { - height: 20px; - margin-bottom:2px; - padding-top:0; - padding-bottom:0; - width: 60px; -} + From c1a8f5af6a57d38cb38c6f15da87234bdd8b98b1 Mon Sep 17 00:00:00 2001 From: "Brian E. Granger" Date: Mon, 3 Feb 2014 10:40:46 -0800 Subject: [PATCH 11/24] Get the existing tests working. --- .../html/services/notebooks/filenbmanager.py | 4 ++++ .../notebooks/tests/test_nbmanager.py | 1 + .../notebooks/tests/test_notebooks_api.py | 20 ++++++++++++------- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/IPython/html/services/notebooks/filenbmanager.py b/IPython/html/services/notebooks/filenbmanager.py index 2276d637f..fd4d70ebc 100644 --- a/IPython/html/services/notebooks/filenbmanager.py +++ b/IPython/html/services/notebooks/filenbmanager.py @@ -153,6 +153,8 @@ 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('/') @@ -167,6 +169,8 @@ class FileNotebookManager(NotebookManager): 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('/') diff --git a/IPython/html/services/notebooks/tests/test_nbmanager.py b/IPython/html/services/notebooks/tests/test_nbmanager.py index 8d0adfae1..1a4245109 100644 --- a/IPython/html/services/notebooks/tests/test_nbmanager.py +++ b/IPython/html/services/notebooks/tests/test_nbmanager.py @@ -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): diff --git a/IPython/html/services/notebooks/tests/test_notebooks_api.py b/IPython/html/services/notebooks/tests/test_notebooks_api.py index 4e6e04de2..e9e17fa32 100644 --- a/IPython/html/services/notebooks/tests/test_notebooks_api.py +++ b/IPython/html/services/notebooks/tests/test_notebooks_api.py @@ -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 'type' not in nb] + + 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) From 717f031e0187cfe06b0490f36b421ed20187f321 Mon Sep 17 00:00:00 2001 From: "Brian E. Granger" Date: Mon, 3 Feb 2014 14:58:53 -0800 Subject: [PATCH 12/24] Adding dashboard navigation tests for dir browsing. --- .../notebooks/tests/test_nbmanager.py | 2 +- .../casperjs/test_cases/dashboard_nav.js | 45 +++++++++++++++++++ IPython/html/tests/launchnotebook.py | 2 + IPython/testing/iptestcontroller.py | 12 +++-- 4 files changed, 56 insertions(+), 5 deletions(-) create mode 100644 IPython/html/tests/casperjs/test_cases/dashboard_nav.js diff --git a/IPython/html/services/notebooks/tests/test_nbmanager.py b/IPython/html/services/notebooks/tests/test_nbmanager.py index 1a4245109..b6a240ecd 100644 --- a/IPython/html/services/notebooks/tests/test_nbmanager.py +++ b/IPython/html/services/notebooks/tests/test_nbmanager.py @@ -68,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: diff --git a/IPython/html/tests/casperjs/test_cases/dashboard_nav.js b/IPython/html/tests/casperjs/test_cases/dashboard_nav.js new file mode 100644 index 000000000..9730b1e2f --- /dev/null +++ b/IPython/html/tests/casperjs/test_cases/dashboard_nav.js @@ -0,0 +1,45 @@ +casper.wait_for_list = function () { + casper.waitForSelector('.list_item'); + // casper.wait(500); +} + +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 (test, 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_list(); + test.assertEquals(this.getCurrentUrl(), followed_url, 'Testing dashboard link: '+followed_url); + casper.test_items(test, baseUrl); + this.back(); + }); + } + } + }); + }); +} + +casper.test.begin('Testing dashboard navigation', function (test) { + var baseUrl = casper.get_notebook_server(); + casper.start(baseUrl); + casper.echo(baseUrl); + casper.wait_for_list(); + casper.test_items(test, baseUrl); + casper.run(function() { + test.done(); + }); +}); diff --git a/IPython/html/tests/launchnotebook.py b/IPython/html/tests/launchnotebook.py index 9f9e0831d..3666a0cf4 100644 --- a/IPython/html/tests/launchnotebook.py +++ b/IPython/html/tests/launchnotebook.py @@ -1,5 +1,7 @@ """Base class for notebook tests.""" +from __future__ import print_function + import sys import time import requests diff --git a/IPython/testing/iptestcontroller.py b/IPython/testing/iptestcontroller.py index 8ad3d7ed5..9e02c9ae7 100644 --- a/IPython/testing/iptestcontroller.py +++ b/IPython/testing/iptestcontroller.py @@ -167,6 +167,10 @@ class JSController(TestController): self.section = section self.ipydir = TemporaryDirectory() + self.nbdir = os.path.join(self.ipydir.name, 'notebooks') + print("Running notebook tests in directory: %r" % self.nbdir) + os.makedirs(os.path.join(self.nbdir, 'subdir1/subdir1a')) + os.makedirs(os.path.join(self.nbdir, 'subdir2/subdir2a')) # print(self.ipydir.name) self.dirs.append(self.ipydir) self.env['IPYTHONDIR'] = self.ipydir.name @@ -191,7 +195,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)) self.server.start() self.server_port = q.get() @@ -202,17 +206,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 + os.environ["IPYTHONDIR"] = ipydir server = nbapp.NotebookApp() args = ['--no-browser'] args.append('--notebook-dir='+nbdir) - args.append('--profile-dir='+nbdir) + args.append('--profile-dir='+ipydir) args.append('--log-level='+str(loglevel)) server.initialize(args) # communicate the port number to the parent process From 6ac7f6c2b4a09ee23e2c424e75fd10da49451bef Mon Sep 17 00:00:00 2001 From: "Brian E. Granger" Date: Mon, 3 Feb 2014 16:05:03 -0800 Subject: [PATCH 13/24] Fixing casperjs tests to run on casperjs 1.0.x. --- .../casperjs/test_cases/dashboard_nav.js | 28 +++++++------------ IPython/html/tests/casperjs/util.js | 28 +++++++++++++++++++ 2 files changed, 38 insertions(+), 18 deletions(-) diff --git a/IPython/html/tests/casperjs/test_cases/dashboard_nav.js b/IPython/html/tests/casperjs/test_cases/dashboard_nav.js index 9730b1e2f..0eebe4dae 100644 --- a/IPython/html/tests/casperjs/test_cases/dashboard_nav.js +++ b/IPython/html/tests/casperjs/test_cases/dashboard_nav.js @@ -1,7 +1,4 @@ -casper.wait_for_list = function () { - casper.waitForSelector('.list_item'); - // casper.wait(500); -} + casper.get_list_items = function () { return this.evaluate(function () { @@ -14,7 +11,7 @@ casper.get_list_items = function () { }); } -casper.test_items = function (test, baseUrl) { +casper.test_items = function (baseUrl) { casper.then(function () { var items = casper.get_list_items(); casper.each(items, function (self, item) { @@ -22,9 +19,9 @@ casper.test_items = function (test, baseUrl) { var followed_url = baseUrl+item.link; if (!followed_url.match('/\.\.$')) { casper.thenOpen(baseUrl+item.link, function () { - casper.wait_for_list(); - test.assertEquals(this.getCurrentUrl(), followed_url, 'Testing dashboard link: '+followed_url); - casper.test_items(test, baseUrl); + casper.wait_for_dashboard(); + this.test.assertEquals(this.getCurrentUrl(), followed_url, 'Testing dashboard link: '+followed_url); + casper.test_items(baseUrl); this.back(); }); } @@ -33,13 +30,8 @@ casper.test_items = function (test, baseUrl) { }); } -casper.test.begin('Testing dashboard navigation', function (test) { - var baseUrl = casper.get_notebook_server(); - casper.start(baseUrl); - casper.echo(baseUrl); - casper.wait_for_list(); - casper.test_items(test, baseUrl); - casper.run(function() { - test.done(); - }); -}); +casper.dashboard_test(function () { + baseUrl = this.get_notebook_server() + casper.test_items(baseUrl); +}) + diff --git a/IPython/html/tests/casperjs/util.js b/IPython/html/tests/casperjs/util.js index 44db968ca..8c49dd240 100644 --- a/IPython/html/tests/casperjs/util.js +++ b/IPython/html/tests/casperjs/util.js @@ -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()); From 378b18b2fb072daa072013cc7c29548caf341e92 Mon Sep 17 00:00:00 2001 From: "Brian E. Granger" Date: Tue, 4 Feb 2014 16:29:55 -0800 Subject: [PATCH 14/24] Nice dashboard page titles like /.../examples/notebooks/ --- IPython/html/templates/tree.html | 2 +- IPython/html/tree/handlers.py | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/IPython/html/templates/tree.html b/IPython/html/templates/tree.html index 652ed13c3..d05020035 100644 --- a/IPython/html/templates/tree.html +++ b/IPython/html/templates/tree.html @@ -1,6 +1,6 @@ {% extends "page.html" %} -{% block title %}IPython Dashboard{% endblock %} +{% block title %}{{page_title}}{% endblock %} {% block stylesheet %} diff --git a/IPython/html/tree/handlers.py b/IPython/html/tree/handlers.py index 98241dea5..e0979c41f 100644 --- a/IPython/html/tree/handlers.py +++ b/IPython/html/tree/handlers.py @@ -39,6 +39,18 @@ class TreeHandler(IPythonHandler): 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) + comps.insert(0, '...') + page_title = url_escape(url_path_join(*comps)) + if page_title: + return '/'+page_title+'/' + else: + return '/' + @web.authenticated def get(self, path='', name=None): path = path.strip('/') @@ -55,9 +67,10 @@ class TreeHandler(IPythonHandler): # no such directory, 404 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 )) From 157f5f64a5dfe5ad7488942aa1d9ec3352e605dc Mon Sep 17 00:00:00 2001 From: "Brian E. Granger" Date: Wed, 5 Feb 2014 09:58:52 -0800 Subject: [PATCH 15/24] Another variation of the dashboard page title. --- IPython/html/tree/handlers.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/IPython/html/tree/handlers.py b/IPython/html/tree/handlers.py index e0979c41f..6be48ed9f 100644 --- a/IPython/html/tree/handlers.py +++ b/IPython/html/tree/handlers.py @@ -44,12 +44,11 @@ class TreeHandler(IPythonHandler): if len(comps) > 3: for i in range(len(comps)-2): comps.pop(0) - comps.insert(0, '...') page_title = url_escape(url_path_join(*comps)) if page_title: - return '/'+page_title+'/' + return page_title+'/' else: - return '/' + return 'Home' @web.authenticated def get(self, path='', name=None): From 977b164d8e63333df35b68221737a59489ba8aef Mon Sep 17 00:00:00 2001 From: "Brian E. Granger" Date: Wed, 5 Feb 2014 11:18:09 -0800 Subject: [PATCH 16/24] Cleaning up JS tests controller. --- IPython/testing/iptestcontroller.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/IPython/testing/iptestcontroller.py b/IPython/testing/iptestcontroller.py index 9e02c9ae7..6e5243604 100644 --- a/IPython/testing/iptestcontroller.py +++ b/IPython/testing/iptestcontroller.py @@ -167,13 +167,12 @@ class JSController(TestController): self.section = section self.ipydir = TemporaryDirectory() - self.nbdir = os.path.join(self.ipydir.name, 'notebooks') - print("Running notebook tests in directory: %r" % self.nbdir) - os.makedirs(os.path.join(self.nbdir, 'subdir1/subdir1a')) - os.makedirs(os.path.join(self.nbdir, 'subdir2/subdir2a')) - # 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 @@ -195,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.nbdir)) + self.server = Process(target=run_webapp, args=(q, self.ipydir.name, self.nbdir.name)) self.server.start() self.server_port = q.get() @@ -212,12 +211,11 @@ def run_webapp(q, ipydir, nbdir, loglevel=0): import IPython.html.notebookapp as nbapp import sys sys.stderr = open(os.devnull, 'w') - os.environ["IPYTHONDIR"] = ipydir server = nbapp.NotebookApp() args = ['--no-browser'] - args.append('--notebook-dir='+nbdir) - args.append('--profile-dir='+ipydir) - 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) From 7e1a179a7c08b14c53a9b974e8f442ae7d2685f2 Mon Sep 17 00:00:00 2001 From: "Brian E. Granger" Date: Wed, 5 Feb 2014 12:01:11 -0800 Subject: [PATCH 17/24] Addressing review comments. * list_dirs and get_dir_model added to base NBM class. * Calling of list_dirs moved to handler. * type=notebook added to notebook model. --- .../html/services/notebooks/filenbmanager.py | 20 +++++++++---------- IPython/html/services/notebooks/handlers.py | 14 +++++++++++-- IPython/html/services/notebooks/nbmanager.py | 20 +++++++++++++++++++ .../notebooks/tests/test_notebooks_api.py | 2 +- 4 files changed, 42 insertions(+), 14 deletions(-) diff --git a/IPython/html/services/notebooks/filenbmanager.py b/IPython/html/services/notebooks/filenbmanager.py index fd4d70ebc..28aeb99a0 100644 --- a/IPython/html/services/notebooks/filenbmanager.py +++ b/IPython/html/services/notebooks/filenbmanager.py @@ -159,12 +159,17 @@ class FileNotebookManager(NotebookManager): """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): + raise web.HTTPError(404, u'diretory 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 name.startswith('.'): - model = self.get_dir_model(name, path) + try: + model = self.get_dir_model(name, path) + except IOError: + pass dirs.append(model) dirs = sorted(dirs, key=lambda item: item['name']) return dirs @@ -206,16 +211,8 @@ class FileNotebookManager(NotebookManager): """ path = path.strip('/') notebook_names = self.get_notebook_names(path) - index = [] - notebooks = [] - for name in notebook_names: - model = self.get_notebook_model(name, path, content=False) - if name.lower() == 'index.ipynb': - index.append(model) - else: - 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']) - notebooks = index + self.list_dirs(path) + notebooks return notebooks def get_notebook_model(self, name, path='', content=True): @@ -248,6 +245,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: @@ -264,7 +262,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) diff --git a/IPython/html/services/notebooks/handlers.py b/IPython/html/services/notebooks/handlers.py index 27e74d76b..ac44dd659 100644 --- a/IPython/html/services/notebooks/handlers.py +++ b/IPython/html/services/notebooks/handlers.py @@ -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 diff --git a/IPython/html/services/notebooks/nbmanager.py b/IPython/html/services/notebooks/nbmanager.py index f86e914e3..2a9771c38 100644 --- a/IPython/html/services/notebooks/nbmanager.py +++ b/IPython/html/services/notebooks/nbmanager.py @@ -112,6 +112,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. diff --git a/IPython/html/services/notebooks/tests/test_notebooks_api.py b/IPython/html/services/notebooks/tests/test_notebooks_api.py index e9e17fa32..5ac0cb1f8 100644 --- a/IPython/html/services/notebooks/tests/test_notebooks_api.py +++ b/IPython/html/services/notebooks/tests/test_notebooks_api.py @@ -24,7 +24,7 @@ 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 'type' not in nb] + return [nb for nb in nb_list if nb['type']=='notebook'] class NBAPI(object): From e0a779322661557aea985960d185b10e9982f07d Mon Sep 17 00:00:00 2001 From: MinRK Date: Wed, 5 Feb 2014 12:25:55 -0800 Subject: [PATCH 18/24] don't strip '.ipynb' from notebook names in nblist The only place the extension is not wanted is editing a name during upload. Everywhere else, it's actually wanted. --- IPython/html/static/tree/js/notebooklist.js | 23 +++++++++++---------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/IPython/html/static/tree/js/notebooklist.js b/IPython/html/static/tree/js/notebooklist.js index 9ba4790f1..e92c34386 100644 --- a/IPython/html/static/tree/js/notebooklist.js +++ b/IPython/html/static/tree/js/notebooklist.js @@ -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); @@ -171,10 +170,10 @@ var IPython = (function (IPython) { var len = data.length; this.clear_list(); if (len === 0) { - var item = this.new_notebook_item(0); + item = this.new_notebook_item(0); var span12 = item.children().first(); span12.empty(); - span12.append($('
').text(message)) + span12.append($('
').text(message)); } var path = this.notebookPath(); var offset = 0; @@ -190,9 +189,8 @@ var IPython = (function (IPython) { this.add_dir(path, name, item); } else { var name = data[i].name; - var nbname = utils.splitext(name)[0]; item = this.new_notebook_item(i+offset); - this.add_link(path, nbname, item); + this.add_link(path, name, item); name = utils.url_path_join(path, name); if(this.sessions[name] === undefined){ this.add_delete_button(item); @@ -247,7 +245,7 @@ var IPython = (function (IPython) { NotebookList.prototype.add_link = function (path, nbname, item) { item.data('nbname', nbname); item.data('path', path); - item.find(".item_name").text(nbname + '.ipynb'); + item.find(".item_name").text(nbname); item.find(".item_icon").addClass('icon-book'); item.find("a.item_link") .attr('href', @@ -255,7 +253,7 @@ var IPython = (function (IPython) { this.baseProjectUrl(), "notebooks", path, - nbname + ".ipynb" + nbname ) ).attr('target','_blank'); }; @@ -267,7 +265,7 @@ var IPython = (function (IPython) { item.find(".item_name").empty().append( $('') .addClass("nbname_input") - .attr('value', nbname) + .attr('value', utils.splitext(nbname)[0]) .attr('size', '30') .attr('type', 'text') ); @@ -336,7 +334,7 @@ var IPython = (function (IPython) { notebooklist.baseProjectUrl(), 'api/notebooks', notebooklist.notebookPath(), - nbname + '.ipynb' + nbname ); $.ajax(url, settings); } @@ -356,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'; @@ -382,7 +383,7 @@ var IPython = (function (IPython) { that.baseProjectUrl(), 'api/notebooks', that.notebookPath(), - nbname + '.ipynb' + nbname ); $.ajax(url, settings); return false; From 243392d81004e82fec4a4c66c505040ce67b4f41 Mon Sep 17 00:00:00 2001 From: "Brian E. Granger" Date: Wed, 5 Feb 2014 13:09:55 -0800 Subject: [PATCH 19/24] Creating and testing IPython.html.utils.is_hidden. --- IPython/html/base/handlers.py | 29 ++-------------- .../html/services/notebooks/filenbmanager.py | 28 +++++++++++++--- IPython/html/services/notebooks/nbmanager.py | 19 ++++++++++- IPython/html/tests/test_utils.py | 16 ++++++++- IPython/html/tree/handlers.py | 6 ++-- IPython/html/utils.py | 33 +++++++++++++++++++ 6 files changed, 96 insertions(+), 35 deletions(-) diff --git a/IPython/html/base/handlers.py b/IPython/html/base/handlers.py index d9b36c3b0..0e3a93500 100644 --- a/IPython/html/base/handlers.py +++ b/IPython/html/base/handlers.py @@ -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) - 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)): + if is_hidden(abs_root, abs_path): 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 + return abs_path def json_errors(method): diff --git a/IPython/html/services/notebooks/filenbmanager.py b/IPython/html/services/notebooks/filenbmanager.py index 28aeb99a0..5e8bd35d3 100644 --- a/IPython/html/services/notebooks/filenbmanager.py +++ b/IPython/html/services/notebooks/filenbmanager.py @@ -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(self.notebook_dir, os_path) + def get_os_path(self, name=None, path=''): """Given a notebook name and a URL path, return its file system path. @@ -159,13 +179,13 @@ class FileNotebookManager(NotebookManager): """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): - raise web.HTTPError(404, u'diretory does not exist: %r' % os_path) + if not os.path.isdir(os_path) or is_hidden(self.notebook_dir, os_path): + 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 name.startswith('.'): + if os.path.isdir(os_path) and not is_hidden(self.notebook_dir, os_path): try: model = self.get_dir_model(name, path) except IOError: diff --git a/IPython/html/services/notebooks/nbmanager.py b/IPython/html/services/notebooks/nbmanager.py index 2a9771c38..80b3ce5a3 100644 --- a/IPython/html/services/notebooks/nbmanager.py +++ b/IPython/html/services/notebooks/nbmanager.py @@ -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): diff --git a/IPython/html/tests/test_utils.py b/IPython/html/tests/test_utils.py index 6ceff595a..a58d3728f 100644 --- a/IPython/html/tests/test_utils.py +++ b/IPython/html/tests/test_utils.py @@ -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,14 @@ 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(root, subdir1), False) + subdir2 = os.path.join(root, '.subdir2') + os.makedirs(subdir2) + nt.assert_equal(is_hidden(root, subdir2), True) + subdir34 = os.path.join(root, 'subdir3', '.subdir4') + os.makedirs(subdir34) + nt.assert_equal(is_hidden(root, subdir34), True) diff --git a/IPython/html/tree/handlers.py b/IPython/html/tree/handlers.py index 6be48ed9f..b958b9e13 100644 --- a/IPython/html/tree/handlers.py +++ b/IPython/html/tree/handlers.py @@ -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 @@ -62,8 +62,8 @@ 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) diff --git a/IPython/html/utils.py b/IPython/html/utils.py index 0ea9265d9..75d042500 100644 --- a/IPython/html/utils.py +++ b/IPython/html/utils.py @@ -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,28 @@ def url_unescape(path): for p in py3compat.unicode_to_str(path).split('/') ]) +def is_hidden(absolute_root, absolute_path): + """Is 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. + + Parameters + ---------- + absolute_root : unicode + absolute_path : unicode + """ + inside_root = absolute_path[len(absolute_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 = 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: + return True + path = os.path.dirname(path) + + return False + From 7ab69652eae6990e4f1224eeff10c980f1bdd992 Mon Sep 17 00:00:00 2001 From: "Brian E. Granger" Date: Wed, 5 Feb 2014 15:35:46 -0800 Subject: [PATCH 20/24] Breadcrumb home icon. --- IPython/html/static/style/ipython.min.css | 3 +++ IPython/html/static/style/style.min.css | 3 +++ IPython/html/static/tree/less/tree.less | 17 ++++++++++++++++- IPython/html/templates/tree.html | 2 +- 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/IPython/html/static/style/ipython.min.css b/IPython/html/static/style/ipython.min.css index f715120d3..7325b41b9 100644 --- a/IPython/html/static/style/ipython.min.css +++ b/IPython/html/static/style/ipython.min.css @@ -31,6 +31,9 @@ div.traceback-wrapper{text-align:left;max-width:800px;margin:auto} .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} diff --git a/IPython/html/static/style/style.min.css b/IPython/html/static/style/style.min.css index f55c0b48b..5023d746a 100644 --- a/IPython/html/static/style/style.min.css +++ b/IPython/html/static/style/style.min.css @@ -1308,6 +1308,9 @@ span#login_widget{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} diff --git a/IPython/html/static/tree/less/tree.less b/IPython/html/static/tree/less/tree.less index 074da6500..0e758b5a6 100644 --- a/IPython/html/static/tree/less/tree.less +++ b/IPython/html/static/tree/less/tree.less @@ -11,6 +11,7 @@ // 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; @@ -21,6 +22,20 @@ ul#tabs a { 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: @dashboard_tb_pad 0 @dashboard_tb_pad 0; } @@ -74,7 +89,7 @@ ul#tabs a { .item_icon { font-size: 14px; - color: darken(@border_color, 30%); + color: @dark_dashboard_color; margin-right: @dashboard_lr_pad; } diff --git a/IPython/html/templates/tree.html b/IPython/html/templates/tree.html index d05020035..9d678f229 100644 --- a/IPython/html/templates/tree.html +++ b/IPython/html/templates/tree.html @@ -51,7 +51,7 @@ data-base-kernel-url="{{base_kernel_url}}"