diff --git a/IPython/html/notebookapp.py b/IPython/html/notebookapp.py
index efc79f8..98a8a5a 100644
--- a/IPython/html/notebookapp.py
+++ b/IPython/html/notebookapp.py
@@ -529,7 +529,6 @@ class NotebookApp(BaseIPythonApplication):
)
kls = import_item(self.notebook_manager_class)
self.notebook_manager = kls(parent=self, log=self.log)
- self.notebook_manager.load_notebook_names('')
self.session_manager = SessionManager(parent=self, log=self.log)
self.cluster_manager = ClusterManager(parent=self, log=self.log)
self.cluster_manager.update_profiles()
diff --git a/IPython/html/services/notebooks/filenbmanager.py b/IPython/html/services/notebooks/filenbmanager.py
index 326bf2f..17fc9a9 100644
--- a/IPython/html/services/notebooks/filenbmanager.py
+++ b/IPython/html/services/notebooks/filenbmanager.py
@@ -3,6 +3,7 @@
Authors:
* Brian Granger
+* Zach Sailer
"""
#-----------------------------------------------------------------------------
@@ -74,55 +75,40 @@ class FileNotebookManager(NotebookManager):
filename_ext = Unicode(u'.ipynb')
-
def get_notebook_names(self, path):
"""List all notebook names in the notebook dir."""
names = glob.glob(self.get_os_path('*'+self.filename_ext, path))
names = [os.path.basename(name)
for name in names]
return names
-
- def list_notebooks(self, path):
- """List all notebooks in the notebook dir."""
- notebook_names = self.get_notebook_names(path)
- notebooks = []
- for name in notebook_names:
- model = self.notebook_model(name, path, content=False)
- notebooks.append(model)
- return notebooks
- def update_notebook(self, data, notebook_name, notebook_path='/'):
- """Changes notebook"""
- changes = data.keys()
- for change in changes:
- full_path = self.get_os_path(notebook_name, notebook_path)
- if change == "name":
- new_path = self.get_os_path(data['name'], notebook_path)
- if not os.path.isfile(new_path):
- os.rename(full_path,
- self.get_os_path(data['name'], notebook_path))
- notebook_name = data['name']
- else:
- raise web.HTTPError(409, u'Notebook name already exists.')
- if change == "path":
- new_path = self.get_os_path(data['name'], data['path'])
- stutil.move(full_path, new_path)
- notebook_path = data['path']
- if change == "content":
- self.save_notebook(data, notebook_name, notebook_path)
- model = self.notebook_model(notebook_name, notebook_path)
- return model
+ def increment_filename(self, basename, path='/'):
+ """Return a non-used filename of the form basename.
+
+ This searches through the filenames (basename0, basename1, ...)
+ until is find one that is not already being used. It is used to
+ create Untitled and Copy names that are unique.
+ """
+ i = 0
+ while True:
+ name = u'%s%i.ipynb' % (basename,i)
+ os_path = self.get_os_path(name, path)
+ if not os.path.isfile(os_path):
+ break
+ else:
+ i = i+1
+ return name
def notebook_exists(self, name, path):
"""Returns a True if the notebook exists. Else, returns False.
-
+
Parameters
----------
name : string
The name of the notebook you are checking.
path : string
The relative path to the notebook (with '/' as separator)
-
+
Returns
-------
bool
@@ -130,218 +116,218 @@ class FileNotebookManager(NotebookManager):
path = self.get_os_path(name, path)
return os.path.isfile(path)
- def read_notebook_object_from_path(self, path):
+ def list_notebooks(self, path):
+ """List all notebooks in the notebook dir."""
+ notebook_names = self.get_notebook_names(path)
+ notebooks = []
+ for name in notebook_names:
+ model = self.get_notebook_model(name, path, content=False)
+ notebooks.append(model)
+ notebooks = sorted(notebooks, key=lambda item: item['name'])
+ return notebooks
+
+ def get_notebook_model(self, name, path='/', content=True):
"""read a notebook object from a path"""
- info = os.stat(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' % name)
+ info = os.stat(os_path)
last_modified = tz.utcfromtimestamp(info.st_mtime)
- with open(path,'r') as f:
- s = f.read()
- try:
- # v1 and v2 and json in the .ipynb files.
- nb = current.reads(s, u'json')
- except ValueError as e:
- msg = u"Unreadable Notebook: %s" % e
- raise web.HTTPError(400, msg, reason=msg)
- return last_modified, nb
-
- def read_notebook_object(self, notebook_name, notebook_path='/'):
- """Get the Notebook representation of a notebook by notebook_name."""
- path = self.get_os_path(notebook_name, notebook_path)
- if not os.path.isfile(path):
- raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_name)
- last_modified, nb = self.read_notebook_object_from_path(path)
- # Always use the filename as the notebook name.
- # Eventually we will get rid of the notebook name in the metadata
- # but for now, that name is just an empty string. Until the notebooks
- # web service knows about names in URLs we still pass the name
- # back to the web app using the metadata though.
- nb.metadata.name = os.path.splitext(os.path.basename(path))[0]
- return last_modified, nb
-
- def write_notebook_object(self, nb, notebook_name=None, notebook_path='/', new_name= None):
- """Save an existing notebook object by notebook_name."""
- if new_name == None:
- try:
- new_name = normalize('NFC', nb.metadata.name)
- except AttributeError:
- raise web.HTTPError(400, u'Missing notebook name')
+ # Create the notebook model.
+ model ={}
+ model['name'] = name
+ model['path'] = path
+ model['last_modified'] = last_modified.ctime()
+ if content is True:
+ with open(os_path,'r') as f:
+ s = f.read()
+ try:
+ # v1 and v2 and json in the .ipynb files.
+ nb = current.reads(s, u'json')
+ except ValueError as e:
+ raise web.HTTPError(400, u"Unreadable Notebook: %s" % e)
+ model['content'] = nb
+ return model
- new_path = notebook_path
- old_name = notebook_name
- old_checkpoints = self.list_checkpoints(old_name)
-
- path = self.get_os_path(new_name, new_path)
-
- # Right before we save the notebook, we write an empty string as the
- # notebook name in the metadata. This is to prepare for removing
- # this attribute entirely post 1.0. The web app still uses the metadata
- # name for now.
- nb.metadata.name = u''
+ def save_notebook_model(self, model, name, path='/'):
+ """Save the notebook model and return the model with no content."""
+
+ if 'content' not in model:
+ raise web.HTTPError(400, u'No notebook JSON data provided')
+
+ new_path = model.get('path', path)
+ new_name = model.get('name', name)
+
+ if path != new_path or name != new_name:
+ self.rename_notebook(name, path, new_name, new_path)
+ # Save the notebook file
+ ospath = self.get_os_path(new_name, new_path)
+ nb = model['content']
+ if 'name' in nb['metadata']:
+ nb['metadata']['name'] = u''
try:
- self.log.debug("Autosaving notebook %s", path)
- with open(path,'w') as f:
+ self.log.debug("Autosaving notebook %s", ospath)
+ with open(ospath,'w') as f:
current.write(nb, f, u'json')
except Exception as e:
- raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s' % e)
+ #raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s' % ospath)
+ raise e
- # save .py script as well
+ # Save .py script as well
if self.save_script:
pypath = os.path.splitext(path)[0] + '.py'
self.log.debug("Writing script %s", pypath)
try:
- with io.open(pypath,'w', encoding='utf-8') as f:
- current.write(nb, f, u'py')
+ with io.open(pypath, 'w', encoding='utf-8') as f:
+ current.write(model, f, u'py')
except Exception as e:
- raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s' % e)
-
- if old_name != None:
- # remove old files if the name changed
- if old_name != new_name:
- # remove renamed original, if it exists
- old_path = self.get_os_path(old_name, notebook_path)
- if os.path.isfile(old_path):
- self.log.debug("unlinking notebook %s", old_path)
- os.unlink(old_path)
-
- # cleanup old script, if it exists
- if self.save_script:
- old_pypath = os.path.splitext(old_path)[0] + '.py'
- if os.path.isfile(old_pypath):
- self.log.debug("unlinking script %s", old_pypath)
- os.unlink(old_pypath)
-
- # rename checkpoints to follow file
- for cp in old_checkpoints:
- checkpoint_id = cp['checkpoint_id']
- old_cp_path = self.get_checkpoint_path_by_name(old_name, checkpoint_id)
- new_cp_path = self.get_checkpoint_path_by_name(new_name, checkpoint_id)
- if os.path.isfile(old_cp_path):
- self.log.debug("renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
- os.rename(old_cp_path, new_cp_path)
-
- return new_name
-
- def delete_notebook(self, notebook_name, notebook_path):
- """Delete notebook by notebook_name."""
- nb_path = self.get_os_path(notebook_name, notebook_path)
+ raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s' % pypath)
+
+ model = self.get_notebook_model(name, path, content=False)
+ return model
+
+ def update_notebook_model(self, model, name, path='/'):
+ """Update the notebook's path and/or name"""
+ new_name = model.get('name', name)
+ new_path = model.get('path', path)
+ if path != new_path or name != new_name:
+ self.rename_notebook(name, path, new_name, new_path)
+ model = self.get_notebook_model(new_name, new_path, content=False)
+ return model
+
+ def delete_notebook_model(self, name, path='/'):
+ """Delete notebook by name and path."""
+ nb_path = self.get_os_path(name, path)
if not os.path.isfile(nb_path):
- raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_name)
+ raise web.HTTPError(404, u'Notebook does not exist: %s' % nb_path)
# clear checkpoints
- for checkpoint in self.list_checkpoints(notebook_name):
+ for checkpoint in self.list_checkpoints(name):
checkpoint_id = checkpoint['checkpoint_id']
- path = self.get_checkpoint_path(notebook_name, checkpoint_id)
- self.log.debug(path)
- if os.path.isfile(path):
- self.log.debug("unlinking checkpoint %s", path)
- os.unlink(path)
+ cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
+ self.log.debug(cp_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", nb_path)
+ self.log.debug("Unlinking notebook %s", nb_path)
os.unlink(nb_path)
- def increment_filename(self, basename, notebook_path='/'):
- """Return a non-used filename of the form basename.
+ def rename_notebook(self, old_name, old_path, new_name, new_path):
+ """Rename a notebook."""
+ if new_name == old_name and new_path == old_path:
+ return
- This searches through the filenames (basename0, basename1, ...)
- until is find one that is not already being used. It is used to
- create Untitled and Copy names that are unique.
- """
- i = 0
- while True:
- name = u'%s%i.ipynb' % (basename,i)
- path = self.get_os_path(name, notebook_path)
- if not os.path.isfile(path):
- break
- else:
- i = i+1
- return name
-
+ new_full_path = self.get_os_path(new_name, new_path)
+ old_full_path = self.get_os_path(old_name, old_path)
+
+ # Should we proceed with the move?
+ if os.path.isfile(new_full_path):
+ raise web.HTTPError(409, u'Notebook with name already exists: ' % new_full_path)
+ if self.save_script:
+ old_pypath = os.path.splitext(old_full_path)[0] + '.py'
+ new_pypath = os.path.splitext(new_full_path)[0] + '.py'
+ if os.path.isfile(new_pypath):
+ raise web.HTTPError(409, u'Python script with name already exists: %s' % new_pypath)
+
+ # Move the notebook file
+ try:
+ os.rename(old_full_path, new_full_path)
+ except:
+ raise web.HTTPError(400, u'Unknown error renaming notebook: %s' % old_full_path)
+
+ # Move the checkpoints
+ old_checkpoints = self.list_checkpoints(old_name, old_path)
+ for cp in old_checkpoints:
+ checkpoint_id = cp['checkpoint_id']
+ old_cp_path = self.get_checkpoint_path(checkpoint_id, old_name, path)
+ new_cp_path = self.get_checkpoint_path(checkpoint_id, new_name, path)
+ if os.path.isfile(old_cp_path):
+ self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
+ os.rename(old_cp_path, new_cp_path)
+
+ # Move the .py script
+ if self.save_script:
+ os.rename(old_pypath, new_pypath)
+
# Checkpoint-related utilities
- def get_checkpoint_path_by_name(self, name, checkpoint_id, notebook_path='/'):
- """Return a full path to a notebook checkpoint, given its name and checkpoint id."""
+ def get_checkpoint_path(self, checkpoint_id, name, path='/'):
+ """find the path to a checkpoint"""
filename = u"{name}-{checkpoint_id}{ext}".format(
name=name,
checkpoint_id=checkpoint_id,
ext=self.filename_ext,
)
- if notebook_path ==None:
- path = os.path.join(self.checkpoint_dir, filename)
- else:
- path = os.path.join(notebook_path, self.checkpoint_dir, filename)
- return path
-
- def get_checkpoint_path(self, notebook_name, checkpoint_id, notebook_path='/'):
- """find the path to a checkpoint"""
- name = notebook_name
- return self.get_checkpoint_path_by_name(name, checkpoint_id, notebook_path)
-
- def get_checkpoint_info(self, notebook_name, checkpoint_id, notebook_path='/'):
+ cp_path = os.path.join(path, self.checkpoint_dir, filename)
+ return cp_path
+
+ def get_checkpoint_model(self, checkpoint_id, name, path='/'):
"""construct the info dict for a given checkpoint"""
- path = self.get_checkpoint_path(notebook_name, checkpoint_id, notebook_path)
- stats = os.stat(path)
+ cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
+ stats = os.stat(cp_path)
last_modified = tz.utcfromtimestamp(stats.st_mtime)
info = dict(
checkpoint_id = checkpoint_id,
last_modified = last_modified,
)
-
return info
# public checkpoint API
- def create_checkpoint(self, notebook_name, notebook_path='/'):
+ def create_checkpoint(self, name, path='/'):
"""Create a checkpoint from the current state of a notebook"""
- nb_path = self.get_os_path(notebook_name, notebook_path)
+ nb_path = self.get_os_path(name, path)
# only the one checkpoint ID:
checkpoint_id = u"checkpoint"
- cp_path = self.get_checkpoint_path(notebook_name, checkpoint_id, notebook_path)
- self.log.debug("creating checkpoint for notebook %s", notebook_name)
+ cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
+ self.log.debug("creating checkpoint for notebook %s", name)
if not os.path.exists(self.checkpoint_dir):
os.mkdir(self.checkpoint_dir)
shutil.copy2(nb_path, cp_path)
# return the checkpoint info
- return self.get_checkpoint_info(notebook_name, checkpoint_id, notebook_path)
+ return self.get_checkpoint_model(checkpoint_id, name, path)
- def list_checkpoints(self, notebook_name, notebook_path='/'):
+ def list_checkpoints(self, name, path='/'):
"""list the checkpoints for a given notebook
This notebook manager currently only supports one checkpoint per notebook.
"""
checkpoint_id = "checkpoint"
- path = self.get_checkpoint_path(notebook_name, checkpoint_id, notebook_path)
+ path = self.get_checkpoint_path(checkpoint_id, name, path)
if not os.path.exists(path):
return []
else:
- return [self.get_checkpoint_info(notebook_name, checkpoint_id, notebook_path)]
+ return [self.get_checkpoint_model(checkpoint_id, name, path)]
- def restore_checkpoint(self, notebook_name, checkpoint_id, notebook_path='/'):
+ def restore_checkpoint(self, checkpoint_id, name, path='/'):
"""restore a notebook to a checkpointed state"""
- self.log.info("restoring Notebook %s from checkpoint %s", notebook_name, checkpoint_id)
- nb_path = self.get_os_path(notebook_name, notebook_path)
- cp_path = self.get_checkpoint_path(notebook_name, checkpoint_id, notebook_path)
+ self.log.info("restoring Notebook %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' % (notebook_name, checkpoint_id)
+ u'Notebook checkpoint does not exist: %s-%s' % (name, checkpoint_id)
)
# ensure notebook is readable (never restore from an unreadable notebook)
- last_modified, nb = self.read_notebook_object_from_path(cp_path)
+ with file(cp_path, 'r') as f:
+ nb = current.read(f, u'json')
shutil.copy2(cp_path, nb_path)
self.log.debug("copying %s -> %s", cp_path, nb_path)
- def delete_checkpoint(self, notebook_name, checkpoint_id, notebook_path='/'):
+ def delete_checkpoint(self, checkpoint_id, name, path='/'):
"""delete a notebook's checkpoint"""
- path = self.get_checkpoint_path(notebook_name, checkpoint_id, notebook_path)
- if not os.path.isfile(path):
+ 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' % (notebook_name, checkpoint_id)
+ u'Notebook checkpoint does not exist: %s-%s' % (name, checkpoint_id)
)
- self.log.debug("unlinking %s", path)
- os.unlink(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/nbmanager.py b/IPython/html/services/notebooks/nbmanager.py
index 4ac2598..4ddbae8 100644
--- a/IPython/html/services/notebooks/nbmanager.py
+++ b/IPython/html/services/notebooks/nbmanager.py
@@ -3,6 +3,7 @@
Authors:
* Brian Granger
+* Zach Sailer
"""
#-----------------------------------------------------------------------------
@@ -18,10 +19,11 @@ Authors:
import os
import uuid
+from urllib import quote, unquote
from tornado import web
-from urllib import quote, unquote
+from IPython.html.utils import url_path_join
from IPython.config.configurable import LoggingConfigurable
from IPython.nbformat import current
from IPython.utils.traitlets import List, Dict, Unicode, TraitError
@@ -42,10 +44,13 @@ class NotebookManager(LoggingConfigurable):
The directory to use for notebooks.
""")
+ filename_ext = Unicode(u'.ipynb')
+
def named_notebook_path(self, notebook_path):
- """Given a notebook_path name, returns a (name, path) tuple, where
- name is a .ipynb file, and path is the directory for the file, which
- *always* starts *and* ends with a '/' character.
+ """Given notebook_path (*always* a URL path to notebook), returns a
+ (name, path) tuple, where name is a .ipynb file, and path is the
+ URL path that describes the file system path for the file.
+ It *always* starts *and* ends with a '/' character.
Parameters
----------
@@ -73,7 +78,7 @@ class NotebookManager(LoggingConfigurable):
return name, path
def get_os_path(self, fname=None, path='/'):
- """Given a notebook name and a server URL path, return its file system
+ """Given a notebook name and a URL path, return its file system
path.
Parameters
@@ -99,21 +104,23 @@ class NotebookManager(LoggingConfigurable):
return path
def url_encode(self, path):
- """Returns the path with all special characters URL encoded"""
- parts = os.path.split(path)
- return os.path.join(*[quote(p) for p in parts])
+ """Takes a URL path with special characters and returns
+ the path with all these characters URL encoded"""
+ parts = path.split('/')
+ return '/'.join([quote(p) for p in parts])
def url_decode(self, path):
- """Returns the URL with special characters decoded"""
- parts = os.path.split(path)
- return os.path.join(*[unquote(p) for p in parts])
+ """Takes a URL path with encoded special characters and
+ returns the URL with special characters decoded"""
+ parts = path.split('/')
+ return '/'.join([unquote(p) for p in parts])
- def _notebook_dir_changed(self, new):
- """do a bit of validation of the notebook dir"""
+ def _notebook_dir_changed(self, name, old, new):
+ """Do a bit of validation of the notebook dir."""
if not os.path.isabs(new):
# If we receive a non-absolute path, make it absolute.
abs_new = os.path.abspath(new)
- #self.notebook_dir = os.path.dirname(abs_new)
+ self.notebook_dir = os.path.dirname(abs_new)
return
if os.path.exists(new) and not os.path.isdir(new):
raise TraitError("notebook dir %r is not a directory" % new)
@@ -123,27 +130,23 @@ class NotebookManager(LoggingConfigurable):
os.mkdir(new)
except:
raise TraitError("Couldn't create notebook dir %r" % new)
-
- allowed_formats = List([u'json',u'py'])
- def add_new_folder(self, path=None):
- new_path = os.path.join(self.notebook_dir, path)
- if not os.path.exists(new_path):
- os.makedirs(new_path)
- else:
- raise web.HTTPError(409, u'Directory already exists or creation permission not allowed.')
+ # Main notebook API
- def load_notebook_names(self, path):
- """Load the notebook names into memory.
-
- This should be called once immediately after the notebook manager
- is created to load the existing notebooks into the mapping in
- memory.
+ def increment_filename(self, basename, path='/'):
+ """Increment a notebook filename without the .ipynb to make it unique.
+
+ Parameters
+ ----------
+ basename : unicode
+ The name of a notebook without the ``.ipynb`` file extension.
+ path : unicode
+ The URL path of the notebooks directory
"""
- self.list_notebooks(path)
+ return basename
def list_notebooks(self):
- """List all notebooks.
+ """Return a list of notebook dicts without content.
This returns a list of dicts, each of the form::
@@ -155,142 +158,62 @@ class NotebookManager(LoggingConfigurable):
"""
raise NotImplementedError('must be implemented in a subclass')
- def notebook_model(self, name, path='/', content=True):
- """ Creates the standard notebook model """
- last_modified, contents = self.read_notebook_model(name, path)
- model = {"name": name,
- "path": path,
- "last_modified": last_modified.ctime()}
- if content is True:
- model['content'] = contents
- return model
-
- def get_notebook(self, notebook_name, notebook_path='/', format=u'json'):
- """Get the representation of a notebook in format by notebook_name."""
- format = unicode(format)
- if format not in self.allowed_formats:
- raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
- kwargs = {}
- last_mod, nb = self.read_notebook_object(notebook_name, notebook_path)
- if format == 'json':
- # don't split lines for sending over the wire, because it
- # should match the Python in-memory format.
- kwargs['split_lines'] = False
- representation = current.writes(nb, format, **kwargs)
- name = nb.metadata.get('name', 'notebook')
- return last_mod, representation, name
-
- def read_notebook_model(self, notebook_name, notebook_path='/'):
- """Get the object representation of a notebook by notebook_id."""
+ def get_notebook_model(self, name, path='/', content=True):
+ """Get the notebook model with or without content."""
raise NotImplementedError('must be implemented in a subclass')
- def save_notebook(self, model, name=None, path='/'):
- """Save the Notebook"""
- if name is None:
- name = self.increment_filename('Untitled', path)
- if 'content' not in model:
- metadata = current.new_metadata(name=name)
- nb = current.new_notebook(metadata=metadata)
- else:
- nb = model['content']
- self.write_notebook_object()
-
-
- def save_new_notebook(self, data, notebook_path='/', name=None, format=u'json'):
- """Save a new notebook and return its name.
+ def save_notebook_model(self, model, name, path='/'):
+ """Save the notebook model and return the model with no content."""
+ raise NotImplementedError('must be implemented in a subclass')
- If a name is passed in, it overrides any values in the notebook data
- and the value in the data is updated to use that value.
- """
- if format not in self.allowed_formats:
- raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
-
- try:
- nb = current.reads(data.decode('utf-8'), format)
- except:
- raise web.HTTPError(400, u'Invalid JSON data')
-
- if name is None:
- try:
- name = nb.metadata.name
- except AttributeError:
- raise web.HTTPError(400, u'Missing notebook name')
- nb.metadata.name = name
-
- notebook_name = self.write_notebook_object(nb, notebook_path=notebook_path)
- return notebook_name
-
- def save_notebook(self, data, notebook_path='/', name=None, format=u'json'):
- """Save an existing notebook by notebook_name."""
- if format not in self.allowed_formats:
- raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
-
- try:
- nb = current.reads(data.decode('utf-8'), format)
- except:
- raise web.HTTPError(400, u'Invalid JSON data')
-
- if name is not None:
- nb.metadata.name = name
- self.write_notebook_object(nb, name, notebook_path, new_name)
-
- def write_notebook_model(self, model):
- """Write a notebook object and return its notebook_name.
-
- If notebook_name is None, this method should create a new notebook_name.
- If notebook_name is not None, this method should check to make sure it
- exists and is valid.
- """
+ def update_notebook_model(self, model, name, path='/'):
+ """Update the notebook model and return the model with no content."""
raise NotImplementedError('must be implemented in a subclass')
- def delete_notebook(self, notebook_name, notebook_path):
- """Delete notebook by notebook_id."""
+ def delete_notebook_model(self, name, path):
+ """Delete notebook by name and path."""
raise NotImplementedError('must be implemented in a subclass')
- def increment_filename(self, name):
- """Increment a filename to make it unique.
+ def create_notebook_model(self, model=None, path='/'):
+ """Create a new untitled notebook and return its model with no content."""
+ name = self.increment_filename('Untitled', path)
+ if model is None:
+ model = {}
+ metadata = current.new_metadata(name=u'')
+ nb = current.new_notebook(metadata=metadata)
+ model['content'] = nb
+ model['name'] = name
+ model['path'] = path
+ model = self.save_notebook_model(model, name, path)
+ return model
- This exists for notebook stores that must have unique names. When a notebook
- is created or copied this method constructs a unique filename, typically
- by appending an integer to the name.
- """
- return name
-
- def new_notebook(self, notebook_path='/'):
- """Create a new notebook and return its notebook_name."""
- name = self.increment_filename('Untitled', notebook_path)
- metadata = current.new_metadata(name=name)
- nb = current.new_notebook(metadata=metadata)
- notebook_name = self.write_notebook_object(nb, notebook_path=notebook_path)
- return notebook_name
-
- def copy_notebook(self, name, path='/'):
- """Copy an existing notebook and return its new notebook_name."""
- last_mod, nb = self.read_notebook_object(name, path)
- name = nb.metadata.name + '-Copy'
- name = self.increment_filename(name, path)
- nb.metadata.name = name
- notebook_name = self.write_notebook_object(nb, notebook_path = path)
- return notebook_name
+ def copy_notebook(self, name, path='/', content=False):
+ """Copy an existing notebook and return its new model."""
+ model = self.get_notebook_model(name, path)
+ name = os.path.splitext(name)[0] + '-Copy'
+ name = self.increment_filename(name, path) + self.filename_ext
+ model['name'] = name
+ model = self.save_notebook_model(model, name, path, content=content)
+ return model
# Checkpoint-related
- def create_checkpoint(self, notebook_name, notebook_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, notebook_name, notebook_path='/'):
+ def list_checkpoints(self, name, path='/'):
"""Return a list of checkpoints for a given notebook"""
return []
- def restore_checkpoint(self, notebook_name, checkpoint_id, notebook_path='/'):
+ def restore_checkpoint(self, checkpoint_id, name, path='/'):
"""Restore a notebook from one of its checkpoints"""
raise NotImplementedError("must be implemented in a subclass")
- def delete_checkpoint(self, notebook_name, checkpoint_id, notebook_path='/'):
+ def delete_checkpoint(self, checkpoint_id, name, path='/'):
"""delete a checkpoint for a notebook"""
raise NotImplementedError("must be implemented in a subclass")
@@ -298,4 +221,4 @@ class NotebookManager(LoggingConfigurable):
self.log.info(self.info_string())
def info_string(self):
- return "Serving notebooks"
+ return "Serving notebooks"
\ No newline at end of file
diff --git a/IPython/html/services/notebooks/tests/test_nbmanager.py b/IPython/html/services/notebooks/tests/test_nbmanager.py
index ec3acb2..1b5d0a4 100644
--- a/IPython/html/services/notebooks/tests/test_nbmanager.py
+++ b/IPython/html/services/notebooks/tests/test_nbmanager.py
@@ -1,11 +1,14 @@
"""Tests for the notebook manager."""
import os
+
+from tornado.web import HTTPError
from unittest import TestCase
from tempfile import NamedTemporaryFile
from IPython.utils.tempdir import TemporaryDirectory
from IPython.utils.traitlets import TraitError
+from IPython.html.utils import url_path_join
from ..filenbmanager import FileNotebookManager
from ..nbmanager import NotebookManager
@@ -54,6 +57,16 @@ class TestFileNotebookManager(TestCase):
self.assertEqual(path, fs_path)
class TestNotebookManager(TestCase):
+
+ 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"""
+ os_path = os.path.join(abs_path, rel_path)
+ try:
+ os.makedirs(os_path)
+ except OSError:
+ print "Directory already exists."
+
def test_named_notebook_path(self):
nm = NotebookManager()
@@ -98,14 +111,156 @@ class TestNotebookManager(TestCase):
def test_url_decode(self):
nm = NotebookManager()
-
+
# decodes a url string to a plain string
# these tests decode paths with spaces
path = nm.url_decode('/this%20is%20a%20test/for%20spaces/')
self.assertEqual(path, '/this is a test/for spaces/')
-
+
path = nm.url_decode('notebook%20with%20space.ipynb')
self.assertEqual(path, 'notebook with space.ipynb')
-
+
path = nm.url_decode('/path%20with%20a/notebook%20and%20space.ipynb')
self.assertEqual(path, '/path with a/notebook and space.ipynb')
+
+ def test_create_notebook_model(self):
+ with TemporaryDirectory() as td:
+ # Test in root directory
+ nm = FileNotebookManager(notebook_dir=td)
+ model = nm.create_notebook_model()
+ assert isinstance(model, dict)
+ self.assertIn('name', model)
+ self.assertIn('path', model)
+ self.assertEqual(model['name'], 'Untitled0.ipynb')
+ self.assertEqual(model['path'], '/')
+
+ # Test in sub-directory
+ sub_dir = '/foo/'
+ self.make_dir(nm.notebook_dir, 'foo')
+ model = nm.create_notebook_model(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)
+
+ def test_get_notebook_model(self):
+ with TemporaryDirectory() as td:
+ # Test in root directory
+ # Create a notebook
+ nm = FileNotebookManager(notebook_dir=td)
+ model = nm.create_notebook_model()
+ name = model['name']
+ path = model['path']
+
+ # Check that we 'get' on the notebook we just created
+ model2 = nm.get_notebook_model(name, path)
+ assert isinstance(model2, dict)
+ self.assertIn('name', model2)
+ self.assertIn('path', model2)
+ self.assertEqual(model['name'], name)
+ self.assertEqual(model['path'], path)
+
+ # Test in sub-directory
+ sub_dir = '/foo/'
+ self.make_dir(nm.notebook_dir, 'foo')
+ model = nm.create_notebook_model(None, sub_dir)
+ model2 = nm.get_notebook_model(name, sub_dir)
+ assert isinstance(model2, dict)
+ self.assertIn('name', model2)
+ self.assertIn('path', model2)
+ self.assertIn('content', model2)
+ self.assertEqual(model2['name'], 'Untitled0.ipynb')
+ self.assertEqual(model2['path'], sub_dir)
+
+ def test_update_notebook_model(self):
+ with TemporaryDirectory() as td:
+ # Test in root directory
+ # Create a notebook
+ nm = FileNotebookManager(notebook_dir=td)
+ model = nm.create_notebook_model()
+ name = model['name']
+ path = model['path']
+
+ # Change the name in the model for rename
+ model['name'] = 'test.ipynb'
+ model = nm.update_notebook_model(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_model, 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_model(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(model, name, path)
+ assert isinstance(model, dict)
+ self.assertIn('name', model)
+ self.assertIn('path', model)
+ self.assertEqual(model['name'], 'test_in_sub.ipynb')
+ self.assertEqual(model['path'], sub_dir)
+
+ # Make sure the old name is gone
+ self.assertRaises(HTTPError, nm.get_notebook_model, name, path)
+
+ def test_save_notebook_model(self):
+ with TemporaryDirectory() as td:
+ # Test in the root directory
+ # Create a notebook
+ nm = FileNotebookManager(notebook_dir=td)
+ model = nm.create_notebook_model()
+ name = model['name']
+ path = model['path']
+
+ # Get the model with 'content'
+ full_model = nm.get_notebook_model(name, path)
+
+ # Save the notebook
+ model = nm.save_notebook_model(full_model, name, path)
+ assert isinstance(model, dict)
+ self.assertIn('name', model)
+ self.assertIn('path', model)
+ self.assertEqual(model['name'], name)
+ self.assertEqual(model['path'], 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_model(None, sub_dir)
+ name = model['name']
+ path = model['path']
+ model = nm.get_notebook_model(name, path)
+
+ # Change the name in the model for rename
+ model = nm.save_notebook_model(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)
+
+ def test_delete_notebook_model(self):
+ with TemporaryDirectory() as td:
+ # Test in the root directory
+ # Create a notebook
+ nm = FileNotebookManager(notebook_dir=td)
+ model = nm.create_notebook_model()
+ name = model['name']
+ path = model['path']
+
+ # Delete the notebook
+ nm.delete_notebook_model(name, path)
+
+ # Check that a 'get' on the deleted notebook raises and error
+ self.assertRaises(HTTPError, nm.get_notebook_model, name, path)
diff --git a/IPython/html/services/notebooks/tests/test_notebooks_api.py b/IPython/html/services/notebooks/tests/test_notebooks_api.py
index 6d44a93..aff2075 100644
--- a/IPython/html/services/notebooks/tests/test_notebooks_api.py
+++ b/IPython/html/services/notebooks/tests/test_notebooks_api.py
@@ -29,7 +29,8 @@ class APITest(NotebookTestBase):
# POST a notebook and test the dict thats returned.
#url, nb = self.mknb()
url = self.notebook_url()
- nb = requests.post(url)
+ nb = requests.post(url+'/')
+ print nb.text
data = nb.json()
assert isinstance(data, dict)
self.assertIn('name', data)
@@ -50,7 +51,6 @@ class APITest(NotebookTestBase):
url = self.notebook_url() + '/Untitled0.ipynb'
r = requests.get(url)
assert isinstance(data, dict)
- self.assertEqual(r.json(), data)
# PATCH (rename) request.
new_name = {'name':'test.ipynb'}
@@ -62,7 +62,6 @@ class APITest(NotebookTestBase):
new_url = self.notebook_url() + '/test.ipynb'
r = requests.get(new_url)
assert isinstance(r.json(), dict)
- self.assertEqual(r.json(), data)
# GET bad (old) notebook name.
r = requests.get(url)
@@ -91,9 +90,7 @@ class APITest(NotebookTestBase):
r = requests.get(url+'/Untitled0.ipynb')
r2 = requests.get(url2+'/Untitled0.ipynb')
assert isinstance(r.json(), dict)
- self.assertEqual(r.json(), data)
assert isinstance(r2.json(), dict)
- self.assertEqual(r2.json(), data2)
# PATCH notebooks that are one and two levels down.
new_name = {'name': 'testfoo.ipynb'}
diff --git a/IPython/html/services/sessions/handlers.py b/IPython/html/services/sessions/handlers.py
index aa0dd3a..68d93f7 100644
--- a/IPython/html/services/sessions/handlers.py
+++ b/IPython/html/services/sessions/handlers.py
@@ -80,6 +80,7 @@ class SessionHandler(IPythonHandler):
sm = self.session_manager
nbm = self.notebook_manager
km = self.kernel_manager
+ data = self.request.body
data = jsonapi.loads(self.request.body)
name, path = nbm.named_notebook_path(data['notebook_path'])
sm.update_session(session_id, name=name)
diff --git a/IPython/html/static/services/sessions/js/session.js b/IPython/html/static/services/sessions/js/session.js
index 9a1130f..328d702 100644
--- a/IPython/html/static/services/sessions/js/session.js
+++ b/IPython/html/static/services/sessions/js/session.js
@@ -32,7 +32,7 @@ var IPython = (function (IPython) {
Session.prototype.notebook_rename = function (notebook_path) {
this.notebook_path = notebook_path;
- name = {'notebook_path': notebook_path}
+ var name = {'notebook_path': notebook_path}
var settings = {
processData : false,
cache : false,
@@ -44,7 +44,6 @@ var IPython = (function (IPython) {
$.ajax(url, settings);
}
-
Session.prototype.delete_session = function() {
var settings = {
processData : false,
diff --git a/IPython/html/static/tree/js/notebooklist.js b/IPython/html/static/tree/js/notebooklist.js
index 464a087..4f82af9 100644
--- a/IPython/html/static/tree/js/notebooklist.js
+++ b/IPython/html/static/tree/js/notebooklist.js
@@ -343,7 +343,7 @@ var IPython = (function (IPython) {
window.open(this.baseProjectUrl() +'notebooks' + this.notebookPath()+ notebook_name, '_blank');
}, this)
};
- var url = this.baseProjectUrl() + 'notebooks' + path;
+ var url = this.baseProjectUrl() + 'api/notebooks' + path;
$.ajax(url,settings);
};
diff --git a/IPython/html/tests/launchnotebook.py b/IPython/html/tests/launchnotebook.py
index 0cc195c..297e635 100644
--- a/IPython/html/tests/launchnotebook.py
+++ b/IPython/html/tests/launchnotebook.py
@@ -16,7 +16,7 @@ class NotebookTestBase(TestCase):
and then starts the notebook server with a separate temp notebook_dir.
"""
- port = 1234
+ port = 12341
def wait_till_alive(self):
url = 'http://localhost:%i/' % self.port
@@ -48,10 +48,9 @@ class NotebookTestBase(TestCase):
'--no-browser',
'--ipython-dir=%s' % self.ipython_dir.name,
'--notebook-dir=%s' % self.notebook_dir.name
- ]
+ ]
self.notebook = Popen(notebook_args, stdout=PIPE, stderr=PIPE)
self.wait_till_alive()
- #time.sleep(3.0)
def tearDown(self):
self.notebook.terminate()