Skip to content
Browse files

Massive work on the notebook document format.

* Finished nbformat work and debugged the versioning API.
* Integrated the nbformat with the notebook. Save/New/Open/Export
  are all now working.
  • Loading branch information...
1 parent 540b3f1 commit 65666c0ba5cec93fc2e7479221239a1ce2216521 @ellisonbg ellisonbg committed Aug 4, 2011
View
93 IPython/frontend/html/notebook/handlers.py
@@ -4,23 +4,31 @@
# Imports
#-----------------------------------------------------------------------------
-import datetime
import json
import logging
-import os
import urllib
from tornado import web
from tornado import websocket
+
#-----------------------------------------------------------------------------
# Handlers
#-----------------------------------------------------------------------------
class MainHandler(web.RequestHandler):
def get(self):
- self.render('notebook.html')
+ notebook_id = self.application.notebook_manager.new_notebook()
+ self.render('notebook.html', notebook_id=notebook_id)
+
+
+class NamedNotebookHandler(web.RequestHandler):
+ def get(self, notebook_id):
+ nbm = self.application.notebook_manager
+ if not nbm.notebook_exists(notebook_id):
+ raise web.HTTPError(404)
+ self.render('notebook.html', notebook_id=notebook_id)
class KernelHandler(web.RequestHandler):
@@ -30,6 +38,7 @@ def get(self):
def post(self):
kernel_id = self.application.start_kernel()
+ self.set_header('Location', '/'+kernel_id)
self.write(json.dumps(kernel_id))
@@ -65,52 +74,52 @@ def on_close(self):
class NotebookRootHandler(web.RequestHandler):
def get(self):
- files = os.listdir(os.getcwd())
- files = [file for file in files if file.endswith(".ipynb")]
+ nbm = self.application.notebook_manager
+ files = nbm.list_notebooks()
self.write(json.dumps(files))
+ def post(self):
+ nbm = self.application.notebook_manager
+ body = self.request.body.strip()
+ format = self.get_argument('format', default='json')
+ if body:
+ notebook_id = nbm.save_new_notebook(body, format)
+ else:
+ notebook_id = nbm.new_notebook()
+ self.set_header('Location', '/'+notebook_id)
+ self.write(json.dumps(notebook_id))
-class NotebookHandler(web.RequestHandler):
-
- SUPPORTED_METHODS = ("GET", "DELETE", "PUT")
- def find_path(self, filename):
- filename = urllib.unquote(filename)
- if not filename.endswith('.ipynb'):
- raise web.HTTPError(400)
- path = os.path.join(os.getcwd(), filename)
- return path
+class NotebookHandler(web.RequestHandler):
- def get(self, filename):
- path = self.find_path(filename)
- if not os.path.isfile(path):
- raise web.HTTPError(404)
- info = os.stat(path)
- self.set_header("Content-Type", "application/unknown")
- self.set_header("Last-Modified", datetime.datetime.utcfromtimestamp(
- info.st_mtime))
- f = open(path, "r")
- try:
- self.finish(f.read())
- finally:
- f.close()
-
- def put(self, filename):
- path = self.find_path(filename)
- f = open(path, "w")
- f.write(self.request.body)
- f.close()
+ SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
+
+ def get(self, notebook_id):
+ nbm = self.application.notebook_manager
+ format = self.get_argument('format', default='json')
+ last_mod, name, data = nbm.get_notebook(notebook_id, format)
+ if format == u'json':
+ self.set_header('Content-Type', 'application/json')
+ self.set_header('Content-Disposition','attachment; filename=%s.json' % name)
+ elif format == u'xml':
+ self.set_header('Content-Type', 'text/xml')
+ self.set_header('Content-Disposition','attachment; filename=%s.ipynb' % name)
+ elif format == u'py':
+ self.set_header('Content-Type', 'text/plain')
+ self.set_header('Content-Disposition','attachment; filename=%s.py' % name)
+ self.set_header('Last-Modified', last_mod)
+ self.finish(data)
+
+ def put(self, notebook_id):
+ nbm = self.application.notebook_manager
+ format = self.get_argument('format', default='json')
+ nbm.save_notebook(notebook_id, self.request.body, format)
+ self.set_status(204)
self.finish()
- def delete(self, filename):
- path = self.find_path(filename)
- if not os.path.isfile(path):
- raise web.HTTPError(404)
- os.unlink(path)
+ def delete(self, notebook_id):
+ nbm = self.application.notebook_manager
+ nbm.delete_notebook(notebook_id)
self.set_status(204)
self.finish()
-
-
-
-
View
17 IPython/frontend/html/notebook/notebookapp.py
@@ -27,13 +27,15 @@
from tornado import httpserver
from tornado import web
-from kernelmanager import KernelManager
-from sessionmanager import SessionManager
-from handlers import (
- MainHandler, KernelHandler, KernelActionHandler, ZMQStreamHandler,
+from .kernelmanager import KernelManager
+from .sessionmanager import SessionManager
+from .handlers import (
+ MainHandler, NamedNotebookHandler,
+ KernelHandler, KernelActionHandler, ZMQStreamHandler,
NotebookRootHandler, NotebookHandler
)
-from routers import IOPubStreamRouter, ShellStreamRouter
+from .routers import IOPubStreamRouter, ShellStreamRouter
+from .notebookmanager import NotebookManager
from IPython.core.application import BaseIPythonApplication
from IPython.core.profiledir import ProfileDir
@@ -53,6 +55,7 @@
_kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
_kernel_action_regex = r"(?P<action>restart|interrupt)"
+_notebook_id_regex = r"(?P<notebook_id>\w+-\w+-\w+-\w+-\w+)"
LOCALHOST = '127.0.0.1'
@@ -65,12 +68,13 @@ class NotebookWebApplication(web.Application):
def __init__(self, kernel_manager, log, kernel_argv, config):
handlers = [
(r"/", MainHandler),
+ (r"/%s" % _notebook_id_regex, NamedNotebookHandler),
(r"/kernels", KernelHandler),
(r"/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
(r"/kernels/%s/iopub" % _kernel_id_regex, ZMQStreamHandler, dict(stream_name='iopub')),
(r"/kernels/%s/shell" % _kernel_id_regex, ZMQStreamHandler, dict(stream_name='shell')),
(r"/notebooks", NotebookRootHandler),
- (r"/notebooks/([^/]+)", NotebookHandler)
+ (r"/notebooks/%s" % _notebook_id_regex, NotebookHandler)
]
settings = dict(
template_path=os.path.join(os.path.dirname(__file__), "templates"),
@@ -84,6 +88,7 @@ def __init__(self, kernel_manager, log, kernel_argv, config):
self.config = config
self._routers = {}
self._session_dict = {}
+ self.notebook_manager = NotebookManager(config=self.config)
#-------------------------------------------------------------------------
# Methods for managing kernels and sessions
View
195 IPython/frontend/html/notebook/notebookmanager.py
@@ -0,0 +1,195 @@
+#-----------------------------------------------------------------------------
+# Copyright (C) 2011 The IPython Development Team
+#
+# Distributed under the terms of the BSD License. The full license is in
+# the file COPYING.txt, distributed as part of this software.
+#-----------------------------------------------------------------------------
+
+#-----------------------------------------------------------------------------
+# Imports
+#-----------------------------------------------------------------------------
+
+import datetime
+import os
+import uuid
+
+from tornado import web
+
+from IPython.config.configurable import Configurable
+from IPython.nbformat import current
+from IPython.utils.traitlets import Unicode, List, Dict
+
+
+#-----------------------------------------------------------------------------
+# Code
+#-----------------------------------------------------------------------------
+
+
+class NotebookManager(Configurable):
+
+ notebook_dir = Unicode(os.getcwd())
+ filename_ext = Unicode(u'.ipynb')
+ allowed_formats = List([u'json',u'xml',u'py'])
+
+ # Map notebook_ids to notebook names
+ mapping = Dict()
+ # Map notebook names to notebook_ids
+ rev_mapping = Dict()
+
+ def list_notebooks(self):
+ """List all notebooks in the notebook dir.
+
+ This returns a list of dicts of the form::
+
+ dict(notebook_id=notebook,name=name)
+ """
+ names = os.listdir(self.notebook_dir)
+ names = [name.strip(self.filename_ext)\
+ for name in names if name.endswith(self.filename_ext)]
+ data = []
+ for name in names:
+ if name not in self.rev_mapping:
+ notebook_id = self.new_notebook_id(name)
+ else:
+ notebook_id = self.rev_mapping[name]
+ data.append(dict(notebook_id=notebook_id,name=name))
+ return data
+
+ def new_notebook_id(self, name):
+ """Generate a new notebook_id for a name and store its mappings."""
+ notebook_id = unicode(uuid.uuid4())
+ self.mapping[notebook_id] = name
+ self.rev_mapping[name] = notebook_id
+ return notebook_id
+
+ def delete_notebook_id(self, notebook_id):
+ """Delete a notebook's id only. This doesn't delete the actual notebook."""
+ name = self.mapping[notebook_id]
+ del self.mapping[notebook_id]
+ del self.rev_mapping[name]
+
+ def notebook_exists(self, notebook_id):
+ """Does a notebook exist?"""
+ if notebook_id not in self.mapping:
+ return False
+ path = self.get_path_by_name(self.mapping[notebook_id])
+ if not os.path.isfile(path):
+ return False
+ return True
+
+ def find_path(self, notebook_id):
+ """Return a full path to a notebook given its notebook_id."""
+ try:
+ name = self.mapping[notebook_id]
+ except KeyError:
+ raise web.HTTPError(404)
+ return self.get_path_by_name(name)
+
+ def get_path_by_name(self, name):
+ """Return a full path to a notebook given its name."""
+ filename = name + self.filename_ext
+ path = os.path.join(self.notebook_dir, filename)
+ return path
+
+ def get_notebook(self, notebook_id, format=u'json'):
+ """Get the representation of a notebook in format by notebook_id."""
+ format = unicode(format)
+ if format not in self.allowed_formats:
+ raise web.HTTPError(415)
+ last_modified, nb = self.get_notebook_object(notebook_id)
+ data = current.writes(nb, format)
+ name = nb.get('name','notebook')
+ return last_modified, name, data
+
+ def get_notebook_object(self, notebook_id):
+ """Get the NotebookNode representation of a notebook by notebook_id."""
+ path = self.find_path(notebook_id)
+ if not os.path.isfile(path):
+ raise web.HTTPError(404)
+ info = os.stat(path)
+ last_modified = datetime.datetime.utcfromtimestamp(info.st_mtime)
+ try:
+ with open(path,'r') as f:
+ s = f.read()
+ try:
+ # v2 and later have xml in the .ipynb files
+ nb = current.reads(s, 'xml')
+ except:
+ # v1 had json in the .ipynb files
+ nb = current.reads(s, 'json')
+ except:
+ raise web.HTTPError(404)
+ return last_modified, nb
+
+ def save_new_notebook(self, data, format=u'json'):
+ """Save a new notebook and return its notebook_id."""
+ if format not in self.allowed_formats:
+ raise web.HTTPError(415)
+ try:
+ nb = current.reads(data, format)
+ except:
+ raise web.HTTPError(400)
+ try:
+ name = nb.name
+ except AttributeError:
+ raise web.HTTPError(400)
+ notebook_id = self.new_notebook_id(name)
+ self.save_notebook_object(notebook_id, nb)
+ return notebook_id
+
+ def save_notebook(self, notebook_id, data, format=u'json'):
+ """Save an existing notebook by notebook_id."""
+ if format not in self.allowed_formats:
+ raise web.HTTPError(415)
+ try:
+ nb = current.reads(data, format)
+ except:
+ raise web.HTTPError(400)
+ self.save_notebook_object(notebook_id, nb)
+
+ def save_notebook_object(self, notebook_id, nb):
+ """Save an existing notebook object by notebook_id."""
+ if notebook_id not in self.mapping:
+ raise web.HTTPError(404)
+ old_name = self.mapping[notebook_id]
+ try:
+ new_name = nb.name
+ except AttributeError:
+ raise web.HTTPError(400)
+ path = self.get_path_by_name(new_name)
+ try:
+ with open(path,'w') as f:
+ current.write(nb, f, u'xml')
+ except:
+ raise web.HTTPError(400)
+ if old_name != new_name:
+ old_path = self.get_path_by_name(old_name)
+ if os.path.isfile(old_path):
+ os.unlink(old_path)
+ self.mapping[notebook_id] = new_name
+ self.rev_mapping[new_name] = notebook_id
+
+ def delete_notebook(self, notebook_id):
+ """Delete notebook by notebook_id."""
+ path = self.find_path(notebook_id)
+ if not os.path.isfile(path):
+ raise web.HTTPError(404)
+ os.unlink(path)
+ self.delete_notebook_id(notebook_id)
+
+ def new_notebook(self):
+ """Create a new notebook and returns its notebook_id."""
+ i = 0
+ while True:
+ name = u'Untitled%i' % i
+ path = self.get_path_by_name(name)
+ if not os.path.isfile(path):
+ break
+ else:
+ i = i+1
+ notebook_id = self.new_notebook_id(name)
+ nb = current.new_notebook(name=name, id=notebook_id)
+ with open(path,'w') as f:
+ current.write(nb, f, u'xml')
+ return notebook_id
+
View
2 IPython/frontend/html/notebook/routers.py
@@ -112,7 +112,7 @@ def _on_zmq_reply(self, msg_list):
def forward_msg(self, client_id, msg):
if len(msg) < self.max_msg_size:
msg = json.loads(msg)
- to_send = self.session.serialize(msg)
+ # to_send = self.session.serialize(msg)
self._request_queue.put(client_id)
self.session.send(self.zmq_stream, msg)
View
7 IPython/frontend/html/notebook/static/css/notebook.css
@@ -206,6 +206,13 @@ span.button_label {
font-size: 77%;
}
+#download_format {
+ float: right;
+ font-size: 85%;
+ width: 60px;
+ margin: 1px 5px;
+}
+
div#left_panel_splitter {
width: 8px;
top: 0px;
View
22 IPython/frontend/html/notebook/static/js/codecell.js
@@ -302,18 +302,28 @@ var IPython = (function (IPython) {
CodeCell.prototype.fromJSON = function (data) {
if (data.cell_type === 'code') {
- this.set_code(data.code);
- this.set_input_prompt(data.prompt_number);
+ if (data.input !== undefined) {
+ this.set_code(data.input);
+ }
+ if (data.prompt_number !== undefined) {
+ this.set_input_prompt(data.prompt_number);
+ } else {
+ this.set_input_prompt();
+ };
};
};
CodeCell.prototype.toJSON = function () {
- return {
- code : this.get_code(),
- cell_type : 'code',
- prompt_number : this.input_prompt_number
+ var data = {}
+ data.input = this.get_code();
+ data.cell_type = 'code';
+ if (this.input_prompt_number !== ' ') {
+ data.prompt_number = this.input_prompt_number
};
+ data.outputs = [];
+ data.language = 'python';
+ return data;
};
IPython.CodeCell = CodeCell;
View
144 IPython/frontend/html/notebook/static/js/notebook.js
@@ -14,14 +14,9 @@ var IPython = (function (IPython) {
this.next_prompt_number = 1;
this.kernel = null;
this.msg_cell_map = {};
- this.filename = null;
- this.notebook_load_re = /%notebook load/
- this.notebook_save_re = /%notebook save/
- this.notebook_filename_re = /(\w)+.ipynb/
this.style();
this.create_elements();
this.bind_events();
- this.start_kernel();
};
@@ -473,24 +468,8 @@ var IPython = (function (IPython) {
if (cell instanceof IPython.CodeCell) {
cell.clear_output();
var code = cell.get_code();
- if (that.notebook_load_re.test(code)) {
- // %notebook load
- var code_parts = code.split(' ');
- if (code_parts.length === 3) {
- that.load_notebook(code_parts[2]);
- };
- } else if (that.notebook_save_re.test(code)) {
- // %notebook save
- var code_parts = code.split(' ');
- if (code_parts.length === 3) {
- that.save_notebook(code_parts[2]);
- } else {
- that.save_notebook()
- };
- } else {
- var msg_id = that.kernel.execute(cell.get_code());
- that.msg_cell_map[msg_id] = cell.cell_id;
- };
+ var msg_id = that.kernel.execute(cell.get_code());
+ that.msg_cell_map[msg_id] = cell.cell_id;
} else if (cell instanceof IPython.TextCell) {
cell.render();
}
@@ -532,18 +511,22 @@ var IPython = (function (IPython) {
// Always delete cell 0 as they get renumbered as they are deleted.
this.delete_cell(0);
};
- var new_cells = data.cells;
- ncells = new_cells.length;
- var cell_data = null;
- for (var i=0; i<ncells; i++) {
- cell_data = new_cells[i];
- if (cell_data.cell_type == 'code') {
- this.insert_code_cell_after();
- this.selected_cell().fromJSON(cell_data);
- } else if (cell_data.cell_type === 'text') {
- this.insert_text_cell_after();
- this.selected_cell().fromJSON(cell_data);
- };
+ // Only handle 1 worksheet for now.
+ var worksheet = data.worksheets[0];
+ if (worksheet !== undefined) {
+ var new_cells = worksheet.cells;
+ ncells = new_cells.length;
+ var cell_data = null;
+ for (var i=0; i<ncells; i++) {
+ cell_data = new_cells[i];
+ if (cell_data.cell_type == 'code') {
+ this.insert_code_cell_after();
+ this.selected_cell().fromJSON(cell_data);
+ } else if (cell_data.cell_type === 'text') {
+ this.insert_text_cell_after();
+ this.selected_cell().fromJSON(cell_data);
+ };
+ };
};
};
@@ -555,67 +538,66 @@ var IPython = (function (IPython) {
for (var i=0; i<ncells; i++) {
cell_array[i] = cells[i].toJSON();
};
- json = {
- cells : cell_array
+ data = {
+ // Only handle 1 worksheet for now.
+ worksheets : [{cells:cell_array}]
+ }
+ return data
+ };
+
+ Notebook.prototype.save_notebook = function () {
+ if (IPython.save_widget.test_notebook_name()) {
+ var notebook_id = IPython.save_widget.get_notebook_id();
+ var nbname = IPython.save_widget.get_notebook_name();
+ // We may want to move the name/id/nbformat logic inside toJSON?
+ var data = this.toJSON();
+ data.name = nbname;
+ data.nbformat = 2;
+ data.id = notebook_id
+ // We do the call with settings so we can set cache to false.
+ var settings = {
+ processData : false,
+ cache : false,
+ type : "PUT",
+ data : JSON.stringify(data),
+ success : $.proxy(this.notebook_saved,this)
+ };
+ IPython.save_widget.status_saving();
+ $.ajax("/notebooks/" + notebook_id, settings);
};
- return json
};
- Notebook.prototype.test_filename = function (filename) {
- if (this.notebook_filename_re.test(filename)) {
- return true;
- } else {
- var bad_filename = $('<div/>');
- bad_filename.html(
- "The filename you entered (" + filename + ") is not valid. Notebook filenames must have the following form: foo.ipynb"
- );
- bad_filename.dialog({title: 'Invalid filename', modal: true});
- return false;
- };
- };
-
- Notebook.prototype.save_notebook = function (filename) {
- this.filename = filename || this.filename || '';
- if (this.filename === '') {
- var no_filename = $('<div/>');
- no_filename.html(
- "This notebook has no filename, please specify a filename of the form: foo.ipynb"
- );
- no_filename.dialog({title: 'Missing filename', modal: true});
- return;
- }
- if (!this.test_filename(this.filename)) {return;}
- var thedata = this.toJSON();
- var settings = {
- processData : false,
- cache : false,
- type : "PUT",
- data : JSON.stringify(thedata),
- success : function (data, status, xhr) {console.log(data);}
- };
- $.ajax("/notebooks/" + this.filename, settings);
- };
+ Notebook.prototype.notebook_saved = function (data, status, xhr) {
+ IPython.save_widget.status_save();
+ }
- Notebook.prototype.load_notebook = function (filename) {
- if (!this.test_filename(filename)) {return;}
- var that = this;
+ Notebook.prototype.load_notebook = function () {
+ var notebook_id = IPython.save_widget.get_notebook_id();
// We do the call with settings so we can set cache to false.
var settings = {
processData : false,
cache : false,
type : "GET",
dataType : "json",
- success : function (data, status, xhr) {
- that.fromJSON(data);
- that.filename = filename;
- that.kernel.restart();
- }
+ success : $.proxy(this.notebook_loaded,this)
};
- $.ajax("/notebooks/" + filename, settings);
+ IPython.save_widget.status_loading();
+ $.ajax("/notebooks/" + notebook_id, settings);
}
+
+ Notebook.prototype.notebook_loaded = function (data, status, xhr) {
+ this.fromJSON(data);
+ if (this.ncells() === 0) {
+ this.insert_code_cell_after();
+ };
+ IPython.save_widget.status_save();
+ IPython.save_widget.set_notebook_name(data.name);
+ this.start_kernel();
+ };
+
IPython.Notebook = Notebook;
return IPython;
View
14 IPython/frontend/html/notebook/static/js/notebook_main.js
@@ -30,14 +30,18 @@ $(document).ready(function () {
IPython.kernel_status_widget.status_idle();
IPython.layout_manager.do_resize();
- IPython.notebook.insert_code_cell_after();
- IPython.layout_manager.do_resize();
// These have display: none in the css file and are made visible here to prevent FLOUC.
$('div#header').css('display','block');
$('div#notebook_app').css('display','block');
- IPython.layout_manager.do_resize();
- IPython.pager.collapse();
- IPython.layout_manager.do_resize();
+
+ IPython.notebook.load_notebook();
+
+ // Perform these actions after the notebook has been loaded.
+ setTimeout(function () {
+ IPython.save_widget.update_url();
+ IPython.layout_manager.do_resize();
+ IPython.pager.collapse();
+ }, 100);
});
View
15 IPython/frontend/html/notebook/static/js/panelsection.js
@@ -82,20 +82,31 @@ var IPython = (function (IPython) {
this.content.addClass('ui-helper-clearfix');
this.content.find('div.section_row').addClass('ui-helper-clearfix');
this.content.find('#new_open').buttonset();
+ this.content.find('#download_notebook').button();
+ this.content.find('#upload_notebook').button();
+ this.content.find('#download_format').addClass('ui-widget ui-widget-content');
+ this.content.find('#download_format option').addClass('ui-widget ui-widget-content');
};
NotebookSection.prototype.bind_events = function () {
PanelSection.prototype.bind_events.apply(this);
+ var that = this;
this.content.find('#new_notebook').click(function () {
- alert('Not Implemented');
+ console.log('click!')
+ window.open('/');
});
this.content.find('#open_notebook').click(function () {
alert('Not Implemented');
});
+ this.content.find('#download_notebook').click(function () {
+ var format = that.content.find('#download_format').val();
+ var notebook_id = IPython.save_widget.get_notebook_id();
+ var url = '/notebooks/' + notebook_id + '?format=' + format;
+ window.open(url,'_newtab');
+ });
};
-
// CellSection
var CellSection = function () {
View
55 IPython/frontend/html/notebook/static/js/savewidget.js
@@ -9,6 +9,7 @@ var IPython = (function (IPython) {
var SaveWidget = function (selector) {
this.selector = selector;
+ this.notebook_name_re = /[^/\\]+/
if (this.selector !== undefined) {
this.element = $(selector);
this.style();
@@ -29,7 +30,7 @@ var IPython = (function (IPython) {
SaveWidget.prototype.bind_events = function () {
var that = this;
this.element.find('button#save_notebook').click(function () {
- IPython.notebook.save_notebook(that.get_notebook_name());
+ IPython.notebook.save_notebook();
});
};
@@ -39,11 +40,59 @@ var IPython = (function (IPython) {
}
- SaveWidget.prototype.set_notebook_name = function (name) {
- this.element.find('input#notebook_name').attr('value',name);
+ SaveWidget.prototype.set_notebook_name = function (nbname) {
+ this.element.find('input#notebook_name').attr('value',nbname);
}
+ SaveWidget.prototype.get_notebook_id = function () {
+ return this.element.find('span#notebook_id').text()
+ };
+
+
+ SaveWidget.prototype.update_url = function () {
+ var notebook_id = this.get_notebook_id();
+ if (notebook_id !== '') {
+ window.history.replaceState({}, '', notebook_id);
+ };
+ };
+
+
+ SaveWidget.prototype.test_notebook_name = function () {
+ var nbname = this.get_notebook_name();
+ if (this.notebook_name_re.test(nbname)) {
+ return true;
+ } else {
+ var bad_name = $('<div/>');
+ bad_name.html(
+ "The notebook name you entered (" +
+ nbname +
+ ") is not valid. Notebook names can contain any characters except / and \\"
+ );
+ bad_name.dialog({title: 'Invalid name', modal: true});
+ return false;
+ };
+ };
+
+
+ SaveWidget.prototype.status_save = function () {
+ this.element.find('span.ui-button-text').text('Save');
+ this.element.find('button#save_notebook').button('enable');
+ };
+
+
+ SaveWidget.prototype.status_saving = function () {
+ this.element.find('span.ui-button-text').text('Saving');
+ this.element.find('button#save_notebook').button('disable');
+ };
+
+
+ SaveWidget.prototype.status_loading = function () {
+ this.element.find('span.ui-button-text').text('Loading');
+ this.element.find('button#save_notebook').button('disable');
+ };
+
+
IPython.SaveWidget = SaveWidget;
return IPython;
View
14 IPython/frontend/html/notebook/static/js/textcell.js
@@ -129,17 +129,19 @@ var IPython = (function (IPython) {
TextCell.prototype.fromJSON = function (data) {
if (data.cell_type === 'text') {
- this.set_text(data.text);
- this.grow(this.element.find("textarea.text_cell_input"));
+ if (data.text !== undefined) {
+ this.set_text(data.text);
+ this.grow(this.element.find("textarea.text_cell_input"));
+ };
};
}
TextCell.prototype.toJSON = function () {
- return {
- cell_type : 'text',
- text : this.get_text(),
- };
+ var data = {}
+ data.cell_type = 'text';
+ data.text = this.get_text();
+ return data;
};
IPython.TextCell = TextCell;
View
13 IPython/frontend/html/notebook/templates/notebook.html
@@ -33,6 +33,7 @@
<span id="ipython_notebook"><h1>IPython Notebook</h1></span>
<span id="save_widget">
<input type="text" id="notebook_name" size="20"></textarea>
+ <span id="notebook_id" style="display:none">{{notebook_id}}</span>
<button id="save_notebook">Save</button>
</span>
<span id="kernel_status">Idle</span>
@@ -52,6 +53,18 @@ <h3 class="section_header">Notebook</h3>
</span>
<span class="section_row_header">Actions</span>
</div>
+ <div class="section_row">
+ <span class="section_row_buttons">
+ <button id="download_notebook">Export</button>
+ </span>
+ <span>
+ <select id="download_format">
+ <option value="xml">xml</option>
+ <option value="json">json</option>
+ <option value="py">py</option>
+ </select>
+ </span>
+ </div>
</div>
</div>
View
5 IPython/nbformat/current.py
@@ -5,6 +5,11 @@
from IPython.nbformat import v2
from IPython.nbformat import v1
+from IPython.nbformat.v2 import (
+ NotebookNode,
+ new_code_cell, new_text_cell, new_notebook, new_output, new_worksheet
+)
+
current_nbformat = 2
View
9 IPython/nbformat/v2/nbpy.py
@@ -36,10 +36,11 @@ def writes(self, nb, **kwargs):
for ws in nb.worksheets:
for cell in ws.cells:
if cell.cell_type == 'code':
- input = cell.input
- lines.extend([u'# <codecell>',u''])
- lines.extend(input.splitlines())
- lines.extend([u'',u'# </codecell>'])
+ input = cell.get('input')
+ if input is not None:
+ lines.extend([u'# <codecell>',u''])
+ lines.extend(input.splitlines())
+ lines.extend([u'',u'# </codecell>'])
lines.append('')
return unicode('\n'.join(lines))

0 comments on commit 65666c0

Please sign in to comment.
Something went wrong with that request. Please try again.