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()