From e918eb57cc45b49ec88080fc1437f8cf8ce3dacf Mon Sep 17 00:00:00 2001 From: MinRK Date: Fri, 30 May 2014 12:39:11 -0700 Subject: [PATCH 01/16] mv services/notebooks services/contents --- .../{notebooks => contents}/__init__.py | 0 .../{notebooks => contents}/filenbmanager.py | 70 +++++++++---------- .../{notebooks => contents}/handlers.py | 53 +++++++------- .../{notebooks => contents}/nbmanager.py | 48 ++++++------- .../{notebooks => contents}/tests/__init__.py | 0 .../tests/test_nbmanager.py | 52 +++++++------- .../tests/test_notebooks_api.py | 3 +- 7 files changed, 112 insertions(+), 114 deletions(-) rename IPython/html/services/{notebooks => contents}/__init__.py (100%) rename IPython/html/services/{notebooks => contents}/filenbmanager.py (98%) rename IPython/html/services/{notebooks => contents}/handlers.py (98%) rename IPython/html/services/{notebooks => contents}/nbmanager.py (98%) rename IPython/html/services/{notebooks => contents}/tests/__init__.py (100%) rename IPython/html/services/{notebooks => contents}/tests/test_nbmanager.py (98%) rename IPython/html/services/{notebooks => contents}/tests/test_notebooks_api.py (99%) diff --git a/IPython/html/services/notebooks/__init__.py b/IPython/html/services/contents/__init__.py similarity index 100% rename from IPython/html/services/notebooks/__init__.py rename to IPython/html/services/contents/__init__.py diff --git a/IPython/html/services/notebooks/filenbmanager.py b/IPython/html/services/contents/filenbmanager.py similarity index 98% rename from IPython/html/services/notebooks/filenbmanager.py rename to IPython/html/services/contents/filenbmanager.py index b9bd38921cb..118f02dbdf6 100644 --- a/IPython/html/services/notebooks/filenbmanager.py +++ b/IPython/html/services/contents/filenbmanager.py @@ -27,10 +27,10 @@ def sort_key(item): #----------------------------------------------------------------------------- class FileNotebookManager(NotebookManager): - + save_script = Bool(False, config=True, help="""Automatically create a Python script when saving the notebook. - + For easier use of import, %run and %load across notebooks, a .py script will be created next to any .ipynb on each save. This can also be set with the @@ -38,7 +38,7 @@ class FileNotebookManager(NotebookManager): """ ) notebook_dir = Unicode(getcwd(), config=True) - + def _notebook_dir_changed(self, name, old, new): """Do a bit of validation of the notebook dir.""" if not os.path.isabs(new): @@ -47,19 +47,19 @@ def _notebook_dir_changed(self, name, old, new): return if not os.path.exists(new) or not os.path.isdir(new): raise TraitError("notebook dir %r is not a directory" % new) - + checkpoint_dir = Unicode('.ipynb_checkpoints', config=True, help="""The directory name in which to keep notebook checkpoints - + This is a path relative to the notebook's own directory. - + By default, it is .ipynb_checkpoints """ ) - + def _copy(self, src, dest): """copy src to dest - + like shutil.copy2, but log errors in copystat """ shutil.copyfile(src, dest) @@ -67,7 +67,7 @@ def _copy(self, src, dest): shutil.copystat(src, dest) except OSError as e: self.log.debug("copystat on %s failed", dest, exc_info=True) - + def get_notebook_names(self, path=''): """List all notebook names in the notebook dir and path.""" path = path.strip('/') @@ -80,13 +80,13 @@ def get_notebook_names(self, path=''): def path_exists(self, path): """Does the API-style path (directory) actually exist? - + Parameters ---------- path : string The path to check. This is an API path (`/` separated, relative to base notebook-dir). - + Returns ------- exists : bool @@ -98,18 +98,18 @@ def path_exists(self, 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) @@ -204,13 +204,13 @@ def get_dir_model(self, name, path=''): def list_notebooks(self, path): """Returns a list of dictionaries that are the standard model for all notebooks in the relative 'path'. - + Parameters ---------- path : str the URL path that describes the relative path for the listed notebooks - + Returns ------- notebooks : list of dicts @@ -225,7 +225,7 @@ def list_notebooks(self, path): def get_notebook(self, name, path='', content=True): """ Takes a path and name for a notebook and returns its model - + Parameters ---------- name : str @@ -233,11 +233,11 @@ def get_notebook(self, name, path='', content=True): path : str the URL path that describes the relative path for the notebook - + Returns ------- model : dict - the notebook model. If contents=True, returns the 'contents' + the notebook model. If contents=True, returns the 'contents' dict in the model as well. """ path = path.strip('/') @@ -284,9 +284,9 @@ def save_notebook(self, model, name='', path=''): # Save the notebook file os_path = self._get_os_path(new_name, new_path) nb = current.to_notebook_json(model['content']) - + self.check_and_sign(nb, new_name, new_path) - + if 'name' in nb['metadata']: nb['metadata']['name'] = u'' try: @@ -325,7 +325,7 @@ def delete_notebook(self, name, path=''): os_path = self._get_os_path(name, path) if not os.path.isfile(os_path): raise web.HTTPError(404, u'Notebook does not exist: %s' % os_path) - + # clear checkpoints for checkpoint in self.list_checkpoints(name, path): checkpoint_id = checkpoint['id'] @@ -333,7 +333,7 @@ def delete_notebook(self, name, path=''): if os.path.isfile(cp_path): self.log.debug("Unlinking checkpoint %s", cp_path) os.unlink(cp_path) - + self.log.debug("Unlinking notebook %s", os_path) os.unlink(os_path) @@ -343,7 +343,7 @@ def rename_notebook(self, old_name, old_path, new_name, new_path): new_path = new_path.strip('/') if new_name == old_name and new_path == old_path: return - + new_os_path = self._get_os_path(new_name, new_path) old_os_path = self._get_os_path(old_name, old_path) @@ -375,9 +375,9 @@ def rename_notebook(self, old_name, old_path, new_name, new_path): # Move the .py script if self.save_script: shutil.move(old_py_path, new_py_path) - + # Checkpoint-related utilities - + def get_checkpoint_path(self, checkpoint_id, name, path=''): """find the path to a checkpoint""" path = path.strip('/') @@ -404,9 +404,9 @@ def get_checkpoint_model(self, checkpoint_id, name, path=''): last_modified = last_modified, ) return info - + # public checkpoint API - + def create_checkpoint(self, name, path=''): """Create a checkpoint from the current state of a notebook""" path = path.strip('/') @@ -416,13 +416,13 @@ def create_checkpoint(self, name, path=''): cp_path = self.get_checkpoint_path(checkpoint_id, name, path) self.log.debug("creating checkpoint for notebook %s", name) self._copy(nb_path, cp_path) - + # return the checkpoint info return self.get_checkpoint_model(checkpoint_id, name, path) - + def list_checkpoints(self, name, path=''): """list the checkpoints for a given notebook - + This notebook manager currently only supports one checkpoint per notebook. """ path = path.strip('/') @@ -432,8 +432,8 @@ def list_checkpoints(self, name, path=''): return [] else: return [self.get_checkpoint_model(checkpoint_id, name, path)] - - + + def restore_checkpoint(self, checkpoint_id, name, path=''): """restore a notebook to a checkpointed state""" path = path.strip('/') @@ -450,7 +450,7 @@ def restore_checkpoint(self, checkpoint_id, name, path=''): current.read(f, u'json') self._copy(cp_path, nb_path) self.log.debug("copying %s -> %s", cp_path, nb_path) - + def delete_checkpoint(self, checkpoint_id, name, path=''): """delete a notebook's checkpoint""" path = path.strip('/') @@ -461,7 +461,7 @@ def delete_checkpoint(self, checkpoint_id, name, path=''): ) self.log.debug("unlinking %s", cp_path) os.unlink(cp_path) - + def info_string(self): return "Serving notebooks from local directory: %s" % self.notebook_dir diff --git a/IPython/html/services/notebooks/handlers.py b/IPython/html/services/contents/handlers.py similarity index 98% rename from IPython/html/services/notebooks/handlers.py rename to IPython/html/services/contents/handlers.py index dab6849f595..5647ce782c4 100644 --- a/IPython/html/services/notebooks/handlers.py +++ b/IPython/html/services/contents/handlers.py @@ -38,7 +38,7 @@ class NotebookHandler(IPythonHandler): def notebook_location(self, name, path=''): """Return the full URL location of a notebook based. - + Parameters ---------- name : unicode @@ -57,7 +57,7 @@ def _finish_model(self, model, location=True): self.set_header('Location', location) self.set_header('Last-Modified', model['last_modified']) self.finish(json.dumps(model, default=date_default)) - + @web.authenticated @json_errors def get(self, path='', name=None): @@ -99,10 +99,10 @@ def patch(self, path='', name=None): raise web.HTTPError(400, u'JSON body missing') model = nbm.update_notebook(model, name, path) self._finish_model(model) - + def _copy_notebook(self, copy_from, path, copy_to=None): """Copy a notebook in path, optionally specifying the new name. - + Only support copying within the same directory. """ self.log.info(u"Copying notebook from %s/%s to %s/%s", @@ -112,23 +112,23 @@ def _copy_notebook(self, copy_from, path, copy_to=None): model = self.notebook_manager.copy_notebook(copy_from, copy_to, path) self.set_status(201) self._finish_model(model) - + def _upload_notebook(self, model, path, name=None): """Upload a notebook - + If name specified, create it in path/name. """ self.log.info(u"Uploading notebook to %s/%s", path, name or '') if name: model['name'] = name - + model = self.notebook_manager.create_notebook(model, path) self.set_status(201) self._finish_model(model) - + def _create_empty_notebook(self, path, name=None): """Create an empty notebook in path - + If name specified, create it in path/name. """ self.log.info(u"Creating new notebook in %s/%s", path, name or '') @@ -138,7 +138,7 @@ def _create_empty_notebook(self, path, name=None): model = self.notebook_manager.create_notebook(model, path=path) self.set_status(201) self._finish_model(model) - + def _save_notebook(self, model, path, name): """Save an existing notebook.""" self.log.info(u"Saving notebook at %s/%s", path, name) @@ -149,26 +149,26 @@ def _save_notebook(self, model, path, name): else: location = False self._finish_model(model, location) - + @web.authenticated @json_errors def post(self, path='', name=None): """Create a new notebook in the specified path. - + POST creates new notebooks. The server always decides on the notebook name. - + POST /api/notebooks/path New untitled notebook in path. If content specified, upload a notebook, otherwise start empty. POST /api/notebooks/path?copy=OtherNotebook.ipynb New copy of OtherNotebook in path """ - + if name is not None: raise web.HTTPError(400, "Only POST to directories. Use PUT for full names.") - + model = self.get_json_body() - + if model is not None: copy_from = model.get('copy_from') if copy_from: @@ -184,10 +184,10 @@ def post(self, path='', name=None): @json_errors def put(self, path='', name=None): """Saves the notebook in the location specified by name and path. - + PUT is very similar to POST, but the requester specifies the name, whereas with POST, the server picks the name. - + PUT /api/notebooks/path/Name.ipynb Save notebook at ``path/Name.ipynb``. Notebook structure is specified in `content` key of JSON request body. If content is not specified, @@ -197,7 +197,7 @@ def put(self, path='', name=None): """ if name is None: raise web.HTTPError(400, "Only PUT to full names. Use POST for directories.") - + model = self.get_json_body() if model: copy_from = model.get('copy_from') @@ -223,9 +223,9 @@ def delete(self, path='', name=None): class NotebookCheckpointsHandler(IPythonHandler): - + SUPPORTED_METHODS = ('GET', 'POST') - + @web.authenticated @json_errors def get(self, path='', name=None): @@ -234,7 +234,7 @@ def get(self, path='', name=None): checkpoints = nbm.list_checkpoints(name, path) data = json.dumps(checkpoints, default=date_default) self.finish(data) - + @web.authenticated @json_errors def post(self, path='', name=None): @@ -250,9 +250,9 @@ def post(self, path='', name=None): class ModifyNotebookCheckpointsHandler(IPythonHandler): - + SUPPORTED_METHODS = ('POST', 'DELETE') - + @web.authenticated @json_errors def post(self, path, name, checkpoint_id): @@ -261,7 +261,7 @@ def post(self, path, name, checkpoint_id): nbm.restore_checkpoint(checkpoint_id, name, path) self.set_status(204) self.finish() - + @web.authenticated @json_errors def delete(self, path, name, checkpoint_id): @@ -270,7 +270,7 @@ def delete(self, path, name, checkpoint_id): nbm.delete_checkpoint(checkpoint_id, name, path) self.set_status(204) self.finish() - + #----------------------------------------------------------------------------- # URL to handler mappings #----------------------------------------------------------------------------- @@ -285,4 +285,3 @@ def delete(self, path, name, checkpoint_id): (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/contents/nbmanager.py similarity index 98% rename from IPython/html/services/notebooks/nbmanager.py rename to IPython/html/services/contents/nbmanager.py index d5b6907bb5f..5f8bd978fae 100644 --- a/IPython/html/services/notebooks/nbmanager.py +++ b/IPython/html/services/contents/nbmanager.py @@ -32,11 +32,11 @@ class NotebookManager(LoggingConfigurable): filename_ext = Unicode(u'.ipynb') - + notary = Instance(sign.NotebookNotary) def _notary_default(self): return sign.NotebookNotary(parent=self) - + hide_globs = List(Unicode, [u'__pycache__'], config=True, help=""" Glob patterns to hide in file and directory listings. """) @@ -46,14 +46,14 @@ def _notary_default(self): def path_exists(self, path): """Does the API-style path (directory) actually exist? - + Override this method in subclasses. - + Parameters ---------- path : string The path to check - + Returns ------- exists : bool @@ -63,18 +63,18 @@ def path_exists(self, 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. - + """ raise NotImplementedError @@ -104,7 +104,7 @@ def list_dirs(self, path): # 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 @@ -145,15 +145,15 @@ def delete_notebook(self, name, path=''): def create_checkpoint(self, name, path=''): """Create a checkpoint of the current state of a notebook - + Returns a checkpoint_id for the new checkpoint. """ raise NotImplementedError("must be implemented in a subclass") - + def list_checkpoints(self, name, path=''): """Return a list of checkpoints for a given notebook""" return [] - + def restore_checkpoint(self, checkpoint_id, name, path=''): """Restore a notebook from one of its checkpoints""" raise NotImplementedError("must be implemented in a subclass") @@ -161,7 +161,7 @@ def restore_checkpoint(self, checkpoint_id, name, path=''): def delete_checkpoint(self, checkpoint_id, name, path=''): """delete a checkpoint for a notebook""" raise NotImplementedError("must be implemented in a subclass") - + def info_string(self): return "Serving notebooks" @@ -174,7 +174,7 @@ def get_kernel_path(self, name, path='', model=None): def increment_filename(self, basename, path=''): """Increment a notebook filename without the .ipynb to make it unique. - + Parameters ---------- basename : unicode @@ -206,14 +206,14 @@ def create_notebook(self, model=None, path=''): model['content'] = current.new_notebook(metadata=metadata) if 'name' not in model: model['name'] = self.increment_filename('Untitled', path) - + model['path'] = path model = self.save_notebook(model, model['name'], model['path']) return model def copy_notebook(self, from_name, to_name=None, path=''): """Copy an existing notebook and return its new model. - + If to_name not specified, increment `from_name-Copy#.ipynb`. """ path = path.strip('/') @@ -224,13 +224,13 @@ def copy_notebook(self, from_name, to_name=None, path=''): model['name'] = to_name model = self.save_notebook(model, to_name, path) return model - + def log_info(self): self.log.info(self.info_string()) def trust_notebook(self, name, path=''): """Explicitly trust a notebook - + Parameters ---------- name : string @@ -243,12 +243,12 @@ def trust_notebook(self, name, path=''): self.log.warn("Trusting notebook %s/%s", path, name) self.notary.mark_cells(nb, True) self.save_notebook(model, name, path) - + def check_and_sign(self, nb, name, path=''): """Check for trusted cells, and sign the notebook. - + Called as a part of saving notebooks. - + Parameters ---------- nb : dict @@ -262,12 +262,12 @@ def check_and_sign(self, nb, name, path=''): self.notary.sign(nb) else: self.log.warn("Saving untrusted notebook %s/%s", path, name) - + def mark_trusted_cells(self, nb, name, path=''): """Mark cells as trusted if the notebook signature matches. - + Called as a part of loading notebooks. - + Parameters ---------- nb : dict diff --git a/IPython/html/services/notebooks/tests/__init__.py b/IPython/html/services/contents/tests/__init__.py similarity index 100% rename from IPython/html/services/notebooks/tests/__init__.py rename to IPython/html/services/contents/tests/__init__.py diff --git a/IPython/html/services/notebooks/tests/test_nbmanager.py b/IPython/html/services/contents/tests/test_nbmanager.py similarity index 98% rename from IPython/html/services/notebooks/tests/test_nbmanager.py rename to IPython/html/services/contents/tests/test_nbmanager.py index bc03a871c06..c4b85b9e5ea 100644 --- a/IPython/html/services/notebooks/tests/test_nbmanager.py +++ b/IPython/html/services/contents/tests/test_nbmanager.py @@ -55,7 +55,7 @@ def test_get_os_path(self): path = fm._get_os_path('test.ipynb', '////') fs_path = os.path.join(fm.notebook_dir, 'test.ipynb') self.assertEqual(path, fs_path) - + def test_checkpoint_subdir(self): subd = u'sub ∂ir' cp_name = 'test-cp.ipynb' @@ -68,10 +68,10 @@ def test_checkpoint_subdir(self): self.assertNotEqual(cp_dir, cp_subdir) self.assertEqual(cp_dir, os.path.join(nbdir, fm.checkpoint_dir, cp_name)) self.assertEqual(cp_subdir, os.path.join(nbdir, subd, fm.checkpoint_dir, cp_name)) - + class TestNotebookManager(TestCase): - + def setUp(self): self._temp_dir = TemporaryDirectory() self.td = self._temp_dir.name @@ -79,10 +79,10 @@ def setUp(self): notebook_dir=self.td, log=logging.getLogger() ) - + def tearDown(self): self._temp_dir.cleanup() - + def make_dir(self, abs_path, rel_path): """make subdirectory, rel_path is the relative path to that directory from the location where the server started""" @@ -91,27 +91,27 @@ def make_dir(self, abs_path, rel_path): os.makedirs(os_path) except OSError: print("Directory already exists: %r" % os_path) - + def add_code_cell(self, nb): output = current.new_output("display_data", output_javascript="alert('hi');") cell = current.new_code_cell("print('hi')", outputs=[output]) if not nb.worksheets: nb.worksheets.append(current.new_worksheet()) nb.worksheets[0].cells.append(cell) - + def new_notebook(self): nbm = self.notebook_manager model = nbm.create_notebook() name = model['name'] path = model['path'] - + full_model = nbm.get_notebook(name, path) nb = full_model['content'] self.add_code_cell(nb) - + nbm.save_notebook(full_model, name, path) return nb, name, path - + def test_create_notebook(self): nm = self.notebook_manager # Test in root directory @@ -158,7 +158,7 @@ def test_get_notebook(self): self.assertIn('content', model2) self.assertEqual(model2['name'], 'Untitled0.ipynb') self.assertEqual(model2['path'], sub_dir.strip('/')) - + def test_update_notebook(self): nm = self.notebook_manager # Create a notebook @@ -184,7 +184,7 @@ def test_update_notebook(self): model = nm.create_notebook(None, sub_dir) name = model['name'] path = model['path'] - + # Change the name in the model for rename model['name'] = 'test_in_sub.ipynb' model = nm.update_notebook(model, name, path) @@ -193,7 +193,7 @@ def test_update_notebook(self): self.assertIn('path', model) self.assertEqual(model['name'], 'test_in_sub.ipynb') self.assertEqual(model['path'], sub_dir.strip('/')) - + # Make sure the old name is gone self.assertRaises(HTTPError, nm.get_notebook, name, path) @@ -255,50 +255,50 @@ def test_delete_notebook(self): nm = self.notebook_manager # Create a notebook nb, name, path = self.new_notebook() - + # Delete the notebook nm.delete_notebook(name, path) - + # Check that a 'get' on the deleted notebook raises and error self.assertRaises(HTTPError, nm.get_notebook, name, path) - + def test_copy_notebook(self): nm = self.notebook_manager path = u'å b' name = u'nb √.ipynb' os.mkdir(os.path.join(nm.notebook_dir, path)) orig = nm.create_notebook({'name' : name}, path=path) - + # copy with unspecified name copy = nm.copy_notebook(name, path=path) self.assertEqual(copy['name'], orig['name'].replace('.ipynb', '-Copy0.ipynb')) - + # copy with specified name copy2 = nm.copy_notebook(name, u'copy 2.ipynb', path=path) self.assertEqual(copy2['name'], u'copy 2.ipynb') - + def test_trust_notebook(self): nbm = self.notebook_manager nb, name, path = self.new_notebook() - + untrusted = nbm.get_notebook(name, path)['content'] assert not nbm.notary.check_cells(untrusted) - + # print(untrusted) nbm.trust_notebook(name, path) trusted = nbm.get_notebook(name, path)['content'] # print(trusted) assert nbm.notary.check_cells(trusted) - + def test_mark_trusted_cells(self): nbm = self.notebook_manager nb, name, path = self.new_notebook() - + nbm.mark_trusted_cells(nb, name, path) for cell in nb.worksheets[0].cells: if cell.cell_type == 'code': assert not cell.trusted - + nbm.trust_notebook(name, path) nb = nbm.get_notebook(name, path)['content'] for cell in nb.worksheets[0].cells: @@ -308,11 +308,11 @@ def test_mark_trusted_cells(self): def test_check_and_sign(self): nbm = self.notebook_manager nb, name, path = self.new_notebook() - + nbm.mark_trusted_cells(nb, name, path) nbm.check_and_sign(nb, name, path) assert not nbm.notary.check_signature(nb) - + nbm.trust_notebook(name, path) nb = nbm.get_notebook(name, path)['content'] nbm.mark_trusted_cells(nb, name, path) diff --git a/IPython/html/services/notebooks/tests/test_notebooks_api.py b/IPython/html/services/contents/tests/test_notebooks_api.py similarity index 99% rename from IPython/html/services/notebooks/tests/test_notebooks_api.py rename to IPython/html/services/contents/tests/test_notebooks_api.py index c8c82e8744a..74c9a257b77 100644 --- a/IPython/html/services/notebooks/tests/test_notebooks_api.py +++ b/IPython/html/services/contents/tests/test_notebooks_api.py @@ -163,7 +163,7 @@ def test_list_notebooks(self): expected = [ u'a.ipynb', u'b.ipynb', u'name with spaces.ipynb', u'unicodé.ipynb'] expected = { normalize('NFC', name) for name in expected } self.assertEqual(nbnames, expected) - + nbs = notebooks_only(self.nb_api.list('ordering').json()) nbnames = [n['name'] for n in nbs] expected = ['A.ipynb', 'b.ipynb', 'C.ipynb'] @@ -344,4 +344,3 @@ def test_checkpoints(self): self.assertEqual(r.status_code, 204) cps = self.nb_api.get_checkpoints('a.ipynb', 'foo').json() self.assertEqual(cps, []) - From 48c2b3dc8ce203acae8f6654a20c6935cb56f25a Mon Sep 17 00:00:00 2001 From: MinRK Date: Fri, 30 May 2014 13:01:21 -0700 Subject: [PATCH 02/16] rename notebooks service to contents service minimal functional changes, committing because tests are passing. --- IPython/html/base/handlers.py | 8 +- IPython/html/nbconvert/handlers.py | 2 +- .../tests/test_nbconvert_handlers.py | 4 +- IPython/html/notebook/handlers.py | 10 +- IPython/html/notebookapp.py | 47 ++--- .../{filenbmanager.py => filemanager.py} | 165 ++++++--------- IPython/html/services/contents/handlers.py | 159 +++++++-------- .../contents/{nbmanager.py => manager.py} | 88 ++++---- ..._notebooks_api.py => test_contents_api.py} | 88 ++++---- .../{test_nbmanager.py => test_manager.py} | 191 ++++++++---------- IPython/html/services/sessions/handlers.py | 25 +-- .../html/services/sessions/sessionmanager.py | 4 +- IPython/html/static/notebook/js/notebook.js | 20 +- IPython/html/static/tree/js/notebooklist.js | 8 +- IPython/html/tests/launchnotebook.py | 2 +- IPython/html/tree/handlers.py | 6 +- 16 files changed, 360 insertions(+), 467 deletions(-) rename IPython/html/services/contents/{filenbmanager.py => filemanager.py} (70%) rename IPython/html/services/contents/{nbmanager.py => manager.py} (73%) rename IPython/html/services/contents/tests/{test_notebooks_api.py => test_contents_api.py} (80%) rename IPython/html/services/contents/tests/{test_nbmanager.py => test_manager.py} (58%) diff --git a/IPython/html/base/handlers.py b/IPython/html/base/handlers.py index b52d5de5757..8796e55cbd5 100644 --- a/IPython/html/base/handlers.py +++ b/IPython/html/base/handlers.py @@ -1,4 +1,4 @@ -"""Base Tornado handlers for the notebook.""" +"""Base Tornado handlers for the notebook server.""" # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. @@ -141,8 +141,8 @@ def kernel_manager(self): return self.settings['kernel_manager'] @property - def notebook_manager(self): - return self.settings['notebook_manager'] + def contents_manager(self): + return self.settings['contents_manager'] @property def cluster_manager(self): @@ -158,7 +158,7 @@ def kernel_spec_manager(self): @property def project_dir(self): - return self.notebook_manager.notebook_dir + return getattr(self.contents_manager, 'root_dir', '/') #--------------------------------------------------------------- # CORS diff --git a/IPython/html/nbconvert/handlers.py b/IPython/html/nbconvert/handlers.py index fb97f5f0323..180e6c67887 100644 --- a/IPython/html/nbconvert/handlers.py +++ b/IPython/html/nbconvert/handlers.py @@ -73,7 +73,7 @@ def get(self, format, path='', name=None): exporter = get_exporter(format, config=self.config, log=self.log) path = path.strip('/') - model = self.notebook_manager.get_notebook(name=name, path=path) + model = self.contents_manager.get(name=name, path=path) self.set_header('Last-Modified', model['last_modified']) diff --git a/IPython/html/nbconvert/tests/test_nbconvert_handlers.py b/IPython/html/nbconvert/tests/test_nbconvert_handlers.py index 6916f1f4c99..ea44217a314 100644 --- a/IPython/html/nbconvert/tests/test_nbconvert_handlers.py +++ b/IPython/html/nbconvert/tests/test_nbconvert_handlers.py @@ -106,7 +106,7 @@ def test_from_file_zip(self): @onlyif_cmds_exist('pandoc') def test_from_post(self): - nbmodel_url = url_path_join(self.base_url(), 'api/notebooks/foo/testnb.ipynb') + nbmodel_url = url_path_join(self.base_url(), 'api/contents/foo/testnb.ipynb') nbmodel = requests.get(nbmodel_url).json() r = self.nbconvert_api.from_post(format='html', nbmodel=nbmodel) @@ -121,7 +121,7 @@ def test_from_post(self): @onlyif_cmds_exist('pandoc') def test_from_post_zip(self): - nbmodel_url = url_path_join(self.base_url(), 'api/notebooks/foo/testnb.ipynb') + nbmodel_url = url_path_join(self.base_url(), 'api/contents/foo/testnb.ipynb') nbmodel = requests.get(nbmodel_url).json() r = self.nbconvert_api.from_post(format='latex', nbmodel=nbmodel) diff --git a/IPython/html/notebook/handlers.py b/IPython/html/notebook/handlers.py index 5db20ccc94b..0fc88919831 100644 --- a/IPython/html/notebook/handlers.py +++ b/IPython/html/notebook/handlers.py @@ -35,12 +35,12 @@ 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.""" path = path.strip('/') - nbm = self.notebook_manager + cm = self.contents_manager if name is None: raise web.HTTPError(500, "This shouldn't be accessible: %s" % self.request.uri) # a .ipynb filename was given - if not nbm.notebook_exists(name, path): + if not cm.file_exists(name, path): raise web.HTTPError(404, u'Notebook does not exist: %s/%s' % (path, name)) name = url_escape(name) path = url_escape(path) @@ -55,8 +55,8 @@ def get(self, path='', name=None): class NotebookRedirectHandler(IPythonHandler): def get(self, path=''): - nbm = self.notebook_manager - if nbm.path_exists(path): + cm = self.contents_manager + if cm.path_exists(path): # it's a *directory*, redirect to /tree url = url_path_join(self.base_url, 'tree', path) else: @@ -68,7 +68,7 @@ def get(self, path=''): # but so is the files handler itself, # so it should work until both are cleaned up. parts = path.split('/') - files_path = os.path.join(nbm.notebook_dir, *parts) + files_path = os.path.join(cm.root_dir, *parts) if not os.path.exists(files_path): self.log.warn("Deprecated files/ URL: %s", path) path = path.replace('/files/', '/', 1) diff --git a/IPython/html/notebookapp.py b/IPython/html/notebookapp.py index 013f14e0791..31d7d0f4dbe 100644 --- a/IPython/html/notebookapp.py +++ b/IPython/html/notebookapp.py @@ -55,8 +55,8 @@ from .base.handlers import Template404 from .log import log_request from .services.kernels.kernelmanager import MappingKernelManager -from .services.notebooks.nbmanager import NotebookManager -from .services.notebooks.filenbmanager import FileNotebookManager +from .services.contents.manager import ContentsManager +from .services.contents.filemanager import FileContentsManager from .services.clusters.clustermanager import ClusterManager from .services.sessions.sessionmanager import SessionManager @@ -121,19 +121,19 @@ def load_handlers(name): class NotebookWebApplication(web.Application): - def __init__(self, ipython_app, kernel_manager, notebook_manager, + def __init__(self, ipython_app, kernel_manager, contents_manager, cluster_manager, session_manager, kernel_spec_manager, log, base_url, settings_overrides, jinja_env_options): settings = self.init_settings( - ipython_app, kernel_manager, notebook_manager, cluster_manager, + ipython_app, kernel_manager, contents_manager, cluster_manager, session_manager, kernel_spec_manager, log, base_url, settings_overrides, jinja_env_options) handlers = self.init_handlers(settings) super(NotebookWebApplication, self).__init__(handlers, **settings) - def init_settings(self, ipython_app, kernel_manager, notebook_manager, + def init_settings(self, ipython_app, kernel_manager, contents_manager, cluster_manager, session_manager, kernel_spec_manager, log, base_url, settings_overrides, jinja_env_options=None): @@ -165,7 +165,7 @@ def init_settings(self, ipython_app, kernel_manager, notebook_manager, # managers kernel_manager=kernel_manager, - notebook_manager=notebook_manager, + contents_manager=contents_manager, cluster_manager=cluster_manager, session_manager=session_manager, kernel_spec_manager=kernel_spec_manager, @@ -193,18 +193,20 @@ def init_handlers(self, settings): handlers.extend(load_handlers('nbconvert.handlers')) handlers.extend(load_handlers('kernelspecs.handlers')) handlers.extend(load_handlers('services.kernels.handlers')) - handlers.extend(load_handlers('services.notebooks.handlers')) + handlers.extend(load_handlers('services.contents.handlers')) handlers.extend(load_handlers('services.clusters.handlers')) handlers.extend(load_handlers('services.sessions.handlers')) handlers.extend(load_handlers('services.nbconvert.handlers')) handlers.extend(load_handlers('services.kernelspecs.handlers')) # FIXME: /files/ should be handled by the Contents service when it exists - nbm = settings['notebook_manager'] - if hasattr(nbm, 'notebook_dir'): - handlers.extend([ - (r"/files/(.*)", AuthenticatedFileHandler, {'path' : nbm.notebook_dir}), + cm = settings['contents_manager'] + if hasattr(cm, 'root_dir'): + handlers.append( + (r"/files/(.*)", AuthenticatedFileHandler, {'path' : cm.root_dir}), + ) + handlers.append( (r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}), - ]) + ) # prepend base_url onto the patterns that we match new_handlers = [] for handler in handlers: @@ -263,11 +265,6 @@ def start(self): """ ) -# Add notebook manager flags -flags.update(boolean_flag('script', 'FileNotebookManager.save_script', - 'Auto-save a .py script everytime the .ipynb notebook is saved', - 'Do not auto-save .py scripts for every notebook')) - aliases = dict(base_aliases) aliases.update({ @@ -302,7 +299,7 @@ class NotebookApp(BaseIPythonApplication): classes = [ KernelManager, ProfileDir, Session, MappingKernelManager, - NotebookManager, FileNotebookManager, NotebookNotary, + ContentsManager, FileContentsManager, NotebookNotary, ] flags = Dict(flags) aliases = Dict(aliases) @@ -557,7 +554,7 @@ def _mathjax_url_changed(self, name, old, new): else: self.log.info("Using MathJax: %s", new) - notebook_manager_class = DottedObjectName('IPython.html.services.notebooks.filenbmanager.FileNotebookManager', + contents_manager_class = DottedObjectName('IPython.html.services.contents.filemanager.FileContentsManager', config=True, help='The notebook manager class to use.' ) @@ -621,7 +618,7 @@ def _notebook_dir_changed(self, name, old, new): raise TraitError("No such notebook dir: %r" % new) # setting App.notebook_dir implies setting notebook and kernel dirs as well - self.config.FileNotebookManager.notebook_dir = new + self.config.FileContentsManager.root_dir = new self.config.MappingKernelManager.root_dir = new @@ -658,12 +655,12 @@ def init_configurables(self): parent=self, log=self.log, kernel_argv=self.kernel_argv, connection_dir = self.profile_dir.security_dir, ) - kls = import_item(self.notebook_manager_class) - self.notebook_manager = kls(parent=self, log=self.log) + kls = import_item(self.contents_manager_class) + self.contents_manager = kls(parent=self, log=self.log) kls = import_item(self.session_manager_class) self.session_manager = kls(parent=self, log=self.log, kernel_manager=self.kernel_manager, - notebook_manager=self.notebook_manager) + contents_manager=self.contents_manager) kls = import_item(self.cluster_manager_class) self.cluster_manager = kls(parent=self, log=self.log) self.cluster_manager.update_profiles() @@ -688,7 +685,7 @@ def init_webapp(self): self.webapp_settings['allow_credentials'] = self.allow_credentials self.web_app = NotebookWebApplication( - self, self.kernel_manager, self.notebook_manager, + self, self.kernel_manager, self.contents_manager, self.cluster_manager, self.session_manager, self.kernel_spec_manager, self.log, self.base_url, self.webapp_settings, self.jinja_environment_options @@ -838,7 +835,7 @@ def cleanup_kernels(self): def notebook_info(self): "Return the current working directory and the server url information" - info = self.notebook_manager.info_string() + "\n" + info = self.contents_manager.info_string() + "\n" info += "%d active kernels \n" % len(self.kernel_manager._kernels) return info + "The IPython Notebook is running at: %s" % self.display_url diff --git a/IPython/html/services/contents/filenbmanager.py b/IPython/html/services/contents/filemanager.py similarity index 70% rename from IPython/html/services/contents/filenbmanager.py rename to IPython/html/services/contents/filemanager.py index 118f02dbdf6..2ddca804796 100644 --- a/IPython/html/services/contents/filenbmanager.py +++ b/IPython/html/services/contents/filemanager.py @@ -1,4 +1,4 @@ -"""A notebook manager that uses the local file system for storage.""" +"""A contents manager that uses the local file system for storage.""" # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. @@ -10,7 +10,7 @@ from tornado import web -from .nbmanager import NotebookManager +from .manager import ContentsManager from IPython.nbformat import current from IPython.utils.path import ensure_dir_exists from IPython.utils.traitlets import Unicode, Bool, TraitError @@ -22,31 +22,19 @@ def sort_key(item): """Case-insensitive sorting.""" return item['name'].lower() -#----------------------------------------------------------------------------- -# Classes -#----------------------------------------------------------------------------- -class FileNotebookManager(NotebookManager): +class FileContentsManager(ContentsManager): - save_script = Bool(False, config=True, - help="""Automatically create a Python script when saving the notebook. + root_dir = Unicode(getcwd(), config=True) - For easier use of import, %run and %load across notebooks, a - .py script will be created next to any - .ipynb on each save. This can also be set with the - short `--script` flag. - """ - ) - notebook_dir = Unicode(getcwd(), config=True) - - def _notebook_dir_changed(self, name, old, new): - """Do a bit of validation of the notebook dir.""" + def _root_dir_changed(self, name, old, new): + """Do a bit of validation of the root_dir.""" if not os.path.isabs(new): # If we receive a non-absolute path, make it absolute. - self.notebook_dir = os.path.abspath(new) + self.root_dir = os.path.abspath(new) return if not os.path.exists(new) or not os.path.isdir(new): - raise TraitError("notebook dir %r is not a directory" % new) + raise TraitError("%r is not a directory" % new) checkpoint_dir = Unicode('.ipynb_checkpoints', config=True, help="""The directory name in which to keep notebook checkpoints @@ -68,14 +56,13 @@ def _copy(self, src, dest): except OSError as e: self.log.debug("copystat on %s failed", dest, exc_info=True) - def get_notebook_names(self, path=''): - """List all notebook names in the notebook dir and path.""" + def get_names(self, path=''): + """List all filenames in the path (relative to root_dir).""" path = path.strip('/') if not os.path.isdir(self._get_os_path(path=path)): raise web.HTTPError(404, 'Directory not found: ' + path) - names = glob.glob(self._get_os_path('*'+self.filename_ext, path)) - names = [os.path.basename(name) - for name in names] + names = glob.glob(self._get_os_path('*', path)) + names = [ os.path.basename(name) for name in names if os.path.isfile(name)] return names def path_exists(self, path): @@ -85,7 +72,7 @@ def path_exists(self, path): ---------- path : string The path to check. This is an API path (`/` separated, - relative to base notebook-dir). + relative to root_dir). Returns ------- @@ -103,7 +90,7 @@ def is_hidden(self, path): ---------- path : string The path to check. This is an API path (`/` separated, - relative to base notebook-dir). + relative to root_dir). Returns ------- @@ -113,40 +100,38 @@ def is_hidden(self, path): """ path = path.strip('/') os_path = self._get_os_path(path=path) - return is_hidden(os_path, self.notebook_dir) + return is_hidden(os_path, self.root_dir) def _get_os_path(self, name=None, path=''): - """Given a notebook name and a URL path, return its file system + """Given a filename and a URL path, return its file system path. Parameters ---------- name : string - The name of a notebook file with the .ipynb extension + A filename path : string The relative URL path (with '/' as separator) to the named - notebook. + file. 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. + API path to be evaluated relative to root_dir. """ if name is not None: path = path + '/' + name - return to_os_path(path, self.notebook_dir) + return to_os_path(path, self.root_dir) - def notebook_exists(self, name, path=''): - """Returns a True if the notebook exists. Else, returns False. + def file_exists(self, name, path=''): + """Returns a True if the file exists, else returns False. Parameters ---------- name : string - The name of the notebook you are checking. + The name of the file you are checking. path : string - The relative path to the notebook (with '/' as separator) + The relative path to the file's directory (with '/' as separator) Returns ------- @@ -164,14 +149,14 @@ def list_dirs(self, path): os_path = self._get_os_path('', path) if not os.path.isdir(os_path): raise web.HTTPError(404, u'directory does not exist: %r' % os_path) - elif is_hidden(os_path, self.notebook_dir): + elif is_hidden(os_path, self.root_dir): self.log.info("Refusing to serve hidden directory, via 404 Error") 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)\ + if os.path.isdir(os_path) and not is_hidden(os_path, self.root_dir)\ and self.should_list(name): try: model = self.get_dir_model(name, path) @@ -201,7 +186,7 @@ def get_dir_model(self, name, path=''): model['type'] = 'directory' return model - def list_notebooks(self, path): + def list_files(self, path): """Returns a list of dictionaries that are the standard model for all notebooks in the relative 'path'. @@ -217,13 +202,13 @@ def list_notebooks(self, path): a list of the notebook models without 'content' """ path = path.strip('/') - notebook_names = self.get_notebook_names(path) - notebooks = [self.get_notebook(name, path, content=False) - for name in notebook_names if self.should_list(name)] + names = self.get_names(path) + notebooks = [self.get(name, path, content=False) + for name in names if self.should_list(name)] notebooks = sorted(notebooks, key=sort_key) return notebooks - def get_notebook(self, name, path='', content=True): + def get(self, name, path='', content=True): """ Takes a path and name for a notebook and returns its model Parameters @@ -241,7 +226,7 @@ def get_notebook(self, name, path='', content=True): dict in the model as well. """ path = path.strip('/') - if not self.notebook_exists(name=name, path=path): + if not self.file_exists(name=name, path=path): raise web.HTTPError(404, u'Notebook does not exist: %s' % name) os_path = self._get_os_path(name, path) info = os.stat(os_path) @@ -264,7 +249,7 @@ def get_notebook(self, name, path='', content=True): model['content'] = nb return model - def save_notebook(self, model, name='', path=''): + def save(self, model, name='', path=''): """Save the notebook model and return the model with no content.""" path = path.strip('/') @@ -272,14 +257,14 @@ def save_notebook(self, model, name='', path=''): 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): + if self.file_exists(name, path) and not self.list_checkpoints(name, path): self.create_checkpoint(name, path) new_path = model.get('path', path).strip('/') new_name = model.get('name', name) if path != new_path or name != new_name: - self.rename_notebook(name, path, new_name, new_path) + self.rename(name, path, new_name, new_path) # Save the notebook file os_path = self._get_os_path(new_name, new_path) @@ -296,35 +281,25 @@ def save_notebook(self, model, name='', path=''): except Exception as e: raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s %s' % (os_path, e)) - # Save .py script as well - if self.save_script: - py_path = os.path.splitext(os_path)[0] + '.py' - self.log.debug("Writing script %s", py_path) - try: - with io.open(py_path, 'w', encoding='utf-8') as f: - current.write(nb, f, u'py') - except Exception as e: - raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s %s' % (py_path, e)) - - model = self.get_notebook(new_name, new_path, content=False) + model = self.get(new_name, new_path, content=False) return model - def update_notebook(self, model, name, path=''): - """Update the notebook's path and/or name""" + def update(self, model, name, path=''): + """Update the file's path and/or name""" path = path.strip('/') new_name = model.get('name', name) new_path = model.get('path', path).strip('/') if path != new_path or name != new_name: - self.rename_notebook(name, path, new_name, new_path) - model = self.get_notebook(new_name, new_path, content=False) + self.rename(name, path, new_name, new_path) + model = self.get(new_name, new_path, content=False) return model - def delete_notebook(self, name, path=''): - """Delete notebook by name and path.""" + def delete(self, name, path=''): + """Delete file by name and path.""" path = path.strip('/') os_path = self._get_os_path(name, path) if not os.path.isfile(os_path): - raise web.HTTPError(404, u'Notebook does not exist: %s' % os_path) + raise web.HTTPError(404, u'File does not exist: %s' % os_path) # clear checkpoints for checkpoint in self.list_checkpoints(name, path): @@ -334,11 +309,11 @@ def delete_notebook(self, name, path=''): self.log.debug("Unlinking checkpoint %s", cp_path) os.unlink(cp_path) - self.log.debug("Unlinking notebook %s", os_path) + self.log.debug("Unlinking file %s", os_path) os.unlink(os_path) - def rename_notebook(self, old_name, old_path, new_name, new_path): - """Rename a notebook.""" + def rename(self, old_name, old_path, new_name, new_path): + """Rename a file.""" old_path = old_path.strip('/') new_path = new_path.strip('/') if new_name == old_name and new_path == old_path: @@ -350,17 +325,12 @@ def rename_notebook(self, old_name, old_path, new_name, new_path): # Should we proceed with the move? if os.path.isfile(new_os_path): raise web.HTTPError(409, u'Notebook with name already exists: %s' % new_os_path) - if self.save_script: - old_py_path = os.path.splitext(old_os_path)[0] + '.py' - new_py_path = os.path.splitext(new_os_path)[0] + '.py' - if os.path.isfile(new_py_path): - raise web.HTTPError(409, u'Python script with name already exists: %s' % new_py_path) - # Move the notebook file + # Move the file try: shutil.move(old_os_path, new_os_path) except Exception as e: - raise web.HTTPError(500, u'Unknown error renaming notebook: %s %s' % (old_os_path, e)) + raise web.HTTPError(500, u'Unknown error renaming file: %s %s' % (old_os_path, e)) # Move the checkpoints old_checkpoints = self.list_checkpoints(old_name, old_path) @@ -372,20 +342,16 @@ def rename_notebook(self, old_name, old_path, new_name, new_path): self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path) shutil.move(old_cp_path, new_cp_path) - # Move the .py script - if self.save_script: - shutil.move(old_py_path, new_py_path) - # Checkpoint-related utilities def get_checkpoint_path(self, checkpoint_id, name, path=''): """find the path to a checkpoint""" path = path.strip('/') - basename, _ = os.path.splitext(name) + basename, ext = os.path.splitext(name) filename = u"{name}-{checkpoint_id}{ext}".format( name=basename, checkpoint_id=checkpoint_id, - ext=self.filename_ext, + ext=ext, ) os_path = self._get_os_path(path=path) cp_dir = os.path.join(os_path, self.checkpoint_dir) @@ -408,22 +374,22 @@ def get_checkpoint_model(self, checkpoint_id, name, path=''): # public checkpoint API def create_checkpoint(self, name, path=''): - """Create a checkpoint from the current state of a notebook""" + """Create a checkpoint from the current state of a file""" path = path.strip('/') - nb_path = self._get_os_path(name, path) + src_path = self._get_os_path(name, path) # only the one checkpoint ID: checkpoint_id = u"checkpoint" cp_path = self.get_checkpoint_path(checkpoint_id, name, path) self.log.debug("creating checkpoint for notebook %s", name) - self._copy(nb_path, cp_path) + self._copy(src_path, cp_path) # return the checkpoint info return self.get_checkpoint_model(checkpoint_id, name, path) def list_checkpoints(self, name, path=''): - """list the checkpoints for a given notebook + """list the checkpoints for a given file - This notebook manager currently only supports one checkpoint per notebook. + This contents manager currently only supports one checkpoint per file. """ path = path.strip('/') checkpoint_id = "checkpoint" @@ -435,36 +401,37 @@ def list_checkpoints(self, name, path=''): def restore_checkpoint(self, checkpoint_id, name, path=''): - """restore a notebook to a checkpointed state""" + """restore a file to a checkpointed state""" path = path.strip('/') - self.log.info("restoring Notebook %s from checkpoint %s", name, checkpoint_id) + self.log.info("restoring %s from checkpoint %s", name, checkpoint_id) nb_path = self._get_os_path(name, path) cp_path = self.get_checkpoint_path(checkpoint_id, name, path) if not os.path.isfile(cp_path): self.log.debug("checkpoint file does not exist: %s", cp_path) raise web.HTTPError(404, - u'Notebook checkpoint does not exist: %s-%s' % (name, checkpoint_id) + u'checkpoint does not exist: %s-%s' % (name, checkpoint_id) ) # ensure notebook is readable (never restore from an unreadable notebook) - with io.open(cp_path, 'r', encoding='utf-8') as f: - current.read(f, u'json') + if cp_path.endswith('.ipynb'): + with io.open(cp_path, 'r', encoding='utf-8') as f: + current.read(f, u'json') self._copy(cp_path, nb_path) self.log.debug("copying %s -> %s", cp_path, nb_path) def delete_checkpoint(self, checkpoint_id, name, path=''): - """delete a notebook's checkpoint""" + """delete a file's checkpoint""" path = path.strip('/') cp_path = self.get_checkpoint_path(checkpoint_id, name, path) if not os.path.isfile(cp_path): raise web.HTTPError(404, - u'Notebook checkpoint does not exist: %s%s-%s' % (path, name, checkpoint_id) + u'Checkpoint does not exist: %s%s-%s' % (path, name, checkpoint_id) ) self.log.debug("unlinking %s", cp_path) os.unlink(cp_path) def info_string(self): - return "Serving notebooks from local directory: %s" % self.notebook_dir + return "Serving notebooks from local directory: %s" % self.root_dir def get_kernel_path(self, name, path='', model=None): - """ Return the path to start kernel in """ - return os.path.join(self.notebook_dir, path) + """Return the initial working dir a kernel associated with a given notebook""" + return os.path.join(self.root_dir, path) diff --git a/IPython/html/services/contents/handlers.py b/IPython/html/services/contents/handlers.py index 5647ce782c4..878b8e71617 100644 --- a/IPython/html/services/contents/handlers.py +++ b/IPython/html/services/contents/handlers.py @@ -1,20 +1,7 @@ -"""Tornado handlers for the notebooks web service. +"""Tornado handlers for the contents web service.""" -Authors: - -* Brian Granger -""" - -#----------------------------------------------------------------------------- -# Copyright (C) 2011 The IPython Development Team -# -# Distributed under the terms of the BSD License. The full license is in -# the file COPYING, distributed as part of this software. -#----------------------------------------------------------------------------- - -#----------------------------------------------------------------------------- -# Imports -#----------------------------------------------------------------------------- +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. import json @@ -27,33 +14,29 @@ notebook_path_regex, path_regex, notebook_name_regex) -#----------------------------------------------------------------------------- -# Notebook web service handlers -#----------------------------------------------------------------------------- - -class NotebookHandler(IPythonHandler): +class ContentsHandler(IPythonHandler): SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE') - def notebook_location(self, name, path=''): - """Return the full URL location of a notebook based. + def location_url(self, name, path=''): + """Return the full URL location of a file. Parameters ---------- name : unicode - The base name of the notebook, such as "foo.ipynb". + The base name of the file, such as "foo.ipynb". path : unicode - The URL path of the notebook. + The API path of the file, such as "foo/bar". """ return url_escape(url_path_join( - self.base_url, 'api', 'notebooks', path, name + self.base_url, 'api', 'contents', path, name )) def _finish_model(self, model, location=True): """Finish a JSON request with a model, setting relevant headers, etc.""" if location: - location = self.notebook_location(model['name'], model['path']) + location = self.location_url(model['name'], model['path']) self.set_header('Location', location) self.set_header('Last-Modified', model['last_modified']) self.finish(json.dumps(model, default=date_default)) @@ -61,68 +44,68 @@ def _finish_model(self, model, location=True): @web.authenticated @json_errors def get(self, path='', name=None): - """Return a Notebook or list of notebooks. + """Return a file or list of files. - * GET with path and no notebook name lists notebooks in a directory - * GET with path and notebook name returns notebook JSON + * GET with path and no filename lists files in a directory + * GET with path and filename returns file contents model """ - nbm = self.notebook_manager - # Check to see if a notebook name was given + cm = self.contents_manager + # Check to see if a filename was given if name is None: # 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 = [] + dirs = cm.list_dirs(path) + files = [] index = [] - for nb in nbm.list_notebooks(path): + for nb in cm.list_files(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)) + files.append(nb) + files = index + dirs + files + self.finish(json.dumps(files, default=date_default)) return # get and return notebook representation - model = nbm.get_notebook(name, path) + model = cm.get(name, path) self._finish_model(model, location=False) @web.authenticated @json_errors def patch(self, path='', name=None): """PATCH renames a notebook without re-uploading content.""" - nbm = self.notebook_manager + cm = self.contents_manager if name is None: - raise web.HTTPError(400, u'Notebook name missing') + raise web.HTTPError(400, u'Filename missing') model = self.get_json_body() if model is None: raise web.HTTPError(400, u'JSON body missing') - model = nbm.update_notebook(model, name, path) + model = cm.update(model, name, path) self._finish_model(model) - def _copy_notebook(self, copy_from, path, copy_to=None): - """Copy a notebook in path, optionally specifying the new name. + def _copy(self, copy_from, path, copy_to=None): + """Copy a file in path, optionally specifying the new name. Only support copying within the same directory. """ - self.log.info(u"Copying notebook from %s/%s to %s/%s", + self.log.info(u"Copying from %s/%s to %s/%s", path, copy_from, path, copy_to or '', ) - model = self.notebook_manager.copy_notebook(copy_from, copy_to, path) + model = self.contents_manager.copy(copy_from, copy_to, path) self.set_status(201) self._finish_model(model) - def _upload_notebook(self, model, path, name=None): - """Upload a notebook + def _upload(self, model, path, name=None): + """Upload a file If name specified, create it in path/name. """ - self.log.info(u"Uploading notebook to %s/%s", path, name or '') + self.log.info(u"Uploading file to %s/%s", path, name or '') if name: model['name'] = name - model = self.notebook_manager.create_notebook(model, path) + model = self.contents_manager.create_notebook(model, path) self.set_status(201) self._finish_model(model) @@ -135,14 +118,14 @@ def _create_empty_notebook(self, path, name=None): model = {} if name: model['name'] = name - model = self.notebook_manager.create_notebook(model, path=path) + model = self.contents_manager.create_notebook(model, path=path) self.set_status(201) self._finish_model(model) - def _save_notebook(self, model, path, name): - """Save an existing notebook.""" - self.log.info(u"Saving notebook at %s/%s", path, name) - model = self.notebook_manager.save_notebook(model, name, path) + def _save(self, model, path, name): + """Save an existing file.""" + self.log.info(u"Saving file at %s/%s", path, name) + model = self.contents_manager.save(model, name, path) if model['path'] != path.strip('/') or model['name'] != name: # a rename happened, set Location header location = True @@ -157,10 +140,10 @@ def post(self, path='', name=None): POST creates new notebooks. The server always decides on the notebook name. - POST /api/notebooks/path + POST /api/contents/path New untitled notebook in path. If content specified, upload a notebook, otherwise start empty. - POST /api/notebooks/path?copy=OtherNotebook.ipynb + POST /api/contents/path?copy=OtherNotebook.ipynb New copy of OtherNotebook in path """ @@ -174,25 +157,25 @@ def post(self, path='', name=None): if copy_from: if model.get('content'): raise web.HTTPError(400, "Can't upload and copy at the same time.") - self._copy_notebook(copy_from, path) + self._copy(copy_from, path) else: - self._upload_notebook(model, path) + self._upload(model, path) else: self._create_empty_notebook(path) @web.authenticated @json_errors def put(self, path='', name=None): - """Saves the notebook in the location specified by name and path. + """Saves the file in the location specified by name and path. PUT is very similar to POST, but the requester specifies the name, whereas with POST, the server picks the name. - PUT /api/notebooks/path/Name.ipynb + PUT /api/contents/path/Name.ipynb Save notebook at ``path/Name.ipynb``. Notebook structure is specified in `content` key of JSON request body. If content is not specified, create a new empty notebook. - PUT /api/notebooks/path/Name.ipynb?copy=OtherNotebook.ipynb + PUT /api/contents/path/Name.ipynb?copy=OtherNotebook.ipynb Copy OtherNotebook to Name """ if name is None: @@ -204,34 +187,34 @@ def put(self, path='', name=None): if copy_from: if model.get('content'): raise web.HTTPError(400, "Can't upload and copy at the same time.") - self._copy_notebook(copy_from, path, name) - elif self.notebook_manager.notebook_exists(name, path): - self._save_notebook(model, path, name) + self._copy(copy_from, path, name) + elif self.contents_manager.file_exists(name, path): + self._save(model, path, name) else: - self._upload_notebook(model, path, name) + self._upload(model, path, name) else: self._create_empty_notebook(path, name) @web.authenticated @json_errors def delete(self, path='', name=None): - """delete the notebook in the given notebook path""" - nbm = self.notebook_manager - nbm.delete_notebook(name, path) + """delete a file in the given path""" + cm = self.contents_manager + cm.delete(name, path) self.set_status(204) self.finish() -class NotebookCheckpointsHandler(IPythonHandler): +class CheckpointsHandler(IPythonHandler): SUPPORTED_METHODS = ('GET', 'POST') @web.authenticated @json_errors def get(self, path='', name=None): - """get lists checkpoints for a notebook""" - nbm = self.notebook_manager - checkpoints = nbm.list_checkpoints(name, path) + """get lists checkpoints for a file""" + cm = self.contents_manager + checkpoints = cm.list_checkpoints(name, path) data = json.dumps(checkpoints, default=date_default) self.finish(data) @@ -239,35 +222,35 @@ def get(self, path='', name=None): @json_errors def post(self, path='', name=None): """post creates a new checkpoint""" - nbm = self.notebook_manager - checkpoint = nbm.create_checkpoint(name, path) + cm = self.contents_manager + checkpoint = cm.create_checkpoint(name, path) data = json.dumps(checkpoint, default=date_default) - location = url_path_join(self.base_url, 'api/notebooks', + location = url_path_join(self.base_url, 'api/contents', path, name, 'checkpoints', checkpoint['id']) self.set_header('Location', url_escape(location)) self.set_status(201) self.finish(data) -class ModifyNotebookCheckpointsHandler(IPythonHandler): +class ModifyCheckpointsHandler(IPythonHandler): SUPPORTED_METHODS = ('POST', 'DELETE') @web.authenticated @json_errors def post(self, path, name, checkpoint_id): - """post restores a notebook from a checkpoint""" - nbm = self.notebook_manager - nbm.restore_checkpoint(checkpoint_id, name, path) + """post restores a file from a checkpoint""" + cm = self.contents_manager + cm.restore_checkpoint(checkpoint_id, name, path) self.set_status(204) self.finish() @web.authenticated @json_errors def delete(self, path, name, checkpoint_id): - """delete clears a checkpoint for a given notebook""" - nbm = self.notebook_manager - nbm.delete_checkpoint(checkpoint_id, name, path) + """delete clears a checkpoint for a given file""" + cm = self.contents_manager + cm.delete_checkpoint(checkpoint_id, name, path) self.set_status(204) self.finish() @@ -279,9 +262,9 @@ def delete(self, path, name, checkpoint_id): _checkpoint_id_regex = r"(?P[\w-]+)" default_handlers = [ - (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" % path_regex, NotebookHandler), + (r"/api/contents%s/checkpoints" % notebook_path_regex, CheckpointsHandler), + (r"/api/contents%s/checkpoints/%s" % (notebook_path_regex, _checkpoint_id_regex), + ModifyCheckpointsHandler), + (r"/api/contents%s" % notebook_path_regex, ContentsHandler), + (r"/api/contents%s" % path_regex, ContentsHandler), ] diff --git a/IPython/html/services/contents/nbmanager.py b/IPython/html/services/contents/manager.py similarity index 73% rename from IPython/html/services/contents/nbmanager.py rename to IPython/html/services/contents/manager.py index 5f8bd978fae..32f3677103d 100644 --- a/IPython/html/services/contents/nbmanager.py +++ b/IPython/html/services/contents/manager.py @@ -1,21 +1,7 @@ -"""A base class notebook manager. +"""A base class for contents managers.""" -Authors: - -* Brian Granger -* Zach Sailer -""" - -#----------------------------------------------------------------------------- -# Copyright (C) 2011 The IPython Development Team -# -# Distributed under the terms of the BSD License. The full license is in -# the file COPYING, distributed as part of this software. -#----------------------------------------------------------------------------- - -#----------------------------------------------------------------------------- -# Imports -#----------------------------------------------------------------------------- +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. from fnmatch import fnmatch import itertools @@ -25,13 +11,8 @@ from IPython.nbformat import current, sign from IPython.utils.traitlets import Instance, Unicode, List -#----------------------------------------------------------------------------- -# Classes -#----------------------------------------------------------------------------- -class NotebookManager(LoggingConfigurable): - - filename_ext = Unicode(u'.ipynb') +class ContentsManager(LoggingConfigurable): notary = Instance(sign.NotebookNotary) def _notary_default(self): @@ -41,7 +22,7 @@ def _notary_default(self): Glob patterns to hide in file and directory listings. """) - # NotebookManager API part 1: methods that must be + # ContentsManager API part 1: methods that must be # implemented in subclasses. def path_exists(self, path): @@ -68,7 +49,7 @@ def is_hidden(self, path): ---------- path : string The path to check. This is an API path (`/` separated, - relative to base notebook-dir). + relative to root dir). Returns ------- @@ -78,7 +59,7 @@ def is_hidden(self, path): """ raise NotImplementedError - def notebook_exists(self, name, path=''): + def file_exists(self, name, path=''): """Returns a True if the notebook exists. Else, returns False. Parameters @@ -114,12 +95,10 @@ def get_dir_model(self, name, path=''): """ raise NotImplementedError('must be implemented in a subclass') - def list_notebooks(self, path=''): - """Return a list of notebook dicts without content. - - This returns a list of dicts, each of the form:: + def list_files(self, path=''): + """Return a list of contents dicts without content. - dict(notebook_id=notebook,name=name) + This returns a list of dicts This list of dicts should be sorted by name:: @@ -127,19 +106,19 @@ def list_notebooks(self, path=''): """ raise NotImplementedError('must be implemented in a subclass') - def get_notebook(self, name, path='', content=True): + def get_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(self, model, name, path=''): + def save(self, model, name, path=''): """Save the notebook and return the model with no content.""" raise NotImplementedError('must be implemented in a subclass') - def update_notebook(self, model, name, path=''): + def update(self, model, name, path=''): """Update the notebook and return the model with no content.""" raise NotImplementedError('must be implemented in a subclass') - def delete_notebook(self, name, path=''): + def delete(self, name, path=''): """Delete notebook by name and path.""" raise NotImplementedError('must be implemented in a subclass') @@ -165,34 +144,34 @@ def delete_checkpoint(self, checkpoint_id, name, path=''): def info_string(self): return "Serving notebooks" - # NotebookManager API part 2: methods that have useable default + # ContentsManager API part 2: methods that have useable default # implementations, but can be overridden in subclasses. def get_kernel_path(self, name, path='', model=None): """ Return the path to start kernel in """ return path - def increment_filename(self, basename, path=''): - """Increment a notebook filename without the .ipynb to make it unique. + def increment_filename(self, filename, path=''): + """Increment a filename until it is unique. Parameters ---------- - basename : unicode - The name of a notebook without the ``.ipynb`` file extension. + filename : unicode + The name of a file, including extension path : unicode The URL path of the notebooks directory Returns ------- name : unicode - A notebook name (with the .ipynb extension) that starts - with basename and does not refer to any existing notebook. + A filename that is unique, based on the input filename. """ path = path.strip('/') + basename, ext = os.path.splitext(filename) for i in itertools.count(): name = u'{basename}{i}{ext}'.format(basename=basename, i=i, - ext=self.filename_ext) - if not self.notebook_exists(name, path): + ext=ext) + if not self.file_exists(name, path): break return name @@ -205,24 +184,25 @@ def create_notebook(self, model=None, path=''): metadata = current.new_metadata(name=u'') model['content'] = current.new_notebook(metadata=metadata) if 'name' not in model: - model['name'] = self.increment_filename('Untitled', path) + model['name'] = self.increment_filename('Untitled.ipynb', path) model['path'] = path - model = self.save_notebook(model, model['name'], model['path']) + model = self.save(model, model['name'], model['path']) return model - def copy_notebook(self, from_name, to_name=None, path=''): - """Copy an existing notebook and return its new model. + def copy(self, from_name, to_name=None, path=''): + """Copy an existing file and return its new model. If to_name not specified, increment `from_name-Copy#.ipynb`. """ path = path.strip('/') - model = self.get_notebook(from_name, path) + model = self.get(from_name, path) if not to_name: - base = os.path.splitext(from_name)[0] + '-Copy' - to_name = self.increment_filename(base, path) + base, ext = os.path.splitext(from_name) + copy_name = u'{0}-Copy{1}'.format(base, ext) + to_name = self.increment_filename(copy_name, path) model['name'] = to_name - model = self.save_notebook(model, to_name, path) + model = self.save(model, to_name, path) return model def log_info(self): @@ -238,11 +218,11 @@ def trust_notebook(self, name, path=''): path : string The notebook's directory """ - model = self.get_notebook(name, path) + model = self.get(name, path) nb = model['content'] self.log.warn("Trusting notebook %s/%s", path, name) self.notary.mark_cells(nb, True) - self.save_notebook(model, name, path) + self.save(model, name, path) def check_and_sign(self, nb, name, path=''): """Check for trusted cells, and sign the notebook. diff --git a/IPython/html/services/contents/tests/test_notebooks_api.py b/IPython/html/services/contents/tests/test_contents_api.py similarity index 80% rename from IPython/html/services/contents/tests/test_notebooks_api.py rename to IPython/html/services/contents/tests/test_contents_api.py index 74c9a257b77..256b234ae44 100644 --- a/IPython/html/services/contents/tests/test_notebooks_api.py +++ b/IPython/html/services/contents/tests/test_contents_api.py @@ -1,5 +1,5 @@ # coding: utf-8 -"""Test the notebooks webservice API.""" +"""Test the contents webservice API.""" import io import json @@ -30,14 +30,14 @@ def dirs_only(nb_list): return [x for x in nb_list if x['type']=='directory'] -class NBAPI(object): - """Wrapper for notebook API calls.""" +class API(object): + """Wrapper for contents API calls.""" def __init__(self, base_url): self.base_url = base_url def _req(self, verb, path, body=None): response = requests.request(verb, - url_path_join(self.base_url, 'api/notebooks', path), + url_path_join(self.base_url, 'api/contents', path), data=body, ) response.raise_for_status() @@ -127,7 +127,7 @@ def setUp(self): nb = new_notebook(name=name) write(nb, f, format='ipynb') - self.nb_api = NBAPI(self.base_url()) + self.api = API(self.base_url()) def tearDown(self): nbdir = self.notebook_dir.name @@ -139,48 +139,48 @@ def tearDown(self): os.unlink(pjoin(nbdir, 'inroot.ipynb')) def test_list_notebooks(self): - nbs = notebooks_only(self.nb_api.list().json()) + nbs = notebooks_only(self.api.list().json()) self.assertEqual(len(nbs), 1) self.assertEqual(nbs[0]['name'], 'inroot.ipynb') - nbs = notebooks_only(self.nb_api.list('/Directory with spaces in/').json()) + nbs = notebooks_only(self.api.list('/Directory with spaces in/').json()) self.assertEqual(len(nbs), 1) self.assertEqual(nbs[0]['name'], 'inspace.ipynb') - nbs = notebooks_only(self.nb_api.list(u'/unicodé/').json()) + nbs = notebooks_only(self.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 = notebooks_only(self.nb_api.list('/foo/bar/').json()) + nbs = notebooks_only(self.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 = notebooks_only(self.nb_api.list('foo').json()) + nbs = notebooks_only(self.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'] expected = { normalize('NFC', name) for name in expected } self.assertEqual(nbnames, expected) - nbs = notebooks_only(self.nb_api.list('ordering').json()) + nbs = notebooks_only(self.api.list('ordering').json()) nbnames = [n['name'] for n in nbs] expected = ['A.ipynb', 'b.ipynb', 'C.ipynb'] self.assertEqual(nbnames, expected) def test_list_dirs(self): - dirs = dirs_only(self.nb_api.list().json()) + dirs = dirs_only(self.api.list().json()) dir_names = {normalize('NFC', d['name']) for d in dirs} self.assertEqual(dir_names, self.top_level_dirs) # Excluding hidden dirs def test_list_nonexistant_dir(self): with assert_http_error(404): - self.nb_api.list('nonexistant') + self.api.list('nonexistant') def test_get_contents(self): for d, name in self.dirs_nbs: - nb = self.nb_api.read('%s.ipynb' % name, d+'/').json() + nb = self.api.read('%s.ipynb' % name, d+'/').json() self.assertEqual(nb['name'], u'%s.ipynb' % name) self.assertIn('content', nb) self.assertIn('metadata', nb['content']) @@ -188,12 +188,12 @@ def test_get_contents(self): # Name that doesn't exist - should be a 404 with assert_http_error(404): - self.nb_api.read('q.ipynb', 'foo') + self.api.read('q.ipynb', 'foo') def _check_nb_created(self, resp, name, path): self.assertEqual(resp.status_code, 201) location_header = py3compat.str_to_unicode(resp.headers['Location']) - self.assertEqual(location_header, url_escape(url_path_join(u'/api/notebooks', path, name))) + self.assertEqual(location_header, url_escape(url_path_join(u'/api/contents', path, name))) self.assertEqual(resp.json()['name'], name) assert os.path.isfile(pjoin( self.notebook_dir.name, @@ -202,28 +202,28 @@ def _check_nb_created(self, resp, name, path): )) def test_create_untitled(self): - resp = self.nb_api.create_untitled(path=u'å b') + resp = self.api.create_untitled(path=u'å b') self._check_nb_created(resp, 'Untitled0.ipynb', u'å b') # Second time - resp = self.nb_api.create_untitled(path=u'å b') + resp = self.api.create_untitled(path=u'å b') self._check_nb_created(resp, 'Untitled1.ipynb', u'å b') # And two directories down - resp = self.nb_api.create_untitled(path='foo/bar') + resp = self.api.create_untitled(path='foo/bar') self._check_nb_created(resp, 'Untitled0.ipynb', 'foo/bar') def test_upload_untitled(self): nb = new_notebook(name='Upload test') nbmodel = {'content': nb} - resp = self.nb_api.upload_untitled(path=u'å b', + resp = self.api.upload_untitled(path=u'å b', body=json.dumps(nbmodel)) self._check_nb_created(resp, 'Untitled0.ipynb', u'å b') def test_upload(self): nb = new_notebook(name=u'ignored') nbmodel = {'content': nb} - resp = self.nb_api.upload(u'Upload tést.ipynb', path=u'å b', + resp = self.api.upload(u'Upload tést.ipynb', path=u'å b', body=json.dumps(nbmodel)) self._check_nb_created(resp, u'Upload tést.ipynb', u'å b') @@ -233,48 +233,48 @@ def test_upload_v2(self): nb.worksheets.append(ws) ws.cells.append(v2.new_code_cell(input='print("hi")')) nbmodel = {'content': nb} - resp = self.nb_api.upload(u'Upload tést.ipynb', path=u'å b', + resp = self.api.upload(u'Upload tést.ipynb', path=u'å b', body=json.dumps(nbmodel)) self._check_nb_created(resp, u'Upload tést.ipynb', u'å b') - resp = self.nb_api.read(u'Upload tést.ipynb', u'å b') + resp = self.api.read(u'Upload tést.ipynb', u'å b') data = resp.json() self.assertEqual(data['content']['nbformat'], current.nbformat) self.assertEqual(data['content']['orig_nbformat'], 2) def test_copy_untitled(self): - resp = self.nb_api.copy_untitled(u'ç d.ipynb', path=u'å b') + resp = self.api.copy_untitled(u'ç d.ipynb', path=u'å b') self._check_nb_created(resp, u'ç d-Copy0.ipynb', u'å b') def test_copy(self): - resp = self.nb_api.copy(u'ç d.ipynb', u'cøpy.ipynb', path=u'å b') + resp = self.api.copy(u'ç d.ipynb', u'cøpy.ipynb', path=u'å b') self._check_nb_created(resp, u'cøpy.ipynb', u'å b') def test_delete(self): for d, name in self.dirs_nbs: - resp = self.nb_api.delete('%s.ipynb' % name, d) + resp = self.api.delete('%s.ipynb' % name, d) self.assertEqual(resp.status_code, 204) for d in self.dirs + ['/']: - nbs = notebooks_only(self.nb_api.list(d).json()) + nbs = notebooks_only(self.api.list(d).json()) self.assertEqual(len(nbs), 0) def test_rename(self): - resp = self.nb_api.rename('a.ipynb', 'foo', 'z.ipynb') + resp = self.api.rename('a.ipynb', 'foo', 'z.ipynb') self.assertEqual(resp.headers['Location'].split('/')[-1], 'z.ipynb') self.assertEqual(resp.json()['name'], 'z.ipynb') assert os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'z.ipynb')) - nbs = notebooks_only(self.nb_api.list('foo').json()) + nbs = notebooks_only(self.api.list('foo').json()) nbnames = set(n['name'] for n in nbs) self.assertIn('z.ipynb', nbnames) self.assertNotIn('a.ipynb', nbnames) def test_rename_existing(self): with assert_http_error(409): - self.nb_api.rename('a.ipynb', 'foo', 'b.ipynb') + self.api.rename('a.ipynb', 'foo', 'b.ipynb') def test_save(self): - resp = self.nb_api.read('a.ipynb', 'foo') + resp = self.api.read('a.ipynb', 'foo') nbcontent = json.loads(resp.text)['content'] nb = to_notebook_json(nbcontent) ws = new_worksheet() @@ -282,32 +282,32 @@ def test_save(self): ws.cells.append(new_heading_cell(u'Created by test ³')) nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb} - resp = self.nb_api.save('a.ipynb', path='foo', body=json.dumps(nbmodel)) + resp = self.api.save('a.ipynb', path='foo', body=json.dumps(nbmodel)) nbfile = pjoin(self.notebook_dir.name, 'foo', 'a.ipynb') with io.open(nbfile, 'r', encoding='utf-8') as f: newnb = read(f, format='ipynb') self.assertEqual(newnb.worksheets[0].cells[0].source, u'Created by test ³') - nbcontent = self.nb_api.read('a.ipynb', 'foo').json()['content'] + nbcontent = self.api.read('a.ipynb', 'foo').json()['content'] newnb = to_notebook_json(nbcontent) self.assertEqual(newnb.worksheets[0].cells[0].source, u'Created by test ³') # Save and rename nbmodel= {'name': 'a2.ipynb', 'path':'foo/bar', 'content': nb} - resp = self.nb_api.save('a.ipynb', path='foo', body=json.dumps(nbmodel)) + resp = self.api.save('a.ipynb', path='foo', body=json.dumps(nbmodel)) saved = resp.json() self.assertEqual(saved['name'], 'a2.ipynb') self.assertEqual(saved['path'], 'foo/bar') assert os.path.isfile(pjoin(self.notebook_dir.name,'foo','bar','a2.ipynb')) assert not os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'a.ipynb')) with assert_http_error(404): - self.nb_api.read('a.ipynb', 'foo') + self.api.read('a.ipynb', 'foo') def test_checkpoints(self): - resp = self.nb_api.read('a.ipynb', 'foo') - r = self.nb_api.new_checkpoint('a.ipynb', 'foo') + resp = self.api.read('a.ipynb', 'foo') + r = self.api.new_checkpoint('a.ipynb', 'foo') self.assertEqual(r.status_code, 201) cp1 = r.json() self.assertEqual(set(cp1), {'id', 'last_modified'}) @@ -322,25 +322,25 @@ def test_checkpoints(self): ws.cells.append(hcell) # Save nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb} - resp = self.nb_api.save('a.ipynb', path='foo', body=json.dumps(nbmodel)) + resp = self.api.save('a.ipynb', path='foo', body=json.dumps(nbmodel)) # List checkpoints - cps = self.nb_api.get_checkpoints('a.ipynb', 'foo').json() + cps = self.api.get_checkpoints('a.ipynb', 'foo').json() self.assertEqual(cps, [cp1]) - nbcontent = self.nb_api.read('a.ipynb', 'foo').json()['content'] + nbcontent = self.api.read('a.ipynb', 'foo').json()['content'] nb = to_notebook_json(nbcontent) self.assertEqual(nb.worksheets[0].cells[0].source, 'Created by test') # Restore cp1 - r = self.nb_api.restore_checkpoint('a.ipynb', 'foo', cp1['id']) + r = self.api.restore_checkpoint('a.ipynb', 'foo', cp1['id']) self.assertEqual(r.status_code, 204) - nbcontent = self.nb_api.read('a.ipynb', 'foo').json()['content'] + nbcontent = self.api.read('a.ipynb', 'foo').json()['content'] nb = to_notebook_json(nbcontent) self.assertEqual(nb.worksheets, []) # Delete cp1 - r = self.nb_api.delete_checkpoint('a.ipynb', 'foo', cp1['id']) + r = self.api.delete_checkpoint('a.ipynb', 'foo', cp1['id']) self.assertEqual(r.status_code, 204) - cps = self.nb_api.get_checkpoints('a.ipynb', 'foo').json() + cps = self.api.get_checkpoints('a.ipynb', 'foo').json() self.assertEqual(cps, []) diff --git a/IPython/html/services/contents/tests/test_nbmanager.py b/IPython/html/services/contents/tests/test_manager.py similarity index 58% rename from IPython/html/services/contents/tests/test_nbmanager.py rename to IPython/html/services/contents/tests/test_manager.py index c4b85b9e5ea..8ad9efa32dd 100644 --- a/IPython/html/services/contents/tests/test_nbmanager.py +++ b/IPython/html/services/contents/tests/test_manager.py @@ -15,59 +15,59 @@ from IPython.utils.traitlets import TraitError from IPython.html.utils import url_path_join -from ..filenbmanager import FileNotebookManager -from ..nbmanager import NotebookManager +from ..filemanager import FileContentsManager +from ..manager import ContentsManager -class TestFileNotebookManager(TestCase): +class TestFileContentsManager(TestCase): - def test_nb_dir(self): + def test_root_dir(self): with TemporaryDirectory() as td: - fm = FileNotebookManager(notebook_dir=td) - self.assertEqual(fm.notebook_dir, td) + fm = FileContentsManager(root_dir=td) + self.assertEqual(fm.root_dir, td) - def test_missing_nb_dir(self): + def test_missing_root_dir(self): with TemporaryDirectory() as td: - nbdir = os.path.join(td, 'notebook', 'dir', 'is', 'missing') - self.assertRaises(TraitError, FileNotebookManager, notebook_dir=nbdir) + root = os.path.join(td, 'notebook', 'dir', 'is', 'missing') + self.assertRaises(TraitError, FileContentsManager, root_dir=root) - def test_invalid_nb_dir(self): + def test_invalid_root_dir(self): with NamedTemporaryFile() as tf: - self.assertRaises(TraitError, FileNotebookManager, notebook_dir=tf.name) + self.assertRaises(TraitError, FileContentsManager, root_dir=tf.name) def test_get_os_path(self): # full filesystem path should be returned with correct operating system # separators. with TemporaryDirectory() as td: - nbdir = td - fm = FileNotebookManager(notebook_dir=nbdir) + root = td + fm = FileContentsManager(root_dir=root) path = fm._get_os_path('test.ipynb', '/path/to/notebook/') rel_path_list = '/path/to/notebook/test.ipynb'.split('/') - fs_path = os.path.join(fm.notebook_dir, *rel_path_list) + fs_path = os.path.join(fm.root_dir, *rel_path_list) self.assertEqual(path, fs_path) - fm = FileNotebookManager(notebook_dir=nbdir) + fm = FileContentsManager(root_dir=root) path = fm._get_os_path('test.ipynb') - fs_path = os.path.join(fm.notebook_dir, 'test.ipynb') + fs_path = os.path.join(fm.root_dir, 'test.ipynb') self.assertEqual(path, fs_path) - fm = FileNotebookManager(notebook_dir=nbdir) + fm = FileContentsManager(root_dir=root) path = fm._get_os_path('test.ipynb', '////') - fs_path = os.path.join(fm.notebook_dir, 'test.ipynb') + fs_path = os.path.join(fm.root_dir, 'test.ipynb') self.assertEqual(path, fs_path) def test_checkpoint_subdir(self): subd = u'sub ∂ir' cp_name = 'test-cp.ipynb' with TemporaryDirectory() as td: - nbdir = td + root = td os.mkdir(os.path.join(td, subd)) - fm = FileNotebookManager(notebook_dir=nbdir) + fm = FileContentsManager(root_dir=root) cp_dir = fm.get_checkpoint_path('cp', 'test.ipynb', '/') cp_subdir = fm.get_checkpoint_path('cp', 'test.ipynb', '/%s/' % subd) self.assertNotEqual(cp_dir, cp_subdir) - self.assertEqual(cp_dir, os.path.join(nbdir, fm.checkpoint_dir, cp_name)) - self.assertEqual(cp_subdir, os.path.join(nbdir, subd, fm.checkpoint_dir, cp_name)) + self.assertEqual(cp_dir, os.path.join(root, fm.checkpoint_dir, cp_name)) + self.assertEqual(cp_subdir, os.path.join(root, subd, fm.checkpoint_dir, cp_name)) class TestNotebookManager(TestCase): @@ -75,8 +75,8 @@ class TestNotebookManager(TestCase): def setUp(self): self._temp_dir = TemporaryDirectory() self.td = self._temp_dir.name - self.notebook_manager = FileNotebookManager( - notebook_dir=self.td, + self.contents_manager = FileContentsManager( + root_dir=self.td, log=logging.getLogger() ) @@ -100,22 +100,22 @@ def add_code_cell(self, nb): nb.worksheets[0].cells.append(cell) def new_notebook(self): - nbm = self.notebook_manager - model = nbm.create_notebook() + cm = self.contents_manager + model = cm.create_notebook() name = model['name'] path = model['path'] - full_model = nbm.get_notebook(name, path) + full_model = cm.get(name, path) nb = full_model['content'] self.add_code_cell(nb) - nbm.save_notebook(full_model, name, path) + cm.save(full_model, name, path) return nb, name, path def test_create_notebook(self): - nm = self.notebook_manager + cm = self.contents_manager # Test in root directory - model = nm.create_notebook() + model = cm.create_notebook() assert isinstance(model, dict) self.assertIn('name', model) self.assertIn('path', model) @@ -124,23 +124,23 @@ def test_create_notebook(self): # Test in sub-directory sub_dir = '/foo/' - self.make_dir(nm.notebook_dir, 'foo') - model = nm.create_notebook(None, sub_dir) + self.make_dir(cm.root_dir, 'foo') + model = cm.create_notebook(None, sub_dir) assert isinstance(model, dict) self.assertIn('name', model) self.assertIn('path', model) self.assertEqual(model['name'], 'Untitled0.ipynb') self.assertEqual(model['path'], sub_dir.strip('/')) - def test_get_notebook(self): - nm = self.notebook_manager + def test_get(self): + cm = self.contents_manager # Create a notebook - model = nm.create_notebook() + model = cm.create_notebook() name = model['name'] path = model['path'] # Check that we 'get' on the notebook we just created - model2 = nm.get_notebook(name, path) + model2 = cm.get(name, path) assert isinstance(model2, dict) self.assertIn('name', model2) self.assertIn('path', model2) @@ -149,9 +149,9 @@ def test_get_notebook(self): # Test in sub-directory sub_dir = '/foo/' - self.make_dir(nm.notebook_dir, 'foo') - model = nm.create_notebook(None, sub_dir) - model2 = nm.get_notebook(name, sub_dir) + self.make_dir(cm.root_dir, 'foo') + model = cm.create_notebook(None, sub_dir) + model2 = cm.get(name, sub_dir) assert isinstance(model2, dict) self.assertIn('name', model2) self.assertIn('path', model2) @@ -159,35 +159,35 @@ def test_get_notebook(self): self.assertEqual(model2['name'], 'Untitled0.ipynb') self.assertEqual(model2['path'], sub_dir.strip('/')) - def test_update_notebook(self): - nm = self.notebook_manager + def test_update(self): + cm = self.contents_manager # Create a notebook - model = nm.create_notebook() + model = cm.create_notebook() name = model['name'] path = model['path'] # Change the name in the model for rename model['name'] = 'test.ipynb' - model = nm.update_notebook(model, name, path) + model = cm.update(model, name, path) assert isinstance(model, dict) self.assertIn('name', model) self.assertIn('path', model) self.assertEqual(model['name'], 'test.ipynb') # Make sure the old name is gone - self.assertRaises(HTTPError, nm.get_notebook, name, path) + self.assertRaises(HTTPError, cm.get, name, path) # Test in sub-directory # Create a directory and notebook in that directory sub_dir = '/foo/' - self.make_dir(nm.notebook_dir, 'foo') - model = nm.create_notebook(None, sub_dir) + self.make_dir(cm.root_dir, 'foo') + model = cm.create_notebook(None, sub_dir) name = model['name'] path = model['path'] # Change the name in the model for rename model['name'] = 'test_in_sub.ipynb' - model = nm.update_notebook(model, name, path) + model = cm.update(model, name, path) assert isinstance(model, dict) self.assertIn('name', model) self.assertIn('path', model) @@ -195,20 +195,20 @@ def test_update_notebook(self): self.assertEqual(model['path'], sub_dir.strip('/')) # Make sure the old name is gone - self.assertRaises(HTTPError, nm.get_notebook, name, path) + self.assertRaises(HTTPError, cm.get, name, path) - def test_save_notebook(self): - nm = self.notebook_manager + def test_save(self): + cm = self.contents_manager # Create a notebook - model = nm.create_notebook() + model = cm.create_notebook() name = model['name'] path = model['path'] # Get the model with 'content' - full_model = nm.get_notebook(name, path) + full_model = cm.get(name, path) # Save the notebook - model = nm.save_notebook(full_model, name, path) + model = cm.save(full_model, name, path) assert isinstance(model, dict) self.assertIn('name', model) self.assertIn('path', model) @@ -218,103 +218,84 @@ def test_save_notebook(self): # Test in sub-directory # Create a directory and notebook in that directory sub_dir = '/foo/' - self.make_dir(nm.notebook_dir, 'foo') - model = nm.create_notebook(None, sub_dir) + self.make_dir(cm.root_dir, 'foo') + model = cm.create_notebook(None, sub_dir) name = model['name'] path = model['path'] - model = nm.get_notebook(name, path) + model = cm.get(name, path) # Change the name in the model for rename - model = nm.save_notebook(model, name, path) + model = cm.save(model, name, path) assert isinstance(model, dict) self.assertIn('name', model) self.assertIn('path', model) self.assertEqual(model['name'], 'Untitled0.ipynb') self.assertEqual(model['path'], sub_dir.strip('/')) - def test_save_notebook_with_script(self): - nm = self.notebook_manager - # Create a notebook - model = nm.create_notebook() - nm.save_script = True - model = nm.create_notebook() - name = model['name'] - path = model['path'] - - # Get the model with 'content' - full_model = nm.get_notebook(name, path) - - # Save the notebook - model = nm.save_notebook(full_model, name, path) - - # Check that the script was created - py_path = os.path.join(nm.notebook_dir, os.path.splitext(name)[0]+'.py') - assert os.path.exists(py_path), py_path - - def test_delete_notebook(self): - nm = self.notebook_manager + def test_delete(self): + cm = self.contents_manager # Create a notebook nb, name, path = self.new_notebook() # Delete the notebook - nm.delete_notebook(name, path) + cm.delete(name, path) # Check that a 'get' on the deleted notebook raises and error - self.assertRaises(HTTPError, nm.get_notebook, name, path) + self.assertRaises(HTTPError, cm.get, name, path) - def test_copy_notebook(self): - nm = self.notebook_manager + def test_copy(self): + cm = self.contents_manager path = u'å b' name = u'nb √.ipynb' - os.mkdir(os.path.join(nm.notebook_dir, path)) - orig = nm.create_notebook({'name' : name}, path=path) + os.mkdir(os.path.join(cm.root_dir, path)) + orig = cm.create_notebook({'name' : name}, path=path) # copy with unspecified name - copy = nm.copy_notebook(name, path=path) + copy = cm.copy(name, path=path) self.assertEqual(copy['name'], orig['name'].replace('.ipynb', '-Copy0.ipynb')) # copy with specified name - copy2 = nm.copy_notebook(name, u'copy 2.ipynb', path=path) + copy2 = cm.copy(name, u'copy 2.ipynb', path=path) self.assertEqual(copy2['name'], u'copy 2.ipynb') def test_trust_notebook(self): - nbm = self.notebook_manager + cm = self.contents_manager nb, name, path = self.new_notebook() - untrusted = nbm.get_notebook(name, path)['content'] - assert not nbm.notary.check_cells(untrusted) + untrusted = cm.get(name, path)['content'] + assert not cm.notary.check_cells(untrusted) # print(untrusted) - nbm.trust_notebook(name, path) - trusted = nbm.get_notebook(name, path)['content'] + cm.trust_notebook(name, path) + trusted = cm.get(name, path)['content'] # print(trusted) - assert nbm.notary.check_cells(trusted) + assert cm.notary.check_cells(trusted) def test_mark_trusted_cells(self): - nbm = self.notebook_manager + cm = self.contents_manager nb, name, path = self.new_notebook() - nbm.mark_trusted_cells(nb, name, path) + cm.mark_trusted_cells(nb, name, path) for cell in nb.worksheets[0].cells: if cell.cell_type == 'code': assert not cell.trusted - nbm.trust_notebook(name, path) - nb = nbm.get_notebook(name, path)['content'] + cm.trust_notebook(name, path) + nb = cm.get(name, path)['content'] for cell in nb.worksheets[0].cells: if cell.cell_type == 'code': assert cell.trusted def test_check_and_sign(self): - nbm = self.notebook_manager + cm = self.contents_manager nb, name, path = self.new_notebook() - nbm.mark_trusted_cells(nb, name, path) - nbm.check_and_sign(nb, name, path) - assert not nbm.notary.check_signature(nb) + cm.mark_trusted_cells(nb, name, path) + cm.check_and_sign(nb, name, path) + assert not cm.notary.check_signature(nb) - nbm.trust_notebook(name, path) - nb = nbm.get_notebook(name, path)['content'] - nbm.mark_trusted_cells(nb, name, path) - nbm.check_and_sign(nb, name, path) - assert nbm.notary.check_signature(nb) + cm.trust_notebook(name, path) + nb = cm.get(name, path)['content'] + cm.mark_trusted_cells(nb, name, path) + cm.check_and_sign(nb, name, path) + assert cm.notary.check_signature(nb) diff --git a/IPython/html/services/sessions/handlers.py b/IPython/html/services/sessions/handlers.py index cd84dc4f47e..691339f0dbf 100644 --- a/IPython/html/services/sessions/handlers.py +++ b/IPython/html/services/sessions/handlers.py @@ -1,20 +1,7 @@ -"""Tornado handlers for the sessions web service. +"""Tornado handlers for the sessions web service.""" -Authors: - -* Zach Sailer -""" - -#----------------------------------------------------------------------------- -# Copyright (C) 2013 The IPython Development Team -# -# Distributed under the terms of the BSD License. The full license is in -# the file COPYING, distributed as part of this software. -#----------------------------------------------------------------------------- - -#----------------------------------------------------------------------------- -# Imports -#----------------------------------------------------------------------------- +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. import json @@ -24,10 +11,6 @@ from IPython.utils.jsonutil import date_default from IPython.html.utils import url_path_join, url_escape -#----------------------------------------------------------------------------- -# Session web service handlers -#----------------------------------------------------------------------------- - class SessionRootHandler(IPythonHandler): @@ -45,6 +28,8 @@ def post(self): # Creates a new session #(unless a session already exists for the named nb) sm = self.session_manager + cm = self.contents_manager + km = self.kernel_manager model = self.get_json_body() if model is None: diff --git a/IPython/html/services/sessions/sessionmanager.py b/IPython/html/services/sessions/sessionmanager.py index cc68028f948..67adbb7c10b 100644 --- a/IPython/html/services/sessions/sessionmanager.py +++ b/IPython/html/services/sessions/sessionmanager.py @@ -32,7 +32,7 @@ class SessionManager(LoggingConfigurable): kernel_manager = Instance('IPython.html.services.kernels.kernelmanager.MappingKernelManager') - notebook_manager = Instance('IPython.html.services.notebooks.nbmanager.NotebookManager', args=()) + contents_manager = Instance('IPython.html.services.contents.manager.ContentsManager', args=()) # Session database initialized below _cursor = None @@ -77,7 +77,7 @@ def create_session(self, name=None, path=None, kernel_name='python'): """Creates a session and returns its model""" session_id = self.new_session_id() # allow nbm to specify kernels cwd - kernel_path = self.notebook_manager.get_kernel_path(name=name, path=path) + kernel_path = self.contents_manager.get_kernel_path(name=name, path=path) kernel_id = self.kernel_manager.start_kernel(path=kernel_path, kernel_name=kernel_name) return self.save_session(session_id, name=name, path=path, diff --git a/IPython/html/static/notebook/js/notebook.js b/IPython/html/static/notebook/js/notebook.js index e01e7ff0857..67e5e8b6fc5 100644 --- a/IPython/html/static/notebook/js/notebook.js +++ b/IPython/html/static/notebook/js/notebook.js @@ -1908,7 +1908,7 @@ define([ this.events.trigger('notebook_saving.Notebook'); var url = utils.url_join_encode( this.base_url, - 'api/notebooks', + 'api/contents', this.notebook_path, this.notebook_name ); @@ -2041,7 +2041,7 @@ define([ }; var url = utils.url_join_encode( base_url, - 'api/notebooks', + 'api/contents', path ); $.ajax(url,settings); @@ -2070,7 +2070,7 @@ define([ }; var url = utils.url_join_encode( base_url, - 'api/notebooks', + 'api/contents', path ); $.ajax(url,settings); @@ -2095,7 +2095,7 @@ define([ this.events.trigger('rename_notebook.Notebook', data); var url = utils.url_join_encode( this.base_url, - 'api/notebooks', + 'api/contents', this.notebook_path, this.notebook_name ); @@ -2113,7 +2113,7 @@ define([ }; var url = utils.url_join_encode( this.base_url, - 'api/notebooks', + 'api/contents', this.notebook_path, this.notebook_name ); @@ -2182,7 +2182,7 @@ define([ this.events.trigger('notebook_loading.Notebook'); var url = utils.url_join_encode( this.base_url, - 'api/notebooks', + 'api/contents', this.notebook_path, this.notebook_name ); @@ -2345,7 +2345,7 @@ define([ Notebook.prototype.list_checkpoints = function () { var url = utils.url_join_encode( this.base_url, - 'api/notebooks', + 'api/contents', this.notebook_path, this.notebook_name, 'checkpoints' @@ -2396,7 +2396,7 @@ define([ Notebook.prototype.create_checkpoint = function () { var url = utils.url_join_encode( this.base_url, - 'api/notebooks', + 'api/contents', this.notebook_path, this.notebook_name, 'checkpoints' @@ -2485,7 +2485,7 @@ define([ this.events.trigger('notebook_restoring.Notebook', checkpoint); var url = utils.url_join_encode( this.base_url, - 'api/notebooks', + 'api/contents', this.notebook_path, this.notebook_name, 'checkpoints', @@ -2533,7 +2533,7 @@ define([ this.events.trigger('notebook_restoring.Notebook', checkpoint); var url = utils.url_join_encode( this.base_url, - 'api/notebooks', + 'api/contents', this.notebook_path, this.notebook_name, 'checkpoints', diff --git a/IPython/html/static/tree/js/notebooklist.js b/IPython/html/static/tree/js/notebooklist.js index 4228e39043c..78272410355 100644 --- a/IPython/html/static/tree/js/notebooklist.js +++ b/IPython/html/static/tree/js/notebooklist.js @@ -148,7 +148,7 @@ define([ var url = utils.url_join_encode( this.base_url, 'api', - 'notebooks', + 'contents', this.notebook_path ); $.ajax(url, settings); @@ -328,7 +328,7 @@ define([ }; var url = utils.url_join_encode( notebooklist.base_url, - 'api/notebooks', + 'api/contents', notebooklist.notebook_path, nbname ); @@ -375,7 +375,7 @@ define([ var url = utils.url_join_encode( that.base_url, - 'api/notebooks', + 'api/contents', that.notebook_path, nbname ); @@ -419,7 +419,7 @@ define([ }; var url = utils.url_join_encode( base_url, - 'api/notebooks', + 'api/contents', path ); $.ajax(url, settings); diff --git a/IPython/html/tests/launchnotebook.py b/IPython/html/tests/launchnotebook.py index 67a1955b729..42ae7686920 100644 --- a/IPython/html/tests/launchnotebook.py +++ b/IPython/html/tests/launchnotebook.py @@ -33,7 +33,7 @@ class NotebookTestBase(TestCase): @classmethod def wait_until_alive(cls): """Wait for the server to be alive""" - url = 'http://localhost:%i/api/notebooks' % cls.port + url = 'http://localhost:%i/api/contents' % cls.port for _ in range(int(MAX_WAITTIME/POLL_INTERVAL)): try: requests.get(url) diff --git a/IPython/html/tree/handlers.py b/IPython/html/tree/handlers.py index 4a4320f7ea3..d1ae0c251a4 100644 --- a/IPython/html/tree/handlers.py +++ b/IPython/html/tree/handlers.py @@ -51,7 +51,7 @@ def generate_page_title(self, path): @web.authenticated def get(self, path='', name=None): path = path.strip('/') - nbm = self.notebook_manager + cm = self.contents_manager if name is not None: # is a notebook, redirect to notebook handler url = url_escape(url_path_join( @@ -60,10 +60,10 @@ def get(self, path='', name=None): self.log.debug("Redirecting %s to %s", self.request.path, url) self.redirect(url) else: - if not nbm.path_exists(path=path): + if not cm.path_exists(path=path): # Directory is hidden or does not exist. raise web.HTTPError(404) - elif nbm.is_hidden(path): + elif cm.is_hidden(path): self.log.info("Refusing to serve hidden directory, via 404 Error") raise web.HTTPError(404) breadcrumbs = self.generate_breadcrumbs(path) From dbd4c509ac72a848d0c5d4c2eae06b111bfd7977 Mon Sep 17 00:00:00 2001 From: MinRK Date: Mon, 2 Jun 2014 13:47:11 -0700 Subject: [PATCH 03/16] teach contents service about non-notebook files --- IPython/html/base/handlers.py | 2 + IPython/html/nbconvert/handlers.py | 7 +- IPython/html/services/contents/filemanager.py | 228 ++++++++++-------- IPython/html/services/contents/handlers.py | 49 ++-- IPython/html/services/contents/manager.py | 26 +- .../contents/tests/test_contents_api.py | 64 ++++- .../services/contents/tests/test_manager.py | 26 +- IPython/html/static/tree/js/notebooklist.js | 9 +- IPython/html/tree/handlers.py | 22 +- 9 files changed, 243 insertions(+), 190 deletions(-) diff --git a/IPython/html/base/handlers.py b/IPython/html/base/handlers.py index 8796e55cbd5..76f9164723d 100644 --- a/IPython/html/base/handlers.py +++ b/IPython/html/base/handlers.py @@ -416,6 +416,8 @@ def get(self): path_regex = r"(?P(?:/.*)*)" notebook_name_regex = r"(?P[^/]+\.ipynb)" notebook_path_regex = "%s/%s" % (path_regex, notebook_name_regex) +file_name_regex = r"(?P[^/]+)" +file_path_regex = "%s/%s" % (path_regex, file_name_regex) #----------------------------------------------------------------------------- # URL to handler mappings diff --git a/IPython/html/nbconvert/handlers.py b/IPython/html/nbconvert/handlers.py index 180e6c67887..93e6bf1d498 100644 --- a/IPython/html/nbconvert/handlers.py +++ b/IPython/html/nbconvert/handlers.py @@ -1,3 +1,8 @@ +"""Tornado handlers for nbconvert.""" + +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. + import io import os import zipfile @@ -73,7 +78,7 @@ def get(self, format, path='', name=None): exporter = get_exporter(format, config=self.config, log=self.log) path = path.strip('/') - model = self.contents_manager.get(name=name, path=path) + model = self.contents_manager.get_model(name=name, path=path) self.set_header('Last-Modified', model['last_modified']) diff --git a/IPython/html/services/contents/filemanager.py b/IPython/html/services/contents/filemanager.py index 2ddca804796..e4a0b599978 100644 --- a/IPython/html/services/contents/filemanager.py +++ b/IPython/html/services/contents/filemanager.py @@ -3,6 +3,7 @@ # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. +import base64 import io import os import glob @@ -56,17 +57,29 @@ def _copy(self, src, dest): except OSError as e: self.log.debug("copystat on %s failed", dest, exc_info=True) - def get_names(self, path=''): - """List all filenames in the path (relative to root_dir).""" - path = path.strip('/') - if not os.path.isdir(self._get_os_path(path=path)): - raise web.HTTPError(404, 'Directory not found: ' + path) - names = glob.glob(self._get_os_path('*', path)) - names = [ os.path.basename(name) for name in names if os.path.isfile(name)] - return names + def _get_os_path(self, name=None, path=''): + """Given a filename and a URL path, return its file system + path. + + Parameters + ---------- + name : string + A filename + path : string + The relative URL path (with '/' as separator) to the named + file. + + Returns + ------- + path : string + API path to be evaluated relative to root_dir. + """ + if name is not None: + path = path + '/' + name + return to_os_path(path, self.root_dir) def path_exists(self, path): - """Does the API-style path (directory) actually exist? + """Does the API-style path refer to an extant directory? Parameters ---------- @@ -102,29 +115,26 @@ def is_hidden(self, path): os_path = self._get_os_path(path=path) return is_hidden(os_path, self.root_dir) - def _get_os_path(self, name=None, path=''): - """Given a filename and a URL path, return its file system - path. + def file_exists(self, name, path=''): + """Returns True if the file exists, else returns False. Parameters ---------- name : string - A filename + The name of the file you are checking. path : string - The relative URL path (with '/' as separator) to the named - file. + The relative path to the file's directory (with '/' as separator) Returns ------- - path : string - API path to be evaluated relative to root_dir. + bool """ - if name is not None: - path = path + '/' + name - return to_os_path(path, self.root_dir) + path = path.strip('/') + nbpath = self._get_os_path(name, path=path) + return os.path.isfile(nbpath) - def file_exists(self, name, path=''): - """Returns a True if the file exists, else returns False. + def exists(self, name=None, path=''): + """Returns True if the path [and name] exists, else returns False. Parameters ---------- @@ -138,83 +148,107 @@ def file_exists(self, name, path=''): bool """ path = path.strip('/') - nbpath = self._get_os_path(name, path=path) - return os.path.isfile(nbpath) + os_path = self._get_os_path(name, path=path) + return os.path.exists(os_path) - # 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): - raise web.HTTPError(404, u'directory does not exist: %r' % os_path) - elif is_hidden(os_path, self.root_dir): - self.log.info("Refusing to serve hidden directory, via 404 Error") - 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.root_dir)\ - and self.should_list(name): - try: - model = self.get_dir_model(name, path) - except IOError: - pass - dirs.append(model) - dirs = sorted(dirs, key=sort_key) - 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('/') + def _base_model(self, name, path=''): + """Build the common base of a contents model""" 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 = {} model['name'] = name model['path'] = path model['last_modified'] = last_modified model['created'] = created + model['content'] = None + model['format'] = None + return model + + def _dir_model(self, name, path='', content=True): + """Build a model for a directory + + if content is requested, will include a listing of the directory + """ + os_path = self._get_os_path(name, path) + + if not os.path.isdir(os_path): + raise web.HTTPError(404, u'directory does not exist: %r' % os_path) + elif is_hidden(os_path, self.root_dir): + self.log.info("Refusing to serve hidden directory, via 404 Error") + raise web.HTTPError(404, u'directory does not exist: %r' % os_path) + + if name is None: + if '/' in path: + path, name = path.rsplit('/', 1) + else: + name = '' + model = self._base_model(name, path) model['type'] = 'directory' + dir_path = u'{}/{}'.format(path, name) + if content: + contents = [] + for os_path in glob.glob(self._get_os_path('*', dir_path)): + name = os.path.basename(os_path) + if self.should_list(name) and not is_hidden(os_path, self.root_dir): + contents.append(self.get_model(name=name, path=dir_path, content=False)) + + model['content'] = sorted(contents, key=sort_key) + return model - def list_files(self, path): - """Returns a list of dictionaries that are the standard model - for all notebooks in the relative 'path'. + def _file_model(self, name, path='', content=True): + """Build a model for a file - Parameters - ---------- - path : str - the URL path that describes the relative path for the - listed notebooks + if content is requested, include the file contents. + Text files will be unicode, binary files will be base64-encoded. + """ + model = self._base_model(name, path) + model['type'] = 'file' + if content: + os_path = self._get_os_path(name, path) + try: + with io.open(os_path, 'r', encoding='utf-8') as f: + model['content'] = f.read() + except UnicodeError as e: + with io.open(os_path, 'rb') as f: + bcontent = f.read() + model['content'] = base64.encodestring(bcontent).decode('ascii') + model['format'] = 'base64' + else: + model['format'] = 'text' + return model - Returns - ------- - notebooks : list of dicts - a list of the notebook models without 'content' + + def _notebook_model(self, name, path='', content=True): + """Build a notebook model + + if content is requested, the notebook content will be populated + as a JSON structure (not double-serialized) """ - path = path.strip('/') - names = self.get_names(path) - notebooks = [self.get(name, path, content=False) - for name in names if self.should_list(name)] - notebooks = sorted(notebooks, key=sort_key) - return notebooks + model = self._base_model(name, path) + model['type'] = 'notebook' + if content: + os_path = self._get_os_path(name, path) + with io.open(os_path, 'r', encoding='utf-8') as f: + try: + nb = current.read(f, u'json') + except Exception as e: + raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e)) + self.mark_trusted_cells(nb, name, path) + model['content'] = nb + model['format'] = 'json' + return model - def get(self, name, path='', content=True): - """ Takes a path and name for a notebook and returns its model + def get_model(self, name, path='', content=True): + """ Takes a path and name for an entity and returns its model Parameters ---------- name : str - the name of the notebook + the name of the target path : str the URL path that describes the relative path for the notebook @@ -222,31 +256,21 @@ def get(self, name, path='', content=True): Returns ------- model : dict - the notebook model. If contents=True, returns the 'contents' - dict in the model as well. + the contents model. If content=True, returns the contents + of the file or directory as well. """ path = path.strip('/') - if not self.file_exists(name=name, path=path): - raise web.HTTPError(404, u'Notebook does not exist: %s' % name) + + if not self.exists(name=name, path=path): + raise web.HTTPError(404, u'No such file or directory: %s/%s' % (path, name)) + os_path = self._get_os_path(name, 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'] = 'notebook' - if content: - with io.open(os_path, 'r', encoding='utf-8') as f: - try: - nb = current.read(f, u'json') - except Exception as e: - raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e)) - self.mark_trusted_cells(nb, name, path) - model['content'] = nb + if os.path.isdir(os_path): + model = self._dir_model(name, path, content) + elif name.endswith('.ipynb'): + model = self._notebook_model(name, path, content) + else: + model = self._file_model(name, path, content) return model def save(self, model, name='', path=''): @@ -281,7 +305,7 @@ def save(self, model, name='', path=''): except Exception as e: raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s %s' % (os_path, e)) - model = self.get(new_name, new_path, content=False) + model = self.get_model(new_name, new_path, content=False) return model def update(self, model, name, path=''): @@ -291,7 +315,7 @@ def update(self, model, name, path=''): new_path = model.get('path', path).strip('/') if path != new_path or name != new_name: self.rename(name, path, new_name, new_path) - model = self.get(new_name, new_path, content=False) + model = self.get_model(new_name, new_path, content=False) return model def delete(self, name, path=''): diff --git a/IPython/html/services/contents/handlers.py b/IPython/html/services/contents/handlers.py index 878b8e71617..e6495259f28 100644 --- a/IPython/html/services/contents/handlers.py +++ b/IPython/html/services/contents/handlers.py @@ -11,15 +11,15 @@ from IPython.utils.jsonutil import date_default from IPython.html.base.handlers import (IPythonHandler, json_errors, - notebook_path_regex, path_regex, - notebook_name_regex) + file_path_regex, path_regex, + file_name_regex) class ContentsHandler(IPythonHandler): SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE') - def location_url(self, name, path=''): + def location_url(self, name, path): """Return the full URL location of a file. Parameters @@ -49,25 +49,19 @@ def get(self, path='', name=None): * GET with path and no filename lists files in a directory * GET with path and filename returns file contents model """ - cm = self.contents_manager - # Check to see if a filename was given - if name is None: - # 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 = cm.list_dirs(path) + path = path or '' + model = self.contents_manager.get_model(name=name, path=path) + if model['type'] == 'directory': + # resort listing to group directories at the top + dirs = [] files = [] - index = [] - for nb in cm.list_files(path): - if nb['name'].lower() == 'index.ipynb': - index.append(nb) + for entry in model['content']: + if entry['type'] == 'directory': + dirs.append(entry) else: - files.append(nb) - files = index + dirs + files - self.finish(json.dumps(files, default=date_default)) - return - # get and return notebook representation - model = cm.get(name, path) + # do we also want to group notebooks separate from files? + files.append(entry) + model['content'] = dirs + files self._finish_model(model, location=False) @web.authenticated @@ -148,8 +142,16 @@ def post(self, path='', name=None): """ if name is not None: + path = u'{}/{}'.format(path, name) + + cm = self.contents_manager + + if cm.file_exists(path): raise web.HTTPError(400, "Only POST to directories. Use PUT for full names.") + if not cm.path_exists(path): + raise web.HTTPError(404, "No such directory: %s" % path) + model = self.get_json_body() if model is not None: @@ -200,6 +202,7 @@ def put(self, path='', name=None): def delete(self, path='', name=None): """delete a file in the given path""" cm = self.contents_manager + self.log.warn('delete %s:%s', path, name) cm.delete(name, path) self.set_status(204) self.finish() @@ -262,9 +265,9 @@ def delete(self, path, name, checkpoint_id): _checkpoint_id_regex = r"(?P[\w-]+)" default_handlers = [ - (r"/api/contents%s/checkpoints" % notebook_path_regex, CheckpointsHandler), - (r"/api/contents%s/checkpoints/%s" % (notebook_path_regex, _checkpoint_id_regex), + (r"/api/contents%s/checkpoints" % file_path_regex, CheckpointsHandler), + (r"/api/contents%s/checkpoints/%s" % (file_path_regex, _checkpoint_id_regex), ModifyCheckpointsHandler), - (r"/api/contents%s" % notebook_path_regex, ContentsHandler), + (r"/api/contents%s" % file_path_regex, ContentsHandler), (r"/api/contents%s" % path_regex, ContentsHandler), ] diff --git a/IPython/html/services/contents/manager.py b/IPython/html/services/contents/manager.py index 32f3677103d..dff77b52e9f 100644 --- a/IPython/html/services/contents/manager.py +++ b/IPython/html/services/contents/manager.py @@ -75,27 +75,7 @@ def file_exists(self, name, 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 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_files(self, path=''): + def list(self, path=''): """Return a list of contents dicts without content. This returns a list of dicts @@ -196,7 +176,7 @@ def copy(self, from_name, to_name=None, path=''): If to_name not specified, increment `from_name-Copy#.ipynb`. """ path = path.strip('/') - model = self.get(from_name, path) + model = self.get_model(from_name, path) if not to_name: base, ext = os.path.splitext(from_name) copy_name = u'{0}-Copy{1}'.format(base, ext) @@ -218,7 +198,7 @@ def trust_notebook(self, name, path=''): path : string The notebook's directory """ - model = self.get(name, path) + model = self.get_model(name, path) nb = model['content'] self.log.warn("Trusting notebook %s/%s", path, name) self.notary.mark_cells(nb, True) diff --git a/IPython/html/services/contents/tests/test_contents_api.py b/IPython/html/services/contents/tests/test_contents_api.py index 256b234ae44..5381b82a27a 100644 --- a/IPython/html/services/contents/tests/test_contents_api.py +++ b/IPython/html/services/contents/tests/test_contents_api.py @@ -1,6 +1,7 @@ # coding: utf-8 """Test the contents webservice API.""" +import base64 import io import json import os @@ -23,11 +24,11 @@ # 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'] +def notebooks_only(dir_model): + return [nb for nb in dir_model['content'] if nb['type']=='notebook'] -def dirs_only(nb_list): - return [x for x in nb_list if x['type']=='directory'] +def dirs_only(dir_model): + return [x for x in dir_model['content'] if x['type']=='directory'] class API(object): @@ -112,8 +113,20 @@ class APITest(NotebookTestBase): del dirs[0] # remove '' top_level_dirs = {normalize('NFC', d.split('/')[0]) for d in dirs} + @staticmethod + def _blob_for_name(name): + return name.encode('utf-8') + b'\xFF' + + @staticmethod + def _txt_for_name(name): + return u'%s text file' % name + def setUp(self): nbdir = self.notebook_dir.name + self.blob = os.urandom(100) + self.b64_blob = base64.encodestring(self.blob).decode('ascii') + + for d in (self.dirs + self.hidden_dirs): d.replace('/', os.sep) @@ -122,11 +135,21 @@ def setUp(self): for d, name in self.dirs_nbs: d = d.replace('/', os.sep) + # create a notebook with io.open(pjoin(nbdir, d, '%s.ipynb' % name), 'w', encoding='utf-8') as f: nb = new_notebook(name=name) write(nb, f, format='ipynb') + # create a text file + with io.open(pjoin(nbdir, d, '%s.txt' % name), 'w', + encoding='utf-8') as f: + f.write(self._txt_for_name(name)) + + # create a binary file + with io.open(pjoin(nbdir, d, '%s.blob' % name), 'wb') as f: + f.write(self._blob_for_name(name)) + self.api = API(self.base_url()) def tearDown(self): @@ -178,18 +201,49 @@ def test_list_nonexistant_dir(self): with assert_http_error(404): self.api.list('nonexistant') - def test_get_contents(self): + def test_get_nb_contents(self): for d, name in self.dirs_nbs: nb = self.api.read('%s.ipynb' % name, d+'/').json() self.assertEqual(nb['name'], u'%s.ipynb' % name) + self.assertEqual(nb['type'], 'notebook') + self.assertIn('content', nb) + self.assertEqual(nb['format'], 'json') self.assertIn('content', nb) self.assertIn('metadata', nb['content']) self.assertIsInstance(nb['content']['metadata'], dict) + def test_get_contents_no_such_file(self): # Name that doesn't exist - should be a 404 with assert_http_error(404): self.api.read('q.ipynb', 'foo') + def test_get_text_file_contents(self): + for d, name in self.dirs_nbs: + model = self.api.read(u'%s.txt' % name, d+'/').json() + self.assertEqual(model['name'], u'%s.txt' % name) + self.assertIn('content', model) + self.assertEqual(model['format'], 'text') + self.assertEqual(model['type'], 'file') + self.assertEqual(model['content'], self._txt_for_name(name)) + + # Name that doesn't exist - should be a 404 + with assert_http_error(404): + self.api.read('q.txt', 'foo') + + def test_get_binary_file_contents(self): + for d, name in self.dirs_nbs: + model = self.api.read(u'%s.blob' % name, d+'/').json() + self.assertEqual(model['name'], u'%s.blob' % name) + self.assertIn('content', model) + self.assertEqual(model['format'], 'base64') + self.assertEqual(model['type'], 'file') + b64_data = base64.encodestring(self._blob_for_name(name)).decode('ascii') + self.assertEqual(model['content'], b64_data) + + # Name that doesn't exist - should be a 404 + with assert_http_error(404): + self.api.read('q.txt', 'foo') + def _check_nb_created(self, resp, name, path): self.assertEqual(resp.status_code, 201) location_header = py3compat.str_to_unicode(resp.headers['Location']) diff --git a/IPython/html/services/contents/tests/test_manager.py b/IPython/html/services/contents/tests/test_manager.py index 8ad9efa32dd..44f7e3303fa 100644 --- a/IPython/html/services/contents/tests/test_manager.py +++ b/IPython/html/services/contents/tests/test_manager.py @@ -70,7 +70,7 @@ def test_checkpoint_subdir(self): self.assertEqual(cp_subdir, os.path.join(root, subd, fm.checkpoint_dir, cp_name)) -class TestNotebookManager(TestCase): +class TestContentsManager(TestCase): def setUp(self): self._temp_dir = TemporaryDirectory() @@ -105,7 +105,7 @@ def new_notebook(self): name = model['name'] path = model['path'] - full_model = cm.get(name, path) + full_model = cm.get_model(name, path) nb = full_model['content'] self.add_code_cell(nb) @@ -140,7 +140,7 @@ def test_get(self): path = model['path'] # Check that we 'get' on the notebook we just created - model2 = cm.get(name, path) + model2 = cm.get_model(name, path) assert isinstance(model2, dict) self.assertIn('name', model2) self.assertIn('path', model2) @@ -151,7 +151,7 @@ def test_get(self): sub_dir = '/foo/' self.make_dir(cm.root_dir, 'foo') model = cm.create_notebook(None, sub_dir) - model2 = cm.get(name, sub_dir) + model2 = cm.get_model(name, sub_dir) assert isinstance(model2, dict) self.assertIn('name', model2) self.assertIn('path', model2) @@ -175,7 +175,7 @@ def test_update(self): self.assertEqual(model['name'], 'test.ipynb') # Make sure the old name is gone - self.assertRaises(HTTPError, cm.get, name, path) + self.assertRaises(HTTPError, cm.get_model, name, path) # Test in sub-directory # Create a directory and notebook in that directory @@ -195,7 +195,7 @@ def test_update(self): self.assertEqual(model['path'], sub_dir.strip('/')) # Make sure the old name is gone - self.assertRaises(HTTPError, cm.get, name, path) + self.assertRaises(HTTPError, cm.get_model, name, path) def test_save(self): cm = self.contents_manager @@ -205,7 +205,7 @@ def test_save(self): path = model['path'] # Get the model with 'content' - full_model = cm.get(name, path) + full_model = cm.get_model(name, path) # Save the notebook model = cm.save(full_model, name, path) @@ -222,7 +222,7 @@ def test_save(self): model = cm.create_notebook(None, sub_dir) name = model['name'] path = model['path'] - model = cm.get(name, path) + model = cm.get_model(name, path) # Change the name in the model for rename model = cm.save(model, name, path) @@ -241,7 +241,7 @@ def test_delete(self): cm.delete(name, path) # Check that a 'get' on the deleted notebook raises and error - self.assertRaises(HTTPError, cm.get, name, path) + self.assertRaises(HTTPError, cm.get_model, name, path) def test_copy(self): cm = self.contents_manager @@ -262,12 +262,12 @@ def test_trust_notebook(self): cm = self.contents_manager nb, name, path = self.new_notebook() - untrusted = cm.get(name, path)['content'] + untrusted = cm.get_model(name, path)['content'] assert not cm.notary.check_cells(untrusted) # print(untrusted) cm.trust_notebook(name, path) - trusted = cm.get(name, path)['content'] + trusted = cm.get_model(name, path)['content'] # print(trusted) assert cm.notary.check_cells(trusted) @@ -281,7 +281,7 @@ def test_mark_trusted_cells(self): assert not cell.trusted cm.trust_notebook(name, path) - nb = cm.get(name, path)['content'] + nb = cm.get_model(name, path)['content'] for cell in nb.worksheets[0].cells: if cell.cell_type == 'code': assert cell.trusted @@ -295,7 +295,7 @@ def test_check_and_sign(self): assert not cm.notary.check_signature(nb) cm.trust_notebook(name, path) - nb = cm.get(name, path)['content'] + nb = cm.get_model(name, path)['content'] cm.mark_trusted_cells(nb, name, path) cm.check_and_sign(nb, name, path) assert cm.notary.check_signature(nb) diff --git a/IPython/html/static/tree/js/notebooklist.js b/IPython/html/static/tree/js/notebooklist.js index 78272410355..0be114afcf5 100644 --- a/IPython/html/static/tree/js/notebooklist.js +++ b/IPython/html/static/tree/js/notebooklist.js @@ -161,7 +161,8 @@ define([ message = param.msg; } var item = null; - var len = data.length; + var content = data.content; + var len = content.length; this.clear_list(); if (len === 0) { item = this.new_notebook_item(0); @@ -177,12 +178,12 @@ define([ offset = 1; } for (var i=0; i Date: Mon, 2 Jun 2014 20:07:28 -0700 Subject: [PATCH 04/16] teach tree view about non-notebook files --- IPython/html/static/style/style.min.css | 16 +++ IPython/html/static/tree/js/notebooklist.js | 103 ++++++++++---------- IPython/html/static/tree/less/tree.less | 4 + 3 files changed, 74 insertions(+), 49 deletions(-) diff --git a/IPython/html/static/style/style.min.css b/IPython/html/static/style/style.min.css index f97d4aeab98..65c8bc0938f 100644 --- a/IPython/html/static/style/style.min.css +++ b/IPython/html/static/style/style.min.css @@ -7955,6 +7955,22 @@ input.engine_num_input { .notebook_icon:before.pull-right { margin-left: .3em; } +.file_icon:before { + display: inline-block; + font-family: FontAwesome; + font-style: normal; + font-weight: normal; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + content: "\f016"; +} +.file_icon:before.pull-left { + margin-right: .3em; +} +.file_icon:before.pull-right { + margin-left: .3em; +} /*! * * IPython notebook diff --git a/IPython/html/static/tree/js/notebooklist.js b/IPython/html/static/tree/js/notebooklist.js index 0be114afcf5..c973bd8177f 100644 --- a/IPython/html/static/tree/js/notebooklist.js +++ b/IPython/html/static/tree/js/notebooklist.js @@ -161,11 +161,12 @@ define([ message = param.msg; } var item = null; - var content = data.content; - var len = content.length; + var model = null; + var list = data.content; + var len = list.length; this.clear_list(); if (len === 0) { - item = this.new_notebook_item(0); + item = this.new_item(0); var span12 = item.children().first(); span12.empty(); span12.append($('
').text(message)); @@ -173,31 +174,24 @@ define([ var path = this.notebook_path; var offset = 0; if (path !== '') { - item = this.new_notebook_item(0); - this.add_dir(path, '..', item); + item = this.new_item(0); + model = { + type: 'directory', + name: '..', + path: path, + }; + this.add_link(model, item); offset = 1; } for (var i=0; i').addClass("list_item").addClass("row"); // item.addClass('list_item ui-widget ui-widget-content ui-helper-clearfix'); // item.css('border-top-style','none'); @@ -220,47 +214,57 @@ define([ }; - NotebookList.prototype.add_dir = function (path, name, item) { + NotebookList.icons = { + directory: 'folder_icon', + notebook: 'notebook_icon', + file: 'file_icon', + }; + + NotebookList.uri_prefixes = { + directory: 'tree', + notebook: 'notebooks', + file: 'files', + }; + + + NotebookList.prototype.add_link = function (model, item) { + var path = model.path, + name = model.name; item.data('name', name); item.data('path', path); item.find(".item_name").text(name); - item.find(".item_icon").addClass('folder_icon').addClass('icon-fixed-width'); + var icon = NotebookList.icons[model.type]; + var uri_prefix = NotebookList.uri_prefixes[model.type]; + item.find(".item_icon").addClass(icon).addClass('icon-fixed-width'); item.find("a.item_link") .attr('href', utils.url_join_encode( this.base_url, - "tree", + uri_prefix, path, name ) ); + var path_name = utils.url_path_join(path, name); + if (model.type == 'file') { + this.add_delete_button(item); + } else if (model.type == 'notebook') { + if(this.sessions[path_name] === undefined){ + this.add_delete_button(item); + } else { + this.add_shutdown_button(item, this.sessions[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('notebook_icon').addClass('icon-fixed-width'); - item.find("a.item_link") - .attr('href', - utils.url_join_encode( - this.base_url, - "notebooks", - path, - nbname - ) - ).attr('target','_blank'); - }; - - - NotebookList.prototype.add_name_input = function (nbname, item) { - item.data('nbname', nbname); + NotebookList.prototype.add_name_input = function (name, item) { + item.data('name', name); item.find(".item_icon").addClass('notebook_icon').addClass('icon-fixed-width'); item.find(".item_name").empty().append( $('') .addClass("nbname_input") - .attr('value', utils.splitext(nbname)[0]) + .attr('value', utils.splitext(name)[0]) .attr('size', '30') .attr('type', 'text') ); @@ -308,10 +312,10 @@ define([ // We use the nbname and notebook_id from the parent notebook_item element's // data because the outer scopes values change as we iterate through the loop. var parent_item = that.parents('div.list_item'); - var nbname = parent_item.data('nbname'); - var message = 'Are you sure you want to permanently delete the notebook: ' + nbname + '?'; + var name = parent_item.data('name'); + var message = 'Are you sure you want to permanently delete the file: ' + name + '?'; dialog.modal({ - title : "Delete notebook", + title : "Delete file", body : message, buttons : { Delete : { @@ -331,7 +335,7 @@ define([ notebooklist.base_url, 'api/contents', notebooklist.notebook_path, - nbname + name ); $.ajax(url, settings); } @@ -442,7 +446,8 @@ define([ }); }; - // Backwards compatability. + + // Backwards compatability. IPython.NotebookList = NotebookList; return {'NotebookList': NotebookList}; diff --git a/IPython/html/static/tree/less/tree.less b/IPython/html/static/tree/less/tree.less index 2364a6db49b..fe13748851a 100644 --- a/IPython/html/static/tree/less/tree.less +++ b/IPython/html/static/tree/less/tree.less @@ -147,3 +147,7 @@ input.engine_num_input { .notebook_icon:before { .icon(@fa-var-book) } + +.file_icon:before { + .icon(@fa-var-file-o) +} From f8d34e13b8013c868b3003dc560672c8628146b8 Mon Sep 17 00:00:00 2001 From: MinRK Date: Tue, 3 Jun 2014 11:35:45 -0700 Subject: [PATCH 05/16] add support and tests for uploading and saving regular files --- IPython/html/services/contents/filemanager.py | 64 ++++++++++++++---- IPython/html/services/contents/handlers.py | 26 ++++---- IPython/html/services/contents/manager.py | 15 +++-- .../contents/tests/test_contents_api.py | 65 ++++++++++++++++--- .../services/contents/tests/test_manager.py | 22 +++---- IPython/html/static/notebook/js/notebook.js | 2 + 6 files changed, 147 insertions(+), 47 deletions(-) diff --git a/IPython/html/services/contents/filemanager.py b/IPython/html/services/contents/filemanager.py index e4a0b599978..e9fab0b16ef 100644 --- a/IPython/html/services/contents/filemanager.py +++ b/IPython/html/services/contents/filemanager.py @@ -196,6 +196,7 @@ def _dir_model(self, name, path='', content=True): contents.append(self.get_model(name=name, path=dir_path, content=False)) model['content'] = sorted(contents, key=sort_key) + model['format'] = 'json' return model @@ -273,12 +274,48 @@ def get_model(self, name, path='', content=True): model = self._file_model(name, path, content) return model + def _save_notebook(self, os_path, model, name='', path=''): + # Save the notebook file + nb = current.to_notebook_json(model['content']) + + self.check_and_sign(nb, name, path) + + if 'name' in nb['metadata']: + nb['metadata']['name'] = u'' + + with io.open(os_path, 'w', encoding='utf-8') as f: + current.write(nb, f, u'json') + + def _save_file(self, os_path, model, name='', path=''): + fmt = model.get('format', None) + if fmt not in {'text', 'base64'}: + raise web.HTTPError(400, "Must specify format of file contents as 'text' or 'base64'") + try: + content = model['content'] + if fmt == 'text': + bcontent = content.encode('utf8') + else: + b64_bytes = content.encode('ascii') + bcontent = base64.decodestring(b64_bytes) + except Exception as e: + raise web.HTTPError(400, u'Encoding error saving %s: %s' % (os_path, e)) + with io.open(os_path, 'wb') as f: + f.write(bcontent) + + def _save_directory(self, os_path, model, name='', path=''): + if not os.path.exists(os_path): + os.mkdir(os_path) + elif not os.path.isdir(os_path): + raise web.HTTPError(400, u'Not a directory: %s' % (os_path)) + def save(self, model, name='', path=''): - """Save the notebook model and return the model with no content.""" + """Save the file model and return the model with no content.""" path = path.strip('/') if 'content' not in model: - raise web.HTTPError(400, u'No notebook JSON data provided') + raise web.HTTPError(400, u'No file content provided') + if 'type' not in model: + raise web.HTTPError(400, u'No file type provided') # One checkpoint should always exist if self.file_exists(name, path) and not self.list_checkpoints(name, path): @@ -290,20 +327,21 @@ def save(self, model, name='', path=''): if path != new_path or name != new_name: self.rename(name, path, new_name, new_path) - # Save the notebook file os_path = self._get_os_path(new_name, new_path) - nb = current.to_notebook_json(model['content']) - - self.check_and_sign(nb, new_name, new_path) - - if 'name' in nb['metadata']: - nb['metadata']['name'] = u'' + self.log.debug("Saving %s", os_path) try: - self.log.debug("Autosaving notebook %s", os_path) - with io.open(os_path, 'w', encoding='utf-8') as f: - current.write(nb, f, u'json') + if model['type'] == 'notebook': + self._save_notebook(os_path, model, new_name, new_path) + elif model['type'] == 'file': + self._save_file(os_path, model, new_name, new_path) + elif model['type'] == 'directory': + self._save_directory(os_path, model, new_name, new_path) + else: + raise web.HTTPError(400, "Unhandled contents type: %s" % model['type']) + except web.HTTPError: + raise except Exception as e: - raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s %s' % (os_path, e)) + raise web.HTTPError(400, u'Unexpected error while saving file: %s %s' % (os_path, e)) model = self.get_model(new_name, new_path, content=False) return model diff --git a/IPython/html/services/contents/handlers.py b/IPython/html/services/contents/handlers.py index e6495259f28..e6f08ed2567 100644 --- a/IPython/html/services/contents/handlers.py +++ b/IPython/html/services/contents/handlers.py @@ -99,20 +99,20 @@ def _upload(self, model, path, name=None): if name: model['name'] = name - model = self.contents_manager.create_notebook(model, path) + model = self.contents_manager.create_file(model, path) self.set_status(201) self._finish_model(model) - def _create_empty_notebook(self, path, name=None): - """Create an empty notebook in path + def _create_empty_file(self, path, name=None, ext='.ipynb'): + """Create an empty file in path If name specified, create it in path/name. """ - self.log.info(u"Creating new notebook in %s/%s", path, name or '') + self.log.info(u"Creating new file in %s/%s", path, name or '') model = {} if name: model['name'] = name - model = self.contents_manager.create_notebook(model, path=path) + model = self.contents_manager.create_file(model, path=path, ext=ext) self.set_status(201) self._finish_model(model) @@ -137,7 +137,8 @@ def post(self, path='', name=None): POST /api/contents/path New untitled notebook in path. If content specified, upload a notebook, otherwise start empty. - POST /api/contents/path?copy=OtherNotebook.ipynb + POST /api/contents/path + with body {"copy_from" : "OtherNotebook.ipynb"} New copy of OtherNotebook in path """ @@ -156,14 +157,17 @@ def post(self, path='', name=None): if model is not None: copy_from = model.get('copy_from') - if copy_from: - if model.get('content'): + ext = model.get('ext', '.ipynb') + if model.get('content') is not None: + if copy_from: raise web.HTTPError(400, "Can't upload and copy at the same time.") + self._upload(model, path) + elif copy_from: self._copy(copy_from, path) else: - self._upload(model, path) + self._create_empty_file(path, ext=ext) else: - self._create_empty_notebook(path) + self._create_empty_file(path) @web.authenticated @json_errors @@ -195,7 +199,7 @@ def put(self, path='', name=None): else: self._upload(model, path, name) else: - self._create_empty_notebook(path, name) + self._create_empty_file(path, name) @web.authenticated @json_errors diff --git a/IPython/html/services/contents/manager.py b/IPython/html/services/contents/manager.py index dff77b52e9f..8cec3983c17 100644 --- a/IPython/html/services/contents/manager.py +++ b/IPython/html/services/contents/manager.py @@ -155,16 +155,23 @@ def increment_filename(self, filename, path=''): break return name - def create_notebook(self, model=None, path=''): + def create_file(self, model=None, path='', ext='.ipynb'): """Create a new notebook and return its model with no content.""" path = path.strip('/') if model is None: model = {} if 'content' not in model: - metadata = current.new_metadata(name=u'') - model['content'] = current.new_notebook(metadata=metadata) + if ext == '.ipynb': + metadata = current.new_metadata(name=u'') + model['content'] = current.new_notebook(metadata=metadata) + model.setdefault('type', 'notebook') + model.setdefault('format', 'json') + else: + model['content'] = '' + model.setdefault('type', 'file') + model.setdefault('format', 'text') if 'name' not in model: - model['name'] = self.increment_filename('Untitled.ipynb', path) + model['name'] = self.increment_filename('Untitled' + ext, path) model['path'] = path model = self.save(model, model['name'], model['path']) diff --git a/IPython/html/services/contents/tests/test_contents_api.py b/IPython/html/services/contents/tests/test_contents_api.py index 5381b82a27a..d0e3e20d7a9 100644 --- a/IPython/html/services/contents/tests/test_contents_api.py +++ b/IPython/html/services/contents/tests/test_contents_api.py @@ -50,8 +50,11 @@ def list(self, path='/'): def read(self, name, path='/'): return self._req('GET', url_path_join(path, name)) - def create_untitled(self, path='/'): - return self._req('POST', path) + def create_untitled(self, path='/', ext=None): + body = None + if ext: + body = json.dumps({'ext': ext}) + return self._req('POST', path, body) def upload_untitled(self, body, path='/'): return self._req('POST', path, body) @@ -267,26 +270,72 @@ def test_create_untitled(self): resp = self.api.create_untitled(path='foo/bar') self._check_nb_created(resp, 'Untitled0.ipynb', 'foo/bar') + def test_create_untitled_txt(self): + resp = self.api.create_untitled(path='foo/bar', ext='.txt') + self._check_nb_created(resp, 'Untitled0.txt', 'foo/bar') + + resp = self.api.read(path='foo/bar', name='Untitled0.txt') + model = resp.json() + self.assertEqual(model['type'], 'file') + self.assertEqual(model['format'], 'text') + self.assertEqual(model['content'], '') + def test_upload_untitled(self): nb = new_notebook(name='Upload test') - nbmodel = {'content': nb} + nbmodel = {'content': nb, 'type': 'notebook'} resp = self.api.upload_untitled(path=u'å b', body=json.dumps(nbmodel)) self._check_nb_created(resp, 'Untitled0.ipynb', u'å b') def test_upload(self): nb = new_notebook(name=u'ignored') - nbmodel = {'content': nb} + nbmodel = {'content': nb, 'type': 'notebook'} resp = self.api.upload(u'Upload tést.ipynb', path=u'å b', body=json.dumps(nbmodel)) self._check_nb_created(resp, u'Upload tést.ipynb', u'å b') + def test_upload_txt(self): + body = u'ünicode téxt' + model = { + 'content' : body, + 'format' : 'text', + 'type' : 'file', + } + resp = self.api.upload(u'Upload tést.txt', path=u'å b', + body=json.dumps(model)) + + # check roundtrip + resp = self.api.read(path=u'å b', name=u'Upload tést.txt') + model = resp.json() + self.assertEqual(model['type'], 'file') + self.assertEqual(model['format'], 'text') + self.assertEqual(model['content'], body) + + def test_upload_b64(self): + body = b'\xFFblob' + b64body = base64.encodestring(body).decode('ascii') + model = { + 'content' : b64body, + 'format' : 'base64', + 'type' : 'file', + } + resp = self.api.upload(u'Upload tést.blob', path=u'å b', + body=json.dumps(model)) + + # check roundtrip + resp = self.api.read(path=u'å b', name=u'Upload tést.blob') + model = resp.json() + self.assertEqual(model['type'], 'file') + self.assertEqual(model['format'], 'base64') + decoded = base64.decodestring(model['content'].encode('ascii')) + self.assertEqual(decoded, body) + def test_upload_v2(self): nb = v2.new_notebook() ws = v2.new_worksheet() nb.worksheets.append(ws) ws.cells.append(v2.new_code_cell(input='print("hi")')) - nbmodel = {'content': nb} + nbmodel = {'content': nb, 'type': 'notebook'} resp = self.api.upload(u'Upload tést.ipynb', path=u'å b', body=json.dumps(nbmodel)) self._check_nb_created(resp, u'Upload tést.ipynb', u'å b') @@ -335,7 +384,7 @@ def test_save(self): nb.worksheets = [ws] ws.cells.append(new_heading_cell(u'Created by test ³')) - nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb} + nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb, 'type': 'notebook'} resp = self.api.save('a.ipynb', path='foo', body=json.dumps(nbmodel)) nbfile = pjoin(self.notebook_dir.name, 'foo', 'a.ipynb') @@ -349,7 +398,7 @@ def test_save(self): u'Created by test ³') # Save and rename - nbmodel= {'name': 'a2.ipynb', 'path':'foo/bar', 'content': nb} + nbmodel= {'name': 'a2.ipynb', 'path':'foo/bar', 'content': nb, 'type': 'notebook'} resp = self.api.save('a.ipynb', path='foo', body=json.dumps(nbmodel)) saved = resp.json() self.assertEqual(saved['name'], 'a2.ipynb') @@ -375,7 +424,7 @@ def test_checkpoints(self): hcell = new_heading_cell('Created by test') ws.cells.append(hcell) # Save - nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb} + nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb, 'type': 'notebook'} resp = self.api.save('a.ipynb', path='foo', body=json.dumps(nbmodel)) # List checkpoints diff --git a/IPython/html/services/contents/tests/test_manager.py b/IPython/html/services/contents/tests/test_manager.py index 44f7e3303fa..e58a895fad7 100644 --- a/IPython/html/services/contents/tests/test_manager.py +++ b/IPython/html/services/contents/tests/test_manager.py @@ -101,7 +101,7 @@ def add_code_cell(self, nb): def new_notebook(self): cm = self.contents_manager - model = cm.create_notebook() + model = cm.create_file() name = model['name'] path = model['path'] @@ -112,10 +112,10 @@ def new_notebook(self): cm.save(full_model, name, path) return nb, name, path - def test_create_notebook(self): + def test_create_file(self): cm = self.contents_manager # Test in root directory - model = cm.create_notebook() + model = cm.create_file() assert isinstance(model, dict) self.assertIn('name', model) self.assertIn('path', model) @@ -125,7 +125,7 @@ def test_create_notebook(self): # Test in sub-directory sub_dir = '/foo/' self.make_dir(cm.root_dir, 'foo') - model = cm.create_notebook(None, sub_dir) + model = cm.create_file(None, sub_dir) assert isinstance(model, dict) self.assertIn('name', model) self.assertIn('path', model) @@ -135,7 +135,7 @@ def test_create_notebook(self): def test_get(self): cm = self.contents_manager # Create a notebook - model = cm.create_notebook() + model = cm.create_file() name = model['name'] path = model['path'] @@ -150,7 +150,7 @@ def test_get(self): # Test in sub-directory sub_dir = '/foo/' self.make_dir(cm.root_dir, 'foo') - model = cm.create_notebook(None, sub_dir) + model = cm.create_file(None, sub_dir) model2 = cm.get_model(name, sub_dir) assert isinstance(model2, dict) self.assertIn('name', model2) @@ -162,7 +162,7 @@ def test_get(self): def test_update(self): cm = self.contents_manager # Create a notebook - model = cm.create_notebook() + model = cm.create_file() name = model['name'] path = model['path'] @@ -181,7 +181,7 @@ def test_update(self): # Create a directory and notebook in that directory sub_dir = '/foo/' self.make_dir(cm.root_dir, 'foo') - model = cm.create_notebook(None, sub_dir) + model = cm.create_file(None, sub_dir) name = model['name'] path = model['path'] @@ -200,7 +200,7 @@ def test_update(self): def test_save(self): cm = self.contents_manager # Create a notebook - model = cm.create_notebook() + model = cm.create_file() name = model['name'] path = model['path'] @@ -219,7 +219,7 @@ def test_save(self): # Create a directory and notebook in that directory sub_dir = '/foo/' self.make_dir(cm.root_dir, 'foo') - model = cm.create_notebook(None, sub_dir) + model = cm.create_file(None, sub_dir) name = model['name'] path = model['path'] model = cm.get_model(name, path) @@ -248,7 +248,7 @@ def test_copy(self): path = u'å b' name = u'nb √.ipynb' os.mkdir(os.path.join(cm.root_dir, path)) - orig = cm.create_notebook({'name' : name}, path=path) + orig = cm.create_file({'name' : name}, path=path) # copy with unspecified name copy = cm.copy(name, path=path) diff --git a/IPython/html/static/notebook/js/notebook.js b/IPython/html/static/notebook/js/notebook.js index 67e5e8b6fc5..02a4439ee44 100644 --- a/IPython/html/static/notebook/js/notebook.js +++ b/IPython/html/static/notebook/js/notebook.js @@ -1885,6 +1885,8 @@ define([ var model = {}; model.name = this.notebook_name; model.path = this.notebook_path; + model.type = 'notebook'; + model.format = 'json'; model.content = this.toJSON(); model.content.nbformat = this.nbformat; model.content.nbformat_minor = this.nbformat_minor; From 2bee5e5abcdeb825dde0aeba7d7d8d5f11e43e92 Mon Sep 17 00:00:00 2001 From: MinRK Date: Mon, 16 Jun 2014 11:49:03 -0700 Subject: [PATCH 06/16] test creating a directory with PUT --- .../contents/tests/test_contents_api.py | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/IPython/html/services/contents/tests/test_contents_api.py b/IPython/html/services/contents/tests/test_contents_api.py index d0e3e20d7a9..4c73075c4ea 100644 --- a/IPython/html/services/contents/tests/test_contents_api.py +++ b/IPython/html/services/contents/tests/test_contents_api.py @@ -247,12 +247,16 @@ def test_get_binary_file_contents(self): with assert_http_error(404): self.api.read('q.txt', 'foo') - def _check_nb_created(self, resp, name, path): + def _check_created(self, resp, name, path, type='notebook'): self.assertEqual(resp.status_code, 201) location_header = py3compat.str_to_unicode(resp.headers['Location']) self.assertEqual(location_header, url_escape(url_path_join(u'/api/contents', path, name))) - self.assertEqual(resp.json()['name'], name) - assert os.path.isfile(pjoin( + rjson = resp.json() + self.assertEqual(rjson['name'], name) + self.assertEqual(rjson['path'], path) + self.assertEqual(rjson['type'], type) + isright = os.path.isdir if type == 'directory' else os.path.isfile + assert isright(pjoin( self.notebook_dir.name, path.replace('/', os.sep), name, @@ -260,19 +264,19 @@ def _check_nb_created(self, resp, name, path): def test_create_untitled(self): resp = self.api.create_untitled(path=u'å b') - self._check_nb_created(resp, 'Untitled0.ipynb', u'å b') + self._check_created(resp, 'Untitled0.ipynb', u'å b') # Second time resp = self.api.create_untitled(path=u'å b') - self._check_nb_created(resp, 'Untitled1.ipynb', u'å b') + self._check_created(resp, 'Untitled1.ipynb', u'å b') # And two directories down resp = self.api.create_untitled(path='foo/bar') - self._check_nb_created(resp, 'Untitled0.ipynb', 'foo/bar') + self._check_created(resp, 'Untitled0.ipynb', 'foo/bar') def test_create_untitled_txt(self): resp = self.api.create_untitled(path='foo/bar', ext='.txt') - self._check_nb_created(resp, 'Untitled0.txt', 'foo/bar') + self._check_created(resp, 'Untitled0.txt', 'foo/bar', type='file') resp = self.api.read(path='foo/bar', name='Untitled0.txt') model = resp.json() @@ -285,14 +289,20 @@ def test_upload_untitled(self): nbmodel = {'content': nb, 'type': 'notebook'} resp = self.api.upload_untitled(path=u'å b', body=json.dumps(nbmodel)) - self._check_nb_created(resp, 'Untitled0.ipynb', u'å b') + self._check_created(resp, 'Untitled0.ipynb', u'å b') def test_upload(self): nb = new_notebook(name=u'ignored') nbmodel = {'content': nb, 'type': 'notebook'} resp = self.api.upload(u'Upload tést.ipynb', path=u'å b', body=json.dumps(nbmodel)) - self._check_nb_created(resp, u'Upload tést.ipynb', u'å b') + self._check_created(resp, u'Upload tést.ipynb', u'å b') + + def test_mkdir(self): + model = {'type': 'directory'} + resp = self.api.upload(u'New ∂ir', path=u'å b', + body=json.dumps(model)) + self._check_created(resp, u'New ∂ir', u'å b', type='directory') def test_upload_txt(self): body = u'ünicode téxt' @@ -338,7 +348,7 @@ def test_upload_v2(self): nbmodel = {'content': nb, 'type': 'notebook'} resp = self.api.upload(u'Upload tést.ipynb', path=u'å b', body=json.dumps(nbmodel)) - self._check_nb_created(resp, u'Upload tést.ipynb', u'å b') + self._check_created(resp, u'Upload tést.ipynb', u'å b') resp = self.api.read(u'Upload tést.ipynb', u'å b') data = resp.json() self.assertEqual(data['content']['nbformat'], current.nbformat) @@ -346,11 +356,11 @@ def test_upload_v2(self): def test_copy_untitled(self): resp = self.api.copy_untitled(u'ç d.ipynb', path=u'å b') - self._check_nb_created(resp, u'ç d-Copy0.ipynb', u'å b') + self._check_created(resp, u'ç d-Copy0.ipynb', u'å b') def test_copy(self): resp = self.api.copy(u'ç d.ipynb', u'cøpy.ipynb', path=u'å b') - self._check_nb_created(resp, u'cøpy.ipynb', u'å b') + self._check_created(resp, u'cøpy.ipynb', u'å b') def test_delete(self): for d, name in self.dirs_nbs: From fb8cd393d5ee7b5cb9d15ad8ac8a9afb8de83e84 Mon Sep 17 00:00:00 2001 From: MinRK Date: Mon, 16 Jun 2014 12:47:00 -0700 Subject: [PATCH 07/16] contents service review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - missed some s/notebook/file/ in docstrings - manager doesn’t sort contents --- IPython/html/services/contents/filemanager.py | 23 ++++++-------- IPython/html/services/contents/handlers.py | 27 +++++++++------- IPython/html/services/contents/manager.py | 31 ++++++++++--------- 3 files changed, 42 insertions(+), 39 deletions(-) diff --git a/IPython/html/services/contents/filemanager.py b/IPython/html/services/contents/filemanager.py index e9fab0b16ef..2c36e05cdf8 100644 --- a/IPython/html/services/contents/filemanager.py +++ b/IPython/html/services/contents/filemanager.py @@ -19,10 +19,6 @@ from IPython.utils import tz from IPython.html.utils import is_hidden, to_os_path -def sort_key(item): - """Case-insensitive sorting.""" - return item['name'].lower() - class FileContentsManager(ContentsManager): @@ -38,9 +34,9 @@ def _root_dir_changed(self, name, old, new): raise TraitError("%r is not a directory" % new) checkpoint_dir = Unicode('.ipynb_checkpoints', config=True, - help="""The directory name in which to keep notebook checkpoints + help="""The directory name in which to keep file checkpoints - This is a path relative to the notebook's own directory. + This is a path relative to the file's own directory. By default, it is .ipynb_checkpoints """ @@ -157,7 +153,7 @@ def _base_model(self, name, path=''): info = os.stat(os_path) last_modified = tz.utcfromtimestamp(info.st_mtime) created = tz.utcfromtimestamp(info.st_ctime) - # Create the notebook model. + # Create the base model. model = {} model['name'] = name model['path'] = path @@ -189,13 +185,12 @@ def _dir_model(self, name, path='', content=True): model['type'] = 'directory' dir_path = u'{}/{}'.format(path, name) if content: - contents = [] + model['content'] = contents = [] for os_path in glob.glob(self._get_os_path('*', dir_path)): name = os.path.basename(os_path) if self.should_list(name) and not is_hidden(os_path, self.root_dir): contents.append(self.get_model(name=name, path=dir_path, content=False)) - model['content'] = sorted(contents, key=sort_key) model['format'] = 'json' return model @@ -204,7 +199,7 @@ def _file_model(self, name, path='', content=True): """Build a model for a file if content is requested, include the file contents. - Text files will be unicode, binary files will be base64-encoded. + UTF-8 text files will be unicode, binary files will be base64-encoded. """ model = self._base_model(name, path) model['type'] = 'file' @@ -251,8 +246,7 @@ def get_model(self, name, path='', content=True): name : str the name of the target path : str - the URL path that describes the relative path for - the notebook + the URL path that describes the relative path for the target Returns ------- @@ -275,6 +269,7 @@ def get_model(self, name, path='', content=True): return model def _save_notebook(self, os_path, model, name='', path=''): + """save a notebook file""" # Save the notebook file nb = current.to_notebook_json(model['content']) @@ -287,6 +282,7 @@ def _save_notebook(self, os_path, model, name='', path=''): current.write(nb, f, u'json') def _save_file(self, os_path, model, name='', path=''): + """save a non-notebook file""" fmt = model.get('format', None) if fmt not in {'text', 'base64'}: raise web.HTTPError(400, "Must specify format of file contents as 'text' or 'base64'") @@ -303,6 +299,7 @@ def _save_file(self, os_path, model, name='', path=''): f.write(bcontent) def _save_directory(self, os_path, model, name='', path=''): + """create a directory""" if not os.path.exists(os_path): os.mkdir(os_path) elif not os.path.isdir(os_path): @@ -442,7 +439,7 @@ def create_checkpoint(self, name, path=''): # only the one checkpoint ID: checkpoint_id = u"checkpoint" cp_path = self.get_checkpoint_path(checkpoint_id, name, path) - self.log.debug("creating checkpoint for notebook %s", name) + self.log.debug("creating checkpoint for %s", name) self._copy(src_path, cp_path) # return the checkpoint info diff --git a/IPython/html/services/contents/handlers.py b/IPython/html/services/contents/handlers.py index e6f08ed2567..7f394f3ba45 100644 --- a/IPython/html/services/contents/handlers.py +++ b/IPython/html/services/contents/handlers.py @@ -15,6 +15,16 @@ file_name_regex) +def sort_key(model): + """key function for case-insensitive sort by name and type""" + iname = model['name'].lower() + type_key = { + 'directory' : '0', + 'notebook' : '1', + 'file' : '2', + }.get(model['type'], '9') + return u'%s%s' % (type_key, iname) + class ContentsHandler(IPythonHandler): SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE') @@ -52,16 +62,9 @@ def get(self, path='', name=None): path = path or '' model = self.contents_manager.get_model(name=name, path=path) if model['type'] == 'directory': - # resort listing to group directories at the top - dirs = [] - files = [] - for entry in model['content']: - if entry['type'] == 'directory': - dirs.append(entry) - else: - # do we also want to group notebooks separate from files? - files.append(entry) - model['content'] = dirs + files + # group listing by type, then by name (case-insensitive) + # FIXME: front-ends shouldn't rely on this sorting + model['content'].sort(key=sort_key) self._finish_model(model, location=False) @web.authenticated @@ -130,9 +133,9 @@ def _save(self, model, path, name): @web.authenticated @json_errors def post(self, path='', name=None): - """Create a new notebook in the specified path. + """Create a new file or directory in the specified path. - POST creates new notebooks. The server always decides on the notebook name. + POST creates new files or directories. The server always decides on the name. POST /api/contents/path New untitled notebook in path. If content specified, upload a diff --git a/IPython/html/services/contents/manager.py b/IPython/html/services/contents/manager.py index 8cec3983c17..cd4231c7957 100644 --- a/IPython/html/services/contents/manager.py +++ b/IPython/html/services/contents/manager.py @@ -18,7 +18,10 @@ class ContentsManager(LoggingConfigurable): def _notary_default(self): return sign.NotebookNotary(parent=self) - hide_globs = List(Unicode, [u'__pycache__'], config=True, help=""" + hide_globs = List(Unicode, [ + u'__pycache__', '*.pyc', '*.pyo', + '.DS_Store', '*.so', '*.dylib', '*~', + ], config=True, help=""" Glob patterns to hide in file and directory listings. """) @@ -60,14 +63,14 @@ def is_hidden(self, path): raise NotImplementedError def file_exists(self, name, path=''): - """Returns a True if the notebook exists. Else, returns False. + """Returns a True if the file exists. Else, returns False. Parameters ---------- name : string - The name of the notebook you are checking. + The name of the file you are checking. path : string - The relative path to the notebook (with '/' as separator) + The relative path to the file's directory (with '/' as separator) Returns ------- @@ -87,38 +90,38 @@ def list(self, path=''): raise NotImplementedError('must be implemented in a subclass') def get_model(self, name, path='', content=True): - """Get the notebook model with or without content.""" + """Get the model of a file or directory with or without content.""" raise NotImplementedError('must be implemented in a subclass') def save(self, model, name, path=''): - """Save the notebook and return the model with no content.""" + """Save the file or directory and return the model with no content.""" raise NotImplementedError('must be implemented in a subclass') def update(self, model, name, path=''): - """Update the notebook and return the model with no content.""" + """Update the file or directory and return the model with no content.""" raise NotImplementedError('must be implemented in a subclass') def delete(self, name, path=''): - """Delete notebook by name and path.""" + """Delete file or directory by name and path.""" raise NotImplementedError('must be implemented in a subclass') def create_checkpoint(self, name, path=''): - """Create a checkpoint of the current state of a notebook + """Create a checkpoint of the current state of a file Returns a checkpoint_id for the new checkpoint. """ raise NotImplementedError("must be implemented in a subclass") def list_checkpoints(self, name, path=''): - """Return a list of checkpoints for a given notebook""" + """Return a list of checkpoints for a given file""" return [] def restore_checkpoint(self, checkpoint_id, name, path=''): - """Restore a notebook from one of its checkpoints""" + """Restore a file from one of its checkpoints""" raise NotImplementedError("must be implemented in a subclass") def delete_checkpoint(self, checkpoint_id, name, path=''): - """delete a checkpoint for a notebook""" + """delete a checkpoint for a file""" raise NotImplementedError("must be implemented in a subclass") def info_string(self): @@ -139,7 +142,7 @@ def increment_filename(self, filename, path=''): filename : unicode The name of a file, including extension path : unicode - The URL path of the notebooks directory + The URL path of the target's directory Returns ------- @@ -156,7 +159,7 @@ def increment_filename(self, filename, path=''): return name def create_file(self, model=None, path='', ext='.ipynb'): - """Create a new notebook and return its model with no content.""" + """Create a new file or directory and return its model with no content.""" path = path.strip('/') if model is None: model = {} From 35c8d208d0c5447fd77231d7664988cb5893d14f Mon Sep 17 00:00:00 2001 From: MinRK Date: Mon, 16 Jun 2014 12:47:35 -0700 Subject: [PATCH 08/16] support deleting empty directories MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit can’t copy directories --- IPython/html/services/contents/filemanager.py | 16 +++++++++--- IPython/html/services/contents/manager.py | 4 +++ .../contents/tests/test_contents_api.py | 26 ++++++++++++++++--- 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/IPython/html/services/contents/filemanager.py b/IPython/html/services/contents/filemanager.py index 2c36e05cdf8..ef4acf41381 100644 --- a/IPython/html/services/contents/filemanager.py +++ b/IPython/html/services/contents/filemanager.py @@ -357,7 +357,13 @@ def delete(self, name, path=''): """Delete file by name and path.""" path = path.strip('/') os_path = self._get_os_path(name, path) - if not os.path.isfile(os_path): + rm = os.unlink + if os.path.isdir(os_path): + listing = os.listdir(os_path) + # don't delete non-empty directories (checkpoints dir doesn't count) + if listing and listing != ['.ipynb_checkpoints']: + raise web.HTTPError(400, u'Directory %s not empty' % os_path) + elif not os.path.isfile(os_path): raise web.HTTPError(404, u'File does not exist: %s' % os_path) # clear checkpoints @@ -368,8 +374,12 @@ def delete(self, name, path=''): self.log.debug("Unlinking checkpoint %s", cp_path) os.unlink(cp_path) - self.log.debug("Unlinking file %s", os_path) - os.unlink(os_path) + if os.path.isdir(os_path): + self.log.debug("Removing directory %s", os_path) + shutil.rmtree(os_path) + else: + self.log.debug("Unlinking file %s", os_path) + rm(os_path) def rename(self, old_name, old_path, new_name, new_path): """Rename a file.""" diff --git a/IPython/html/services/contents/manager.py b/IPython/html/services/contents/manager.py index cd4231c7957..871c3639e62 100644 --- a/IPython/html/services/contents/manager.py +++ b/IPython/html/services/contents/manager.py @@ -7,6 +7,8 @@ import itertools import os +from tornado.web import HTTPError + from IPython.config.configurable import LoggingConfigurable from IPython.nbformat import current, sign from IPython.utils.traitlets import Instance, Unicode, List @@ -187,6 +189,8 @@ def copy(self, from_name, to_name=None, path=''): """ path = path.strip('/') model = self.get_model(from_name, path) + if model['type'] == 'directory': + raise HTTPError(400, "Can't copy directories") if not to_name: base, ext = os.path.splitext(from_name) copy_name = u'{0}-Copy{1}'.format(base, ext) diff --git a/IPython/html/services/contents/tests/test_contents_api.py b/IPython/html/services/contents/tests/test_contents_api.py index 4c73075c4ea..aadf0c93ba0 100644 --- a/IPython/html/services/contents/tests/test_contents_api.py +++ b/IPython/html/services/contents/tests/test_contents_api.py @@ -69,6 +69,9 @@ def create(self, name, path='/'): def upload(self, name, body, path='/'): return self._req('PUT', url_path_join(path, name), body) + def mkdir(self, name, path='/'): + return self._req('PUT', url_path_join(path, name), json.dumps({'type': 'directory'})) + def copy(self, copy_from, copy_to, path='/'): body = json.dumps({'copy_from':copy_from}) return self._req('PUT', url_path_join(path, copy_to), body) @@ -299,9 +302,7 @@ def test_upload(self): self._check_created(resp, u'Upload tést.ipynb', u'å b') def test_mkdir(self): - model = {'type': 'directory'} - resp = self.api.upload(u'New ∂ir', path=u'å b', - body=json.dumps(model)) + resp = self.api.mkdir(u'New ∂ir', path=u'å b') self._check_created(resp, u'New ∂ir', u'å b', type='directory') def test_upload_txt(self): @@ -362,6 +363,11 @@ def test_copy(self): resp = self.api.copy(u'ç d.ipynb', u'cøpy.ipynb', path=u'å b') self._check_created(resp, u'cøpy.ipynb', u'å b') + def test_copy_dir_400(self): + # can't copy directories + with assert_http_error(400): + resp = self.api.copy(u'å b', u'å c') + def test_delete(self): for d, name in self.dirs_nbs: resp = self.api.delete('%s.ipynb' % name, d) @@ -371,6 +377,20 @@ def test_delete(self): nbs = notebooks_only(self.api.list(d).json()) self.assertEqual(len(nbs), 0) + def test_delete_dirs(self): + # depth-first delete everything, so we don't try to delete empty directories + for name in sorted(self.dirs + ['/'], key=len, reverse=True): + listing = self.api.list(name).json()['content'] + for model in listing: + self.api.delete(model['name'], model['path']) + listing = self.api.list('/').json()['content'] + self.assertEqual(listing, []) + + def test_delete_non_empty_dir(self): + """delete non-empty dir raises 400""" + with assert_http_error(400): + self.api.delete(u'å b') + def test_rename(self): resp = self.api.rename('a.ipynb', 'foo', 'z.ipynb') self.assertEqual(resp.headers['Location'].split('/')[-1], 'z.ipynb') From ec353c7dcc051e51e0d31912a7710a39172d06ac Mon Sep 17 00:00:00 2001 From: MinRK Date: Tue, 17 Jun 2014 11:18:43 -0700 Subject: [PATCH 09/16] use contents manager to handle redirect of /notebooks -> /files avoids 404 on /files for visiting a nonexistent notebook --- IPython/html/notebook/handlers.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/IPython/html/notebook/handlers.py b/IPython/html/notebook/handlers.py index 0fc88919831..78938a584fc 100644 --- a/IPython/html/notebook/handlers.py +++ b/IPython/html/notebook/handlers.py @@ -60,20 +60,23 @@ def get(self, path=''): # it's a *directory*, redirect to /tree url = url_path_join(self.base_url, 'tree', path) else: + orig_path = path # otherwise, redirect to /files - if '/files/' in path: + parts = path.split('/') + path = '/'.join(parts[:-1]) + name = parts[-1] + + if not cm.file_exists(name=name, path=path) and 'files' in parts: # redirect without files/ iff it would 404 # this preserves pre-2.0-style 'files/' links - # FIXME: this is hardcoded based on notebook_path, - # but so is the files handler itself, - # so it should work until both are cleaned up. - parts = path.split('/') - files_path = os.path.join(cm.root_dir, *parts) - if not os.path.exists(files_path): - self.log.warn("Deprecated files/ URL: %s", path) - path = path.replace('/files/', '/', 1) + self.log.warn("Deprecated files/ URL: %s", orig_path) + parts.remove('files') + path = '/'.join(parts[:-1]) + + if not cm.file_exists(name=name, path=path): + raise web.HTTPError(404) - url = url_path_join(self.base_url, 'files', path) + url = url_path_join(self.base_url, 'files', path, name) url = url_escape(url) self.log.debug("Redirecting %s to %s", self.request.path, url) self.redirect(url) From b3343796a091b8bee869f7cb8182d1350c70825f Mon Sep 17 00:00:00 2001 From: MinRK Date: Mon, 30 Jun 2014 10:50:07 -0700 Subject: [PATCH 10/16] updates per review a few more notebook mentions in docstrings, and inappropriate use of setdefault. --- IPython/html/services/contents/filemanager.py | 4 ++-- IPython/html/services/contents/manager.py | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/IPython/html/services/contents/filemanager.py b/IPython/html/services/contents/filemanager.py index ef4acf41381..a2f35eea571 100644 --- a/IPython/html/services/contents/filemanager.py +++ b/IPython/html/services/contents/filemanager.py @@ -309,10 +309,10 @@ def save(self, model, name='', path=''): """Save the file model and return the model with no content.""" path = path.strip('/') - if 'content' not in model: - raise web.HTTPError(400, u'No file content provided') if 'type' not in model: raise web.HTTPError(400, u'No file type provided') + if 'content' not in model and model['type'] != 'directory': + raise web.HTTPError(400, u'No file content provided') # One checkpoint should always exist if self.file_exists(name, path) and not self.list_checkpoints(name, path): diff --git a/IPython/html/services/contents/manager.py b/IPython/html/services/contents/manager.py index 871c3639e62..2a8c7760ba1 100644 --- a/IPython/html/services/contents/manager.py +++ b/IPython/html/services/contents/manager.py @@ -165,16 +165,16 @@ def create_file(self, model=None, path='', ext='.ipynb'): path = path.strip('/') if model is None: model = {} - if 'content' not in model: + if 'content' not in model and model.get('type', None) != 'directory': if ext == '.ipynb': metadata = current.new_metadata(name=u'') model['content'] = current.new_notebook(metadata=metadata) - model.setdefault('type', 'notebook') - model.setdefault('format', 'json') + model['type'] = 'notebook' + model['format'] = 'json' else: model['content'] = '' - model.setdefault('type', 'file') - model.setdefault('format', 'text') + model['type'] = 'file' + model['format'] = 'text' if 'name' not in model: model['name'] = self.increment_filename('Untitled' + ext, path) @@ -185,7 +185,7 @@ def create_file(self, model=None, path='', ext='.ipynb'): def copy(self, from_name, to_name=None, path=''): """Copy an existing file and return its new model. - If to_name not specified, increment `from_name-Copy#.ipynb`. + If to_name not specified, increment `from_name-Copy#.ext`. """ path = path.strip('/') model = self.get_model(from_name, path) From 8f08d4e14486c6a10f21aa1533d1bf1fc8a942f7 Mon Sep 17 00:00:00 2001 From: MinRK Date: Tue, 15 Jul 2014 11:02:26 -0700 Subject: [PATCH 11/16] move `/files/` redirect to base handlers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit and reuse it in /nbconvert/html/… closes #6137 --- IPython/html/base/handlers.py | 33 ++++++++++++++++- IPython/html/nbconvert/handlers.py | 7 +++- IPython/html/notebook/handlers.py | 59 +++++------------------------- 3 files changed, 47 insertions(+), 52 deletions(-) diff --git a/IPython/html/base/handlers.py b/IPython/html/base/handlers.py index 76f9164723d..50551eea6c2 100644 --- a/IPython/html/base/handlers.py +++ b/IPython/html/base/handlers.py @@ -27,7 +27,7 @@ from IPython.config import Application from IPython.utils.path import filefind from IPython.utils.py3compat import string_types -from IPython.html.utils import is_hidden +from IPython.html.utils import is_hidden, url_path_join, url_escape #----------------------------------------------------------------------------- # Top-level handlers @@ -409,6 +409,37 @@ class TrailingSlashHandler(web.RequestHandler): def get(self): self.redirect(self.request.uri.rstrip('/')) + +class FilesRedirectHandler(IPythonHandler): + """Handler for redirecting relative URLs to the /files/ handler""" + def get(self, path=''): + cm = self.contents_manager + if cm.path_exists(path): + # it's a *directory*, redirect to /tree + url = url_path_join(self.base_url, 'tree', path) + else: + orig_path = path + # otherwise, redirect to /files + parts = path.split('/') + path = '/'.join(parts[:-1]) + name = parts[-1] + + if not cm.file_exists(name=name, path=path) and 'files' in parts: + # redirect without files/ iff it would 404 + # this preserves pre-2.0-style 'files/' links + self.log.warn("Deprecated files/ URL: %s", orig_path) + parts.remove('files') + path = '/'.join(parts[:-1]) + + if not cm.file_exists(name=name, path=path): + raise web.HTTPError(404) + + url = url_path_join(self.base_url, 'files', path, name) + url = url_escape(url) + self.log.debug("Redirecting %s to %s", self.request.path, url) + self.redirect(url) + + #----------------------------------------------------------------------------- # URL pattern fragments for re-use #----------------------------------------------------------------------------- diff --git a/IPython/html/nbconvert/handlers.py b/IPython/html/nbconvert/handlers.py index 93e6bf1d498..f6e1094d164 100644 --- a/IPython/html/nbconvert/handlers.py +++ b/IPython/html/nbconvert/handlers.py @@ -9,7 +9,10 @@ from tornado import web -from ..base.handlers import IPythonHandler, notebook_path_regex +from ..base.handlers import ( + IPythonHandler, FilesRedirectHandler, + notebook_path_regex, path_regex, +) from IPython.nbformat.current import to_notebook_json from IPython.utils.py3compat import cast_bytes @@ -128,6 +131,7 @@ def post(self, format): self.finish(output) + #----------------------------------------------------------------------------- # URL to handler mappings #----------------------------------------------------------------------------- @@ -139,4 +143,5 @@ def post(self, format): (r"/nbconvert/%s%s" % (_format_regex, notebook_path_regex), NbconvertFileHandler), (r"/nbconvert/%s" % _format_regex, NbconvertPostHandler), + (r"/nbconvert/html%s" % path_regex, FilesRedirectHandler), ] diff --git a/IPython/html/notebook/handlers.py b/IPython/html/notebook/handlers.py index 78938a584fc..a7eae8a405c 100644 --- a/IPython/html/notebook/handlers.py +++ b/IPython/html/notebook/handlers.py @@ -1,31 +1,17 @@ -"""Tornado handlers for the live notebook view. +"""Tornado handlers for the live notebook view.""" -Authors: - -* Brian Granger -""" - -#----------------------------------------------------------------------------- -# Copyright (C) 2011 The IPython Development Team -# -# Distributed under the terms of the BSD License. The full license is in -# the file COPYING, distributed as part of this software. -#----------------------------------------------------------------------------- - -#----------------------------------------------------------------------------- -# Imports -#----------------------------------------------------------------------------- +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. import os from tornado import web HTTPError = web.HTTPError -from ..base.handlers import IPythonHandler, notebook_path_regex, path_regex -from ..utils import url_path_join, url_escape - -#----------------------------------------------------------------------------- -# Handlers -#----------------------------------------------------------------------------- +from ..base.handlers import ( + IPythonHandler, FilesRedirectHandler, + notebook_path_regex, path_regex, +) +from ..utils import url_escape class NotebookHandler(IPythonHandler): @@ -53,33 +39,6 @@ def get(self, path='', name=None): ) ) -class NotebookRedirectHandler(IPythonHandler): - def get(self, path=''): - cm = self.contents_manager - if cm.path_exists(path): - # it's a *directory*, redirect to /tree - url = url_path_join(self.base_url, 'tree', path) - else: - orig_path = path - # otherwise, redirect to /files - parts = path.split('/') - path = '/'.join(parts[:-1]) - name = parts[-1] - - if not cm.file_exists(name=name, path=path) and 'files' in parts: - # redirect without files/ iff it would 404 - # this preserves pre-2.0-style 'files/' links - self.log.warn("Deprecated files/ URL: %s", orig_path) - parts.remove('files') - path = '/'.join(parts[:-1]) - - if not cm.file_exists(name=name, path=path): - raise web.HTTPError(404) - - url = url_path_join(self.base_url, 'files', path, name) - url = url_escape(url) - self.log.debug("Redirecting %s to %s", self.request.path, url) - self.redirect(url) #----------------------------------------------------------------------------- # URL to handler mappings @@ -88,6 +47,6 @@ def get(self, path=''): default_handlers = [ (r"/notebooks%s" % notebook_path_regex, NotebookHandler), - (r"/notebooks%s" % path_regex, NotebookRedirectHandler), + (r"/notebooks%s" % path_regex, FilesRedirectHandler), ] From 8c324cdd3a2fbbf32183d30b95e76f97ce5cf67d Mon Sep 17 00:00:00 2001 From: MinRK Date: Mon, 21 Jul 2014 13:15:27 -0700 Subject: [PATCH 12/16] whatsnew for contents service --- docs/source/whatsnew/pr/incompat-contents-service.rst | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 docs/source/whatsnew/pr/incompat-contents-service.rst diff --git a/docs/source/whatsnew/pr/incompat-contents-service.rst b/docs/source/whatsnew/pr/incompat-contents-service.rst new file mode 100644 index 00000000000..3f7b9d1b23f --- /dev/null +++ b/docs/source/whatsnew/pr/incompat-contents-service.rst @@ -0,0 +1,6 @@ +- The NotebookManager and ``/api/notebooks`` service has been replaced by + a more generic ContentsManager and ``/api/contents`` service, + which supports all kinds of files. +- The Dashboard now lists all files, not just notebooks and directories. +- The ``--script`` hook for saving notebooks to Python scripts is removed, + use ``ipython nbconvert --to python [notebook]`` instead. From 920c0ba667342ca43712500cde622ebc2c40447d Mon Sep 17 00:00:00 2001 From: MinRK Date: Tue, 22 Jul 2014 14:39:54 -0700 Subject: [PATCH 13/16] updates per review - clarified docstrings and errors - still more notebook/file renames - configurable untitled names - copy_from can be full path - fix running, upload, new-tab behaviors in dashboard Yay, review! --- IPython/html/services/contents/filemanager.py | 27 ++-- IPython/html/services/contents/handlers.py | 38 +++--- IPython/html/services/contents/manager.py | 120 ++++++++++++++---- .../contents/tests/test_contents_api.py | 10 +- IPython/html/static/tree/js/kernellist.js | 21 ++- IPython/html/static/tree/js/notebooklist.js | 14 +- 6 files changed, 168 insertions(+), 62 deletions(-) diff --git a/IPython/html/services/contents/filemanager.py b/IPython/html/services/contents/filemanager.py index a2f35eea571..3f56baa5c48 100644 --- a/IPython/html/services/contents/filemanager.py +++ b/IPython/html/services/contents/filemanager.py @@ -54,7 +54,7 @@ def _copy(self, src, dest): self.log.debug("copystat on %s failed", dest, exc_info=True) def _get_os_path(self, name=None, path=''): - """Given a filename and a URL path, return its file system + """Given a filename and API path, return its file system path. Parameters @@ -62,8 +62,7 @@ def _get_os_path(self, name=None, path=''): name : string A filename path : string - The relative URL path (with '/' as separator) to the named - file. + The relative API path to the named file. Returns ------- @@ -77,6 +76,8 @@ def _get_os_path(self, name=None, path=''): def path_exists(self, path): """Does the API-style path refer to an extant directory? + API-style wrapper for os.path.isdir + Parameters ---------- path : string @@ -114,6 +115,8 @@ def is_hidden(self, path): def file_exists(self, name, path=''): """Returns True if the file exists, else returns False. + API-style wrapper for os.path.isfile + Parameters ---------- name : string @@ -123,7 +126,8 @@ def file_exists(self, name, path=''): Returns ------- - bool + exists : bool + Whether the file exists. """ path = path.strip('/') nbpath = self._get_os_path(name, path=path) @@ -132,6 +136,8 @@ def file_exists(self, name, path=''): def exists(self, name=None, path=''): """Returns True if the path [and name] exists, else returns False. + API-style wrapper for os.path.exists + Parameters ---------- name : string @@ -141,7 +147,8 @@ def exists(self, name=None, path=''): Returns ------- - bool + exists : bool + Whether the target exists. """ path = path.strip('/') os_path = self._get_os_path(name, path=path) @@ -246,7 +253,7 @@ def get_model(self, name, path='', content=True): name : str the name of the target path : str - the URL path that describes the relative path for the target + the API path that describes the relative path for the target Returns ------- @@ -344,7 +351,11 @@ def save(self, model, name='', path=''): return model def update(self, model, name, path=''): - """Update the file's path and/or name""" + """Update the file's path and/or name + + For use in PATCH requests, to enable renaming a file without + re-uploading its contents. Only used for renaming at the moment. + """ path = path.strip('/') new_name = model.get('name', name) new_path = model.get('path', path).strip('/') @@ -393,7 +404,7 @@ def rename(self, old_name, old_path, new_name, new_path): # Should we proceed with the move? if os.path.isfile(new_os_path): - raise web.HTTPError(409, u'Notebook with name already exists: %s' % new_os_path) + raise web.HTTPError(409, u'File with name already exists: %s' % new_os_path) # Move the file try: diff --git a/IPython/html/services/contents/handlers.py b/IPython/html/services/contents/handlers.py index 7f394f3ba45..72860ad6722 100644 --- a/IPython/html/services/contents/handlers.py +++ b/IPython/html/services/contents/handlers.py @@ -54,16 +54,16 @@ def _finish_model(self, model, location=True): @web.authenticated @json_errors def get(self, path='', name=None): - """Return a file or list of files. + """Return a model for a file or directory. - * GET with path and no filename lists files in a directory - * GET with path and filename returns file contents model + A directory model contains a list of models (without content) + of the files and directories it contains. """ path = path or '' model = self.contents_manager.get_model(name=name, path=path) if model['type'] == 'directory': # group listing by type, then by name (case-insensitive) - # FIXME: front-ends shouldn't rely on this sorting + # FIXME: sorting should be done in the frontends model['content'].sort(key=sort_key) self._finish_model(model, location=False) @@ -81,22 +81,22 @@ def patch(self, path='', name=None): self._finish_model(model) def _copy(self, copy_from, path, copy_to=None): - """Copy a file in path, optionally specifying the new name. - - Only support copying within the same directory. + """Copy a file, optionally specifying the new name. """ - self.log.info(u"Copying from %s/%s to %s/%s", - path, copy_from, - path, copy_to or '', - ) + self.log.info(u"Copying {copy_from} to {path}/{copy_to}".format( + copy_from=copy_from, + path=path, + copy_to=copy_to or '', + )) model = self.contents_manager.copy(copy_from, copy_to, path) self.set_status(201) self._finish_model(model) def _upload(self, model, path, name=None): - """Upload a file + """Handle upload of a new file - If name specified, create it in path/name. + If name specified, create it in path/name, + otherwise create a new untitled file in path. """ self.log.info(u"Uploading file to %s/%s", path, name or '') if name: @@ -151,7 +151,7 @@ def post(self, path='', name=None): cm = self.contents_manager if cm.file_exists(path): - raise web.HTTPError(400, "Only POST to directories. Use PUT for full names.") + raise web.HTTPError(400, "Cannot POST to existing files, use PUT instead.") if not cm.path_exists(path): raise web.HTTPError(404, "No such directory: %s" % path) @@ -184,11 +184,17 @@ def put(self, path='', name=None): Save notebook at ``path/Name.ipynb``. Notebook structure is specified in `content` key of JSON request body. If content is not specified, create a new empty notebook. - PUT /api/contents/path/Name.ipynb?copy=OtherNotebook.ipynb + PUT /api/contents/path/Name.ipynb + with JSON body:: + + { + "copy_from" : "[path/to/]OtherNotebook.ipynb" + } + Copy OtherNotebook to Name """ if name is None: - raise web.HTTPError(400, "Only PUT to full names. Use POST for directories.") + raise web.HTTPError(400, "name must be specified with PUT.") model = self.get_json_body() if model: diff --git a/IPython/html/services/contents/manager.py b/IPython/html/services/contents/manager.py index 2a8c7760ba1..e6a11ed4d3b 100644 --- a/IPython/html/services/contents/manager.py +++ b/IPython/html/services/contents/manager.py @@ -15,6 +15,31 @@ class ContentsManager(LoggingConfigurable): + """Base class for serving files and directories. + + This serves any text or binary file, + as well as directories, + with special handling for JSON notebook documents. + + Most APIs take a path argument, + which is always an API-style unicode path, + and always refers to a directory. + + - unicode, not url-escaped + - '/'-separated + - leading and trailing '/' will be stripped + - if unspecified, path defaults to '', + indicating the root path. + + name is also unicode, and refers to a specfic target: + + - unicode, not url-escaped + - must not contain '/' + - It refers to an individual filename + - It may refer to a directory name, + in the case of listing or creating directories. + + """ notary = Instance(sign.NotebookNotary) def _notary_default(self): @@ -27,12 +52,26 @@ def _notary_default(self): Glob patterns to hide in file and directory listings. """) + untitled_notebook = Unicode("Untitled", config=True, + help="The base name used when creating untitled notebooks." + ) + + untitled_file = Unicode("untitled", config=True, + help="The base name used when creating untitled files." + ) + + untitled_directory = Unicode("Untitled Folder", config=True, + help="The base name used when creating untitled directories." + ) + # ContentsManager API part 1: methods that must be # implemented in subclasses. def path_exists(self, path): """Does the API-style path (directory) actually exist? + Like os.path.isdir + Override this method in subclasses. Parameters @@ -58,14 +97,18 @@ def is_hidden(self, path): Returns ------- - exists : bool + hidden : bool Whether the path is hidden. """ raise NotImplementedError def file_exists(self, name, path=''): - """Returns a True if the file exists. Else, returns False. + """Does a file exist at the given name and path? + + Like os.path.isfile + + Override this method in subclasses. Parameters ---------- @@ -76,20 +119,29 @@ def file_exists(self, name, path=''): Returns ------- - bool + exists : bool + Whether the file exists. """ raise NotImplementedError('must be implemented in a subclass') - def list(self, path=''): - """Return a list of contents dicts without content. + def exists(self, name, path=''): + """Does a file or directory exist at the given name and path? - This returns a list of dicts + Like os.path.exists - This list of dicts should be sorted by name:: + Parameters + ---------- + name : string + The name of the file you are checking. + path : string + The relative path to the file's directory (with '/' as separator) - data = sorted(data, key=lambda item: item['name']) + Returns + ------- + exists : bool + Whether the target exists. """ - raise NotImplementedError('must be implemented in a subclass') + return self.file_exists(name, path) or self.path_exists("%s/%s" % (path, name)) def get_model(self, name, path='', content=True): """Get the model of a file or directory with or without content.""" @@ -100,7 +152,11 @@ def save(self, model, name, path=''): raise NotImplementedError('must be implemented in a subclass') def update(self, model, name, path=''): - """Update the file or directory and return the model with no content.""" + """Update the file or directory and return the model with no content. + + For use in PATCH requests, to enable renaming a file without + re-uploading its contents. Only used for renaming at the moment. + """ raise NotImplementedError('must be implemented in a subclass') def delete(self, name, path=''): @@ -126,12 +182,12 @@ def delete_checkpoint(self, checkpoint_id, name, path=''): """delete a checkpoint for a file""" raise NotImplementedError("must be implemented in a subclass") - def info_string(self): - return "Serving notebooks" - # ContentsManager API part 2: methods that have useable default # implementations, but can be overridden in subclasses. + def info_string(self): + return "Serving contents" + def get_kernel_path(self, name, path='', model=None): """ Return the path to start kernel in """ return path @@ -144,7 +200,7 @@ def increment_filename(self, filename, path=''): filename : unicode The name of a file, including extension path : unicode - The URL path of the target's directory + The API path of the target's directory Returns ------- @@ -176,7 +232,15 @@ def create_file(self, model=None, path='', ext='.ipynb'): model['type'] = 'file' model['format'] = 'text' if 'name' not in model: - model['name'] = self.increment_filename('Untitled' + ext, path) + if model['type'] == 'directory': + untitled = self.untitled_directory + elif model['type'] == 'notebook': + untitled = self.untitled_notebook + elif model['type'] == 'file': + untitled = self.untitled_file + else: + raise HTTPError(400, "Unexpected model type: %r" % model['type']) + model['name'] = self.increment_filename(untitled + ext, path) model['path'] = path model = self.save(model, model['name'], model['path']) @@ -186,9 +250,16 @@ def copy(self, from_name, to_name=None, path=''): """Copy an existing file and return its new model. If to_name not specified, increment `from_name-Copy#.ext`. + + copy_from can be a full path to a file, + or just a base name. If a base name, `path` is used. """ path = path.strip('/') - model = self.get_model(from_name, path) + if '/' in from_name: + from_path, from_name = from_name.rsplit('/', 1) + else: + from_path = path + model = self.get_model(from_name, from_path) if model['type'] == 'directory': raise HTTPError(400, "Can't copy directories") if not to_name: @@ -196,6 +267,7 @@ def copy(self, from_name, to_name=None, path=''): copy_name = u'{0}-Copy{1}'.format(base, ext) to_name = self.increment_filename(copy_name, path) model['name'] = to_name + model['path'] = path model = self.save(model, to_name, path) return model @@ -218,7 +290,7 @@ def trust_notebook(self, name, path=''): self.notary.mark_cells(nb, True) self.save(model, name, path) - def check_and_sign(self, nb, name, path=''): + def check_and_sign(self, nb, name='', path=''): """Check for trusted cells, and sign the notebook. Called as a part of saving notebooks. @@ -226,18 +298,18 @@ def check_and_sign(self, nb, name, path=''): Parameters ---------- nb : dict - The notebook structure + The notebook object (in nbformat.current format) name : string - The filename of the notebook + The filename of the notebook (for logging) path : string - The notebook's directory + The notebook's directory (for logging) """ if self.notary.check_cells(nb): self.notary.sign(nb) else: self.log.warn("Saving untrusted notebook %s/%s", path, name) - def mark_trusted_cells(self, nb, name, path=''): + def mark_trusted_cells(self, nb, name='', path=''): """Mark cells as trusted if the notebook signature matches. Called as a part of loading notebooks. @@ -245,11 +317,11 @@ def mark_trusted_cells(self, nb, name, path=''): Parameters ---------- nb : dict - The notebook structure + The notebook object (in nbformat.current format) name : string - The filename of the notebook + The filename of the notebook (for logging) path : string - The notebook's directory + The notebook's directory (for logging) """ trusted = self.notary.check_signature(nb) if not trusted: diff --git a/IPython/html/services/contents/tests/test_contents_api.py b/IPython/html/services/contents/tests/test_contents_api.py index aadf0c93ba0..12dbf6be457 100644 --- a/IPython/html/services/contents/tests/test_contents_api.py +++ b/IPython/html/services/contents/tests/test_contents_api.py @@ -22,8 +22,6 @@ 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(dir_model): return [nb for nb in dir_model['content'] if nb['type']=='notebook'] @@ -279,9 +277,9 @@ def test_create_untitled(self): def test_create_untitled_txt(self): resp = self.api.create_untitled(path='foo/bar', ext='.txt') - self._check_created(resp, 'Untitled0.txt', 'foo/bar', type='file') + self._check_created(resp, 'untitled0.txt', 'foo/bar', type='file') - resp = self.api.read(path='foo/bar', name='Untitled0.txt') + resp = self.api.read(path='foo/bar', name='untitled0.txt') model = resp.json() self.assertEqual(model['type'], 'file') self.assertEqual(model['format'], 'text') @@ -363,6 +361,10 @@ def test_copy(self): resp = self.api.copy(u'ç d.ipynb', u'cøpy.ipynb', path=u'å b') self._check_created(resp, u'cøpy.ipynb', u'å b') + def test_copy_path(self): + resp = self.api.copy(u'foo/a.ipynb', u'cøpyfoo.ipynb', path=u'å b') + self._check_created(resp, u'cøpyfoo.ipynb', u'å b') + def test_copy_dir_400(self): # can't copy directories with assert_http_error(400): diff --git a/IPython/html/static/tree/js/kernellist.js b/IPython/html/static/tree/js/kernellist.js index 60b5628ef95..a4c318f58e4 100644 --- a/IPython/html/static/tree/js/kernellist.js +++ b/IPython/html/static/tree/js/kernellist.js @@ -19,7 +19,7 @@ define([ // base_url: string // notebook_path: string notebooklist.NotebookList.call(this, selector, $.extend({ - element_name: 'running'}, + element_name: 'running'}, options)); }; @@ -28,13 +28,20 @@ define([ KernelList.prototype.sessions_loaded = function (d) { this.sessions = d; this.clear_list(); - var item; - for (var path in d) { - item = this.new_notebook_item(-1); - this.add_link('', path, item); - this.add_shutdown_button(item, this.sessions[path]); + var item, path_name; + for (path_name in d) { + if (!d.hasOwnProperty(path_name)) { + // nothing is safe in javascript + continue; + } + item = this.new_item(-1); + this.add_link({ + name: path_name, + path: '', + type: 'notebook', + }, item); + this.add_shutdown_button(item, this.sessions[path_name]); } - $('#running_list_header').toggle($.isEmptyObject(d)); }; diff --git a/IPython/html/static/tree/js/notebooklist.js b/IPython/html/static/tree/js/notebooklist.js index c973bd8177f..6c9aedece34 100644 --- a/IPython/html/static/tree/js/notebooklist.js +++ b/IPython/html/static/tree/js/notebooklist.js @@ -80,7 +80,7 @@ define([ var name_and_ext = utils.splitext(f.name); var file_ext = name_and_ext[1]; if (file_ext === '.ipynb') { - var item = that.new_notebook_item(0); + var item = that.new_item(0); item.addClass('new-file'); that.add_name_input(f.name, item); // Store the notebook item in the reader so we can use it later @@ -236,7 +236,7 @@ define([ var icon = NotebookList.icons[model.type]; var uri_prefix = NotebookList.uri_prefixes[model.type]; item.find(".item_icon").addClass(icon).addClass('icon-fixed-width'); - item.find("a.item_link") + var link = item.find("a.item_link") .attr('href', utils.url_join_encode( this.base_url, @@ -245,6 +245,11 @@ define([ name ) ); + // directory nav doesn't open new tabs + // files, notebooks do + if (model.type !== "directory") { + link.attr('target','_blank'); + } var path_name = utils.url_path_join(path, name); if (model.type == 'file') { this.add_delete_button(item); @@ -362,7 +367,10 @@ define([ var nbdata = item.data('nbdata'); var content_type = 'application/json'; var model = { + path: path, + name: nbname, content : JSON.parse(nbdata), + type : 'notebook' }; var settings = { processData : false, @@ -372,7 +380,7 @@ define([ data : JSON.stringify(model), headers : {'Content-Type': content_type}, success : function (data, status, xhr) { - that.add_link(path, nbname, item); + that.add_link(model, item); that.add_delete_button(item); }, error : utils.log_ajax_error, From 459d5078412d576c00680d7fcf8acfed4f4cc967 Mon Sep 17 00:00:00 2001 From: MinRK Date: Sat, 26 Jul 2014 12:53:22 -0700 Subject: [PATCH 14/16] various upload fixes - recognize 201 as success (dataType must not be 'json') - support uploading non-notebook files (b64-encoded) --- IPython/html/static/tree/js/notebooklist.js | 95 ++++++++++++--------- 1 file changed, 57 insertions(+), 38 deletions(-) diff --git a/IPython/html/static/tree/js/notebooklist.js b/IPython/html/static/tree/js/notebooklist.js index 6c9aedece34..6d2fb338d99 100644 --- a/IPython/html/static/tree/js/notebooklist.js +++ b/IPython/html/static/tree/js/notebooklist.js @@ -75,30 +75,27 @@ define([ } for (var i = 0; i < files.length; i++) { var f = files[i]; - var reader = new FileReader(); - reader.readAsText(f); var name_and_ext = utils.splitext(f.name); var file_ext = name_and_ext[1]; + + var reader = new FileReader(); if (file_ext === '.ipynb') { - var item = that.new_item(0); - item.addClass('new-file'); - 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); - reader.onload = function (event) { - var nbitem = $(event.target).data('item'); - that.add_notebook_data(event.target.result, nbitem); - that.add_upload_button(nbitem); - }; + reader.readAsText(f); } else { - var dialog_body = 'Uploaded notebooks must be .ipynb files'; - dialog.modal({ - title : 'Invalid file type', - body : dialog_body, - buttons : {'OK' : {'class' : 'btn-primary'}} - }); + // read non-notebook files as binary + reader.readAsArrayBuffer(f); } + var item = that.new_item(0); + item.addClass('new-file'); + that.add_name_input(f.name, item); + // Store the list item in the reader so we can use it later + // to know which item it belongs to. + $(reader).data('item', item); + reader.onload = function (event) { + var item = $(event.target).data('item'); + that.add_file_data(event.target.result, item); + that.add_upload_button(item); + }; } // Replace the file input form wth a clone of itself. This is required to // reset the form. Otherwise, if you upload a file, delete it and try to @@ -268,16 +265,16 @@ define([ item.find(".item_icon").addClass('notebook_icon').addClass('icon-fixed-width'); item.find(".item_name").empty().append( $('') - .addClass("nbname_input") - .attr('value', utils.splitext(name)[0]) + .addClass("filename_input") + .attr('value', name) .attr('size', '30') .attr('type', 'text') ); }; - NotebookList.prototype.add_notebook_data = function (data, item) { - item.data('nbdata', data); + NotebookList.prototype.add_file_data = function (data, item) { + item.data('filedata', data); }; @@ -314,8 +311,8 @@ define([ click(function (e) { // $(this) is the button that was clicked. var that = $(this); - // We use the nbname and notebook_id from the parent notebook_item element's - // data because the outer scopes values change as we iterate through the loop. + // We use the filename from the parent list_item element's + // data because the outer scope's values change as we iterate through the loop. var parent_item = that.parents('div.list_item'); var name = parent_item.data('name'); var message = 'Are you sure you want to permanently delete the file: ' + name + '?'; @@ -354,32 +351,55 @@ define([ }; - NotebookList.prototype.add_upload_button = function (item) { + NotebookList.prototype.add_upload_button = function (item, type) { var that = this; var upload_button = $('