diff --git a/IPython/html/services/notebooks/filenbmanager.py b/IPython/html/services/notebooks/filenbmanager.py index c0f0d1905..d6b9e6c74 100644 --- a/IPython/html/services/notebooks/filenbmanager.py +++ b/IPython/html/services/notebooks/filenbmanager.py @@ -94,17 +94,16 @@ class FileNotebookManager(NotebookManager): def change_notebook(self, data, notebook_name, notebook_path='/'): """Changes notebook""" changes = data.keys() - response = 200 for change in changes: full_path = self.get_os_path(notebook_name, notebook_path) if change == "name": new_path = self.get_os_path(data['name'], notebook_path) - if not os.path.isfile(new_path): + try: os.rename(full_path, self.get_os_path(data['name'], notebook_path)) notebook_name = data['name'] - else: - response = 409 + except OSError as e: + raise web.HTTPError(409, u'Notebook name already exists.') if change == "path": new_path = self.get_os_path(data['name'], data['path']) stutil.move(full_path, new_path) @@ -112,7 +111,7 @@ class FileNotebookManager(NotebookManager): if change == "content": self.save_notebook(data, notebook_name, notebook_path) model = self.notebook_model(notebook_name, notebook_path) - return model, response + return model def notebook_exists(self, name, path): """Returns a True if the notebook exists. Else, returns False. @@ -131,31 +130,6 @@ class FileNotebookManager(NotebookManager): path = self.get_os_path(name, path) return os.path.isfile(path) - def get_os_path(self, fname, path='/'): - """Given a notebook name and a server URL path, return its file system - path. - - Parameters - ---------- - fname : string - The name of a notebook file with the .ipynb extension - path : string - The relative URL path (with '/' as separator) to the named - notebook. - - Returns - ------- - path : string - A file system path that combines notebook_dir (location where - server started), the relative path, and the filename with the - current operating system's url. - """ - parts = path.split('/') - parts = [p for p in parts if p != ''] # remove duplicate splits - parts += [fname] - path = os.path.join(self.notebook_dir, *parts) - return path - def read_notebook_object_from_path(self, path): """read a notebook object from a path""" info = os.stat(path) diff --git a/IPython/html/services/notebooks/handlers.py b/IPython/html/services/notebooks/handlers.py index 29fc4c2aa..2c4f86d17 100644 --- a/IPython/html/services/notebooks/handlers.py +++ b/IPython/html/services/notebooks/handlers.py @@ -36,7 +36,7 @@ class NotebookRootHandler(IPythonHandler): """get returns a list of notebooks from the location where the server was started.""" nbm = self.notebook_manager - notebooks = nbm.list_notebooks("") + notebooks = nbm.list_notebooks("/") self.finish(jsonapi.dumps(notebooks)) @web.authenticated @@ -53,18 +53,9 @@ class NotebookRootHandler(IPythonHandler): fname = nbm.new_notebook(notebook_path='/') self.set_header('Location', nbm.notebook_dir + fname) model = nbm.notebook_model(fname) - self.set_header('Location', '{0}api/notebooks/{1}'.format(self.base_project_url, notebook_name)) + self.set_header('Location', '{0}api/notebooks/{1}'.format(self.base_project_url, fname)) self.finish(jsonapi.dumps(model)) - -class NotebookRootRedirect(IPythonHandler): - - @web.authenticated - def get(self): - """get redirects to not include trailing backslash""" - self.redirect("/api/notebooks") - - class NotebookHandler(IPythonHandler): SUPPORTED_METHODS = ('GET', 'PUT', 'PATCH', 'POST','DELETE') @@ -86,7 +77,7 @@ class NotebookHandler(IPythonHandler): # get and return notebook representation format = self.get_argument('format', default='json') download = self.get_argument('download', default='False') - model = nbm.notebook_model(name,path) + model = nbm.notebook_model(name, path) last_mod, representation, name = nbm.get_notebook(name, path, format) self.set_header('Last-Modified', last_mod) @@ -109,8 +100,7 @@ class NotebookHandler(IPythonHandler): nbm = self.notebook_manager name, path = nbm.named_notebook_path(notebook_path) data = jsonapi.loads(self.request.body) - model, response = nbm.change_notebook(data, name, path) - self.set_status(response) + model = nbm.change_notebook(data, name, path) self.finish(jsonapi.dumps(model)) @web.authenticated @@ -217,7 +207,7 @@ default_handlers = [ ModifyNotebookCheckpointsHandler), (r"api/notebooks/%s/" % _notebook_path_regex, NotebookHandler), (r"api/notebooks/%s" % _notebook_path_regex, NotebookHandler), - (r"api/notebooks/", NotebookRootRedirect), + (r"api/notebooks/", NotebookRootHandler), (r"api/notebooks", NotebookRootHandler), ] diff --git a/IPython/html/services/notebooks/nbmanager.py b/IPython/html/services/notebooks/nbmanager.py index be33166bf..d610aad35 100644 --- a/IPython/html/services/notebooks/nbmanager.py +++ b/IPython/html/services/notebooks/nbmanager.py @@ -71,6 +71,32 @@ class NotebookManager(LoggingConfigurable): name = None path = "/".join(names) + '/' return name, path + + def get_os_path(self, fname=None, path='/'): + """Given a notebook name and a server URL path, return its file system + path. + + Parameters + ---------- + fname : string + The name of a notebook file with the .ipynb extension + path : string + The relative URL path (with '/' as separator) to the named + notebook. + + Returns + ------- + path : string + A file system path that combines notebook_dir (location where + server started), the relative path, and the filename with the + current operating system's url. + """ + parts = path.split('/') + parts = [p for p in parts if p != ''] # remove duplicate splits + if fname is not None: + parts += [fname] + path = os.path.join(self.notebook_dir, *parts) + return path def url_encode(self, path): """Returns the path with all special characters URL encoded""" @@ -135,7 +161,7 @@ class NotebookManager(LoggingConfigurable): model = {"name": notebook_name, "path": notebook_path, "last_modified (UTC)": last_modified.ctime()} - if content == True: + if content is True: model['content'] = contents return model @@ -196,7 +222,7 @@ class NotebookManager(LoggingConfigurable): nb.metadata.name = name self.write_notebook_object(nb, name, notebook_path, new_name) - def write_notebook_object(self, nb, notebook_name=None, notebook_path='/', new_name=None): + def write_notebook_object(self, nb, notebook_name='/', notebook_path='/', new_name=None): """Write a notebook object and return its notebook_name. If notebook_name is None, this method should create a new notebook_name. diff --git a/IPython/html/services/tests/test_services_api.py b/IPython/html/services/tests/test_services_api.py new file mode 100644 index 000000000..ddc1e405d --- /dev/null +++ b/IPython/html/services/tests/test_services_api.py @@ -0,0 +1,124 @@ +"""Test the all of the services API.""" + + +import os +import sys +import json +import urllib +from zmq.utils import jsonapi + +import requests + +from IPython.html.tests.launchnotebook import NotebookTestBase + +class APITest(NotebookTestBase): + """Test the kernels web service API""" + + def base_url(self): + return super(APITest,self).base_url() + + def notebooks_url(self): + return self.base_url() + 'api/notebooks' + + def kernels_url(self): + return self.base_url() + 'api/kernels' + + def sessions_url(self): + return self.base_url() + 'api/sessions' + + def contents_url(self): + return self.contents_url() + 'api/contents' + + def mknb(self, name='', path='/'): + url = self.notebooks_url() + path + return url, requests.post(url) + + def delnb(self, name, path='/'): + url = self.notebooks_url() + path + name + r = requests.delete(url) + return r.status_code + + def test_no_notebooks(self): + url = self.notebooks_url() + r = requests.get(url) + self.assertEqual(r.json(), []) + + def test_root_notebook_handler(self): + # POST a notebook and test the dict thats returned. + url, nb = self.mknb() + data = nb.json() + assert isinstance(data, dict) + assert data.has_key("name") + assert data.has_key("path") + self.assertEqual(data['name'], u'Untitled0.ipynb') + self.assertEqual(data['path'], u'/') + + # GET list of notebooks in directory. + r = requests.get(url) + assert isinstance(r.json(), list) + assert isinstance(r.json()[0], dict) + + # GET with a notebook name. + url = self.notebooks_url() + '/Untitled0.ipynb' + r = requests.get(url) + assert isinstance(data, dict) + self.assertEqual(r.json(), data) + + # PATCH (rename) request. + new_name = {'name':'test.ipynb'} + r = requests.patch(url, data=jsonapi.dumps(new_name)) + data = r.json() + assert isinstance(data, dict) + + # make sure the patch worked. + new_url = self.notebooks_url() + '/test.ipynb' + r = requests.get(new_url) + assert isinstance(r.json(), dict) + self.assertEqual(r.json(), data) + + # GET bad (old) notebook name. + r = requests.get(url) + self.assertEqual(r.status_code, 404) + + # POST notebooks to folders one and two levels down. + os.makedirs(os.path.join(self.notebook_dir.name, 'foo')) + os.makedirs(os.path.join(self.notebook_dir.name, 'foo','bar')) + url, nb = self.mknb(path='/foo/') + url2, nb2 = self.mknb(path='/foo/bar/') + data = nb.json() + data2 = nb2.json() + assert isinstance(data, dict) + assert isinstance(data2, dict) + assert data.has_key("name") + assert data.has_key("path") + self.assertEqual(data['name'], u'Untitled0.ipynb') + self.assertEqual(data['path'], u'/foo/') + assert data2.has_key("name") + assert data2.has_key("path") + self.assertEqual(data2['name'], u'Untitled0.ipynb') + self.assertEqual(data2['path'], u'/foo/bar/') + + # GET request on notebooks one and two levels down. + r = requests.get(url+'Untitled0.ipynb') + r2 = requests.get(url2+'Untitled0.ipynb') + assert isinstance(r.json(), dict) + self.assertEqual(r.json(), data) + assert isinstance(r2.json(), dict) + self.assertEqual(r2.json(), data2) + + # PATCH notebooks that are one and two levels down. + new_name = {'name': 'testfoo.ipynb'} + r = requests.patch(url+'Untitled0.ipynb', data=jsonapi.dumps(new_name)) + r = requests.get(url+'testfoo.ipynb') + data = r.json() + assert isinstance(data, dict) + assert data.has_key('name') + self.assertEqual(data['name'], 'testfoo.ipynb') + r = requests.get(url+'Untitled0.ipynb') + self.assertEqual(r.status_code, 404) + + # DELETE notebooks + r = self.delnb('testfoo.ipynb', '/foo/') + r2 = self.delnb('Untitled0.ipynb', '/foo/bar/') + self.assertEqual(r, 204) + self.assertEqual(r2, 204) \ No newline at end of file diff --git a/IPython/html/tests/launchnotebook.py b/IPython/html/tests/launchnotebook.py index a83cbc38c..d64929ca6 100644 --- a/IPython/html/tests/launchnotebook.py +++ b/IPython/html/tests/launchnotebook.py @@ -28,6 +28,7 @@ class NotebookTestBase(TestCase): '--ipython-dir=%s' % self.ipython_dir.name, '--notebook-dir=%s' % self.notebook_dir.name ] + #self.notebook = Popen(notebook_args) self.notebook = Popen(notebook_args, stdout=PIPE, stderr=PIPE) time.sleep(3.0)