diff --git a/IPython/html/services/notebooks/handlers.py b/IPython/html/services/notebooks/handlers.py index 3a7d9d7..351c053 100644 --- a/IPython/html/services/notebooks/handlers.py +++ b/IPython/html/services/notebooks/handlers.py @@ -71,8 +71,7 @@ class NotebookHandler(IPythonHandler): @web.authenticated @json_errors def patch(self, path='', name=None): - """patch is currently used strictly for notebook renaming. - Changes the notebook name to the name given in data.""" + """PATCH renames a notebook without re-uploading content.""" nbm = self.notebook_manager if name is None: raise web.HTTPError(400, u'Notebook name missing') @@ -90,10 +89,31 @@ class NotebookHandler(IPythonHandler): @web.authenticated @json_errors def post(self, path='', name=None): - """Create a new notebook in the location given by 'notebook_path'.""" + """Create a new notebook in the specified path. + + POST creates new notebooks. + + POST /api/notebooks/path : new untitled notebook in path + POST /api/notebooks/path/notebook.ipynb : new notebook with name in path + If content specified upload notebook, otherwise start empty. + """ nbm = self.notebook_manager model = self.get_json_body() - model = nbm.create_notebook_model(model, path) + if name is None: + # creating new notebook, model doesn't make sense + if model is not None: + raise web.HTTPError(400, "Model not valid when creating untitled notebooks.") + model = nbm.create_notebook_model(path=path) + else: + if model is None: + self.log.info("Creating new Notebook at %s/%s", path, name) + model = {} + else: + self.log.info("Uploading Notebook to %s/%s", path, name) + # set the model name from the URL + model['name'] = name + model = nbm.create_notebook_model(model, path) + location = self.notebook_location(model[u'name'], model[u'path']) self.set_header(u'Location', location) self.set_header(u'Last-Modified', model[u'last_modified']) @@ -120,6 +140,29 @@ class NotebookHandler(IPythonHandler): self.set_status(204) self.finish() +class NotebookCopyHandler(IPythonHandler): + + SUPPORTED_METHODS = ('POST') + + @web.authenticated + @json_errors + def post(self, path='', name=None): + """Copy an existing notebook.""" + nbm = self.notebook_manager + model = self.get_json_body() + if name is None: + raise web.HTTPError(400, "Notebook name required") + self.log.info("Copying Notebook %s/%s", path, name) + model = nbm.copy_notebook(name, path) + location = url_path_join( + self.base_project_url, 'api', 'notebooks', + model['path'], model['name'], + ) + self.set_header(u'Location', location) + self.set_header(u'Last-Modified', model[u'last_modified']) + self.set_status(201) + self.finish(json.dumps(model, default=date_default)) + class NotebookCheckpointsHandler(IPythonHandler): @@ -180,6 +223,7 @@ _notebook_name_regex = r"(?P[^/]+\.ipynb)" _notebook_path_regex = "%s/%s" % (_path_regex, _notebook_name_regex) default_handlers = [ + (r"/api/notebooks/?%s/copy" % _notebook_path_regex, NotebookCopyHandler), (r"/api/notebooks/?%s/checkpoints" % _notebook_path_regex, NotebookCheckpointsHandler), (r"/api/notebooks/?%s/checkpoints/%s" % (_notebook_path_regex, _checkpoint_id_regex), ModifyNotebookCheckpointsHandler), diff --git a/IPython/html/services/notebooks/nbmanager.py b/IPython/html/services/notebooks/nbmanager.py index 2acbd57..ed01f43 100644 --- a/IPython/html/services/notebooks/nbmanager.py +++ b/IPython/html/services/notebooks/nbmanager.py @@ -87,7 +87,7 @@ class NotebookManager(LoggingConfigurable): """ return basename - def list_notebooks(self): + def list_notebooks(self, path=''): """Return a list of notebook dicts without content. This returns a list of dicts, each of the form:: @@ -112,53 +112,51 @@ class NotebookManager(LoggingConfigurable): """Update the notebook model and return the model with no content.""" raise NotImplementedError('must be implemented in a subclass') - def delete_notebook_model(self, name, path): + def delete_notebook_model(self, name, path=''): """Delete notebook by name and path.""" raise NotImplementedError('must be implemented in a subclass') def create_notebook_model(self, model=None, path=''): """Create a new untitled notebook and return its model with no content.""" - untitled = self.increment_filename('Untitled', path) if model is None: model = {} + if 'content' not in model: metadata = current.new_metadata(name=u'') - nb = current.new_notebook(metadata=metadata) - model['content'] = nb - model['name'] = name = untitled - model['path'] = path - else: - name = model.setdefault('name', untitled) - model['path'] = path - model = self.save_notebook_model(model, name, 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, model['name'], model['path']) return model - def copy_notebook(self, name, path='/', content=False): + def copy_notebook(self, name, path=''): """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) + model = self.save_notebook_model(model, name, path) return model # Checkpoint-related - def create_checkpoint(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='/'): + def list_checkpoints(self, name, path=''): """Return a list of checkpoints for a given notebook""" return [] - def restore_checkpoint(self, checkpoint_id, name, 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, 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") diff --git a/IPython/html/static/notebook/js/notebook.js b/IPython/html/static/notebook/js/notebook.js index 3fd4f50..e7a2b40 100644 --- a/IPython/html/static/notebook/js/notebook.js +++ b/IPython/html/static/notebook/js/notebook.js @@ -1768,15 +1768,12 @@ var IPython = (function (IPython) { Notebook.prototype.copy_notebook = function(){ var path = this.notebookPath(); - var name = {'name': this.notebook_name} var settings = { processData : false, cache : false, type : "POST", - data: JSON.stringify(name), - dataType : "json", success:$.proxy(function (data, status, xhr){ - notebook_name = data.name; + var notebook_name = data.name; window.open(utils.url_path_join( this._baseProjectUrl, 'notebooks', @@ -1787,8 +1784,10 @@ var IPython = (function (IPython) { }; var url = utils.url_path_join( this._baseProjectUrl, - 'notebooks', - path + 'api/notebooks', + path, + this.notebook_name, + 'copy' ); $.ajax(url,settings); }; diff --git a/IPython/html/static/tree/js/notebooklist.js b/IPython/html/static/tree/js/notebooklist.js index b10395f..69957eb 100644 --- a/IPython/html/static/tree/js/notebooklist.js +++ b/IPython/html/static/tree/js/notebooklist.js @@ -312,7 +312,7 @@ var IPython = (function (IPython) { var upload_button = $('