diff --git a/IPython/html/notebook/handlers.py b/IPython/html/notebook/handlers.py index 42feea6..c3777ec 100644 --- a/IPython/html/notebook/handlers.py +++ b/IPython/html/notebook/handlers.py @@ -23,6 +23,7 @@ from zmq.utils import jsonapi from ..base.handlers import IPythonHandler +from ..services.notebooks.handlers import _notebook_path_regex, _path_regex from ..utils import url_path_join from urllib import quote @@ -51,32 +52,31 @@ class NotebookHandler(IPythonHandler): class NamedNotebookHandler(IPythonHandler): @web.authenticated - def get(self, notebook_path): + def get(self, path='', name=None): """get renders the notebook template if a name is given, or redirects to the '/files/' handler if the name is not given.""" nbm = self.notebook_manager - name, path = nbm.named_notebook_path(notebook_path) - if name is not None: - # a .ipynb filename was given - if not nbm.notebook_exists(name, path): - raise web.HTTPError(404, u'Notebook does not exist: %s' % name) - name = nbm.url_encode(name) - path = nbm.url_encode(path) - self.write(self.render_template('notebook.html', - project=self.project_dir, - notebook_path=path, - notebook_name=name, - kill_kernel=False, - mathjax_url=self.mathjax_url, - ) - ) - else: - url = "/files/" + notebook_path + if name is None: + url = url_path_join(self.base_project_url, 'files', path) self.redirect(url) + return + # a .ipynb filename was given + if not nbm.notebook_exists(name, path): + raise web.HTTPError(404, u'Notebook does not exist: %s/%s' % (path, name)) + name = nbm.url_encode(name) + path = nbm.url_encode(path) + self.write(self.render_template('notebook.html', + project=self.project_dir, + notebook_path=path, + notebook_name=name, + kill_kernel=False, + mathjax_url=self.mathjax_url, + ) + ) @web.authenticated - def post(self, notebook_path): + def post(self, path='', name=None): """post either creates a new notebook if no json data is sent to the server, or copies the data and returns a copied notebook in the location given by 'notebook_path.""" @@ -95,10 +95,9 @@ class NamedNotebookHandler(IPythonHandler): #----------------------------------------------------------------------------- -_notebook_path_regex = r"(?P.+)" - default_handlers = [ - (r"/notebooks/%s" % _notebook_path_regex, NamedNotebookHandler), - (r"/notebooks/", NotebookHandler), + (r"/notebooks/?%s" % _notebook_path_regex, NamedNotebookHandler), + (r"/notebooks/?%s" % _path_regex, NamedNotebookHandler), + (r"/notebooks/?", NotebookHandler), ] diff --git a/IPython/html/notebookapp.py b/IPython/html/notebookapp.py index 98a8a5a..51b52ed 100644 --- a/IPython/html/notebookapp.py +++ b/IPython/html/notebookapp.py @@ -734,10 +734,9 @@ class NotebookApp(BaseIPythonApplication): browser = None if self.file_to_run: - name, _ = os.path.splitext(os.path.basename(self.file_to_run)) - url = 'notebooks/' + self.entry_path + name + _ + url = url_path_join('notebooks', self.entry_path, self.file_to_run) else: - url = 'tree/' + self.entry_path + url = url_path_join('tree', self.entry_path) if browser: b = lambda : browser.open("%s://%s:%i%s%s" % (proto, ip, self.port, self.base_project_url, url), new=2) diff --git a/IPython/html/services/notebooks/filenbmanager.py b/IPython/html/services/notebooks/filenbmanager.py index b5693e1..b01c3a7 100644 --- a/IPython/html/services/notebooks/filenbmanager.py +++ b/IPython/html/services/notebooks/filenbmanager.py @@ -73,14 +73,14 @@ class FileNotebookManager(NotebookManager): except: raise TraitError("Couldn't create checkpoint dir %r" % new) - def get_notebook_names(self, path='/'): + def get_notebook_names(self, path=''): """List all notebook names in the notebook dir and path.""" names = glob.glob(self.get_os_path('*'+self.filename_ext, path)) names = [os.path.basename(name) for name in names] return names - def increment_filename(self, basename, path='/'): + def increment_filename(self, basename, path=''): """Return a non-used filename of the form basename.""" i = 0 while True: @@ -97,7 +97,7 @@ class FileNotebookManager(NotebookManager): if os.path.exists(path) is False: raise web.HTTPError(404, "No file or directory found.") - def notebook_exists(self, name, path='/'): + def notebook_exists(self, name, path=''): """Returns a True if the notebook exists. Else, returns False. Parameters @@ -111,8 +111,8 @@ class FileNotebookManager(NotebookManager): ------- bool """ - path = self.get_os_path(name, path='/') - return os.path.isfile(path) + nbpath = self.get_os_path(name, path=path) + return os.path.isfile(nbpath) def list_notebooks(self, path): """Returns a list of dictionaries that are the standard model @@ -137,7 +137,7 @@ class FileNotebookManager(NotebookManager): notebooks = sorted(notebooks, key=lambda item: item['name']) return notebooks - def get_notebook_model(self, name, path='/', content=True): + def get_notebook_model(self, name, path='', content=True): """ Takes a path and name for a notebook and returns it's model Parameters @@ -173,13 +173,13 @@ class FileNotebookManager(NotebookManager): model['content'] = nb return model - def save_notebook_model(self, model, name, path='/'): + def save_notebook_model(self, model, name, path=''): """Save the notebook model and return the model with no content.""" if 'content' not in model: raise web.HTTPError(400, u'No notebook JSON data provided') - new_path = model.get('path', path) + new_path = model.get('path', path).strip('/') new_name = model.get('name', name) if path != new_path or name != new_name: diff --git a/IPython/html/services/notebooks/handlers.py b/IPython/html/services/notebooks/handlers.py index 40ae7e3..f834d95 100644 --- a/IPython/html/services/notebooks/handlers.py +++ b/IPython/html/services/notebooks/handlers.py @@ -20,10 +20,10 @@ import json from tornado import web -from ...utils import url_path_join +from IPython.html.utils import url_path_join from IPython.utils.jsonutil import date_default -from ...base.handlers import IPythonHandler, json_errors +from IPython.html.base.handlers import IPythonHandler, json_errors #----------------------------------------------------------------------------- # Notebook web service handlers @@ -34,31 +34,32 @@ class NotebookHandler(IPythonHandler): SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE') - def notebook_location(self, name, path): + def notebook_location(self, name, path=''): """Return the full URL location of a notebook based. Parameters ---------- name : unicode - The name of the notebook like "foo.ipynb". + The base name of the notebook, such as "foo.ipynb". path : unicode The URL path of the notebook. """ - return url_path_join(self.base_project_url, u'/api/notebooks', path, name) + return url_path_join(self.base_project_url, 'api', 'notebooks', path, name) @web.authenticated @json_errors - def get(self, notebook_path): - """get checks if a notebook is not named, an returns a list of notebooks + def get(self, path='', name=None): + """ + GET with path and no notebook lists notebooks in a directory + GET with path and notebook name + + GET get checks if a notebook is not named, an returns a list of notebooks in the notebook path given. If a name is given, return the notebook representation""" nbm = self.notebook_manager - # path will have leading and trailing slashes, such as '/foo/bar/' - name, path = nbm.named_notebook_path(notebook_path) - # Check to see if a notebook name was given if name is None: - # List notebooks in 'notebook_path' + # List notebooks in 'path' notebooks = nbm.list_notebooks(path) self.finish(json.dumps(notebooks, default=date_default)) else: @@ -68,13 +69,11 @@ class NotebookHandler(IPythonHandler): self.finish(json.dumps(model, default=date_default)) @web.authenticated - # @json_errors - def patch(self, notebook_path): + @json_errors + def patch(self, path='', name=None): """patch is currently used strictly for notebook renaming. Changes the notebook name to the name given in data.""" nbm = self.notebook_manager - # path will have leading and trailing slashes, such as '/foo/bar/' - name, path = nbm.named_notebook_path(notebook_path) if name is None: raise web.HTTPError(400, u'Notebook name missing') model = self.get_json_body() @@ -90,11 +89,9 @@ class NotebookHandler(IPythonHandler): @web.authenticated @json_errors - def post(self, notebook_path): + def post(self, path='', name=None): """Create a new notebook in the location given by 'notebook_path'.""" nbm = self.notebook_manager - # path will have leading and trailing slashes, such as '/foo/bar/' - name, path = nbm.named_notebook_path(notebook_path) model = self.get_json_body() if name is not None: raise web.HTTPError(400, 'No name can be provided when POSTing a new notebook.') @@ -108,11 +105,9 @@ class NotebookHandler(IPythonHandler): @web.authenticated @json_errors - def put(self, notebook_path): + def put(self, path='', name=None): """saves the notebook in the location given by 'notebook_path'.""" nbm = self.notebook_manager - # path will have leading and trailing slashes, such as '/foo/bar/' - name, path = nbm.named_notebook_path(notebook_path) model = self.get_json_body() if model is None: raise web.HTTPError(400, u'JSON body missing') @@ -121,11 +116,9 @@ class NotebookHandler(IPythonHandler): @web.authenticated @json_errors - def delete(self, notebook_path): + def delete(self, path='', name=None): """delete the notebook in the given notebook path""" nbm = self.notebook_manager - # path will have leading and trailing slashes, such as '/foo/bar/' - name, path = nbm.named_notebook_path(notebook_path) nbm.delete_notebook_model(name, path) self.set_status(204) self.finish() @@ -137,26 +130,22 @@ class NotebookCheckpointsHandler(IPythonHandler): @web.authenticated @json_errors - def get(self, notebook_path): + def get(self, path='', name=None): """get lists checkpoints for a notebook""" nbm = self.notebook_manager - # path will have leading and trailing slashes, such as '/foo/bar/' - name, path = nbm.named_notebook_path(notebook_path) checkpoints = nbm.list_checkpoints(name, path) data = json.dumps(checkpoints, default=date_default) self.finish(data) @web.authenticated @json_errors - def post(self, notebook_path): + def post(self, path='', name=None): """post creates a new checkpoint""" nbm = self.notebook_manager - name, path = nbm.named_notebook_path(notebook_path) - # path will have leading and trailing slashes, such as '/foo/bar/' checkpoint = nbm.create_checkpoint(name, path) data = json.dumps(checkpoint, default=date_default) location = url_path_join(self.base_project_url, u'/api/notebooks', - path, name, '/checkpoints', checkpoint[u'checkpoint_id']) + path, name, 'checkpoints', checkpoint[u'checkpoint_id']) self.set_header(u'Location', location) self.finish(data) @@ -167,22 +156,18 @@ class ModifyNotebookCheckpointsHandler(IPythonHandler): @web.authenticated @json_errors - def post(self, notebook_path, checkpoint_id): + def post(self, path, name, checkpoint_id): """post restores a notebook from a checkpoint""" nbm = self.notebook_manager - # path will have leading and trailing slashes, such as '/foo/bar/' - name, path = nbm.named_notebook_path(notebook_path) nbm.restore_checkpoint(checkpoint_id, name, path) self.set_status(204) self.finish() @web.authenticated @json_errors - def delete(self, notebook_path, checkpoint_id): + def delete(self, path, name, checkpoint_id): """delete clears a checkpoint for a given notebook""" nbm = self.notebook_manager - # path will have leading and trailing slashes, such as '/foo/bar/' - name, path = nbm.named_notebook_path(notebook_path) nbm.delete_checkpoint(checkpoint_id, name, path) self.set_status(204) self.finish() @@ -192,14 +177,17 @@ class ModifyNotebookCheckpointsHandler(IPythonHandler): #----------------------------------------------------------------------------- -_notebook_path_regex = r"(?P.*)" +_path_regex = r"(?P.*)" _checkpoint_id_regex = r"(?P[\w-]+)" +_notebook_name_regex = r"(?P[^/]+\.ipynb)" +_notebook_path_regex = "%s/%s" % (_path_regex, _notebook_name_regex) default_handlers = [ - (r"/api/notebooks/%s/checkpoints" % _notebook_path_regex, NotebookCheckpointsHandler), - (r"/api/notebooks/%s/checkpoints/%s" % (_notebook_path_regex, _checkpoint_id_regex), + (r"/api/notebooks/?%s/checkpoints" % _notebook_path_regex, NotebookCheckpointsHandler), + (r"/api/notebooks/?%s/checkpoints/%s" % (_notebook_path_regex, _checkpoint_id_regex), ModifyNotebookCheckpointsHandler), - (r"/api/notebooks%s" % _notebook_path_regex, NotebookHandler), + (r"/api/notebooks/?%s" % _notebook_path_regex, NotebookHandler), + (r"/api/notebooks/?%s/?" % _path_regex, NotebookHandler), ] diff --git a/IPython/html/services/notebooks/nbmanager.py b/IPython/html/services/notebooks/nbmanager.py index ad5a7b1..c17f420 100644 --- a/IPython/html/services/notebooks/nbmanager.py +++ b/IPython/html/services/notebooks/nbmanager.py @@ -45,45 +45,33 @@ class NotebookManager(LoggingConfigurable): """) filename_ext = Unicode(u'.ipynb') - - def named_notebook_path(self, notebook_path): - """Given notebook_path (*always* a URL path to notebook), returns a - (name, path) tuple, where name is a .ipynb file, and path is the - URL path that describes the file system path for the file. - It *always* starts *and* ends with a '/' character. - + + def path_exists(self, path): + """Does the API-style path (directory) actually exist? + + Override this method for non-filesystem-based notebooks. + Parameters ---------- - notebook_path : string - A path that may be a .ipynb name or a directory - + path : string + The + Returns ------- - name : string or None - the filename of the notebook, or None if not a .ipynb extension - path : string - the path to the directory which contains the notebook + exists : bool + Whether the path does indeed exist. """ - names = notebook_path.split('/') - names = [n for n in names if n != ''] # remove duplicate splits - - names = [''] + names - - if names and names[-1].endswith(".ipynb"): - name = names[-1] - path = "/".join(names[:-1]) + '/' - else: - name = None - path = "/".join(names) + '/' - return name, path + os_path = self.get_os_path(name, path) + return os.path.exists(os_path) - def get_os_path(self, fname=None, path='/'): + + def get_os_path(self, name=None, path=''): """Given a notebook name and a URL path, return its file system path. Parameters ---------- - fname : string + name : string The name of a notebook file with the .ipynb extension path : string The relative URL path (with '/' as separator) to the named @@ -96,10 +84,10 @@ class NotebookManager(LoggingConfigurable): server started), the relative path, and the filename with the current operating system's url. """ - parts = path.split('/') + parts = path.strip('/').split('/') parts = [p for p in parts if p != ''] # remove duplicate splits - if fname is not None: - parts += [fname] + if name is not None: + parts.append(name) path = os.path.join(self.notebook_dir, *parts) return path @@ -132,7 +120,7 @@ class NotebookManager(LoggingConfigurable): # Main notebook API - def increment_filename(self, basename, path='/'): + def increment_filename(self, basename, path=''): """Increment a notebook filename without the .ipynb to make it unique. Parameters @@ -157,15 +145,15 @@ class NotebookManager(LoggingConfigurable): """ raise NotImplementedError('must be implemented in a subclass') - def get_notebook_model(self, name, path='/', content=True): + def get_notebook_model(self, name, path='', content=True): """Get the notebook model with or without content.""" raise NotImplementedError('must be implemented in a subclass') - def save_notebook_model(self, model, name, path='/'): + def save_notebook_model(self, model, name, path=''): """Save the notebook model and return the model with no content.""" raise NotImplementedError('must be implemented in a subclass') - def update_notebook_model(self, model, name, path='/'): + def update_notebook_model(self, model, name, path=''): """Update the notebook model and return the model with no content.""" raise NotImplementedError('must be implemented in a subclass') @@ -173,7 +161,7 @@ class NotebookManager(LoggingConfigurable): """Delete notebook by name and path.""" raise NotImplementedError('must be implemented in a subclass') - def create_notebook_model(self, model=None, path='/'): + def create_notebook_model(self, model=None, path=''): """Create a new untitled notebook and return its model with no content.""" name = self.increment_filename('Untitled', path) if model is None: diff --git a/IPython/html/services/notebooks/tests/test_nbmanager.py b/IPython/html/services/notebooks/tests/test_nbmanager.py index 9ae9f0a..cf65fbf 100644 --- a/IPython/html/services/notebooks/tests/test_nbmanager.py +++ b/IPython/html/services/notebooks/tests/test_nbmanager.py @@ -67,48 +67,6 @@ class TestNotebookManager(TestCase): except OSError: print "Directory already exists." - def test_named_notebook_path(self): - """the `named_notebook_path` method takes a URL path to - a notebook and returns a url path split into nb and path""" - nm = NotebookManager() - - # doesn't end with ipynb, should just be path - name, path = nm.named_notebook_path('hello') - self.assertEqual(name, None) - self.assertEqual(path, '/hello/') - - # Root path returns just the root slash - name, path = nm.named_notebook_path('/') - self.assertEqual(name, None) - self.assertEqual(path, '/') - - # get notebook, and return the path as '/' - name, path = nm.named_notebook_path('notebook.ipynb') - self.assertEqual(name, 'notebook.ipynb') - self.assertEqual(path, '/') - - # Test a notebook name with leading slash returns - # the same as above - name, path = nm.named_notebook_path('/notebook.ipynb') - self.assertEqual(name, 'notebook.ipynb') - self.assertEqual(path, '/') - - # Multiple path arguments splits the notebook name - # and returns path with leading and trailing '/' - name, path = nm.named_notebook_path('/this/is/a/path/notebook.ipynb') - self.assertEqual(name, 'notebook.ipynb') - self.assertEqual(path, '/this/is/a/path/') - - # path without leading slash is returned with leading slash - name, path = nm.named_notebook_path('path/without/leading/slash/notebook.ipynb') - self.assertEqual(name, 'notebook.ipynb') - self.assertEqual(path, '/path/without/leading/slash/') - - # path with spaces and no leading or trailing '/' - name, path = nm.named_notebook_path('foo / bar% path& to# @/ notebook name.ipynb') - self.assertEqual(name, ' notebook name.ipynb') - self.assertEqual(path, '/foo / bar% path& to# @/') - def test_url_encode(self): nm = NotebookManager() diff --git a/IPython/html/tree/handlers.py b/IPython/html/tree/handlers.py index b7d7614..01b23cd 100644 --- a/IPython/html/tree/handlers.py +++ b/IPython/html/tree/handlers.py @@ -20,6 +20,7 @@ import os from tornado import web from ..base.handlers import IPythonHandler from ..utils import url_path_join, path2url, url2path +from ..services.notebooks.handlers import _notebook_path_regex, _path_regex #----------------------------------------------------------------------------- # Handlers @@ -30,23 +31,19 @@ class TreeHandler(IPythonHandler): """Render the tree view, listing notebooks, clusters, etc.""" @web.authenticated - def get(self, notebook_path=""): + def get(self, path='', name=None): nbm = self.notebook_manager - name, path = nbm.named_notebook_path(notebook_path) if name is not None: # is a notebook, redirect to notebook handler url = url_path_join(self.base_project_url, 'notebooks', path, name) self.redirect(url) else: - location = nbm.get_os_path(path=path) - - if not os.path.exists(location): + if not nbm.path_exists(path=path): # no such directory, 404 raise web.HTTPError(404) - self.write(self.render_template('tree.html', project=self.project_dir, - tree_url_path=path2url(location), + tree_url_path=path, notebook_path=path, )) @@ -55,8 +52,8 @@ class TreeRedirectHandler(IPythonHandler): """Redirect a request to the corresponding tree URL""" @web.authenticated - def get(self, notebook_path=''): - url = url_path_join(self.base_project_url, 'tree', notebook_path) + def get(self, path=''): + url = url_path_join(self.base_project_url, 'tree', path).rstrip('/') self.log.debug("Redirecting %s to %s", self.request.uri, url) self.redirect(url) @@ -66,11 +63,10 @@ class TreeRedirectHandler(IPythonHandler): #----------------------------------------------------------------------------- -_notebook_path_regex = r"(?P.+)" - default_handlers = [ - (r"/tree/%s/" % _notebook_path_regex, TreeRedirectHandler), - (r"/tree/%s" % _notebook_path_regex, TreeHandler), + (r"/tree/(.*)/", TreeRedirectHandler), + (r"/tree/?%s" % _notebook_path_regex, TreeHandler), + (r"/tree/?%s" % _path_regex, TreeHandler), (r"/tree/", TreeRedirectHandler), (r"/tree", TreeHandler), (r"/", TreeRedirectHandler),