##// END OF EJS Templates
Massive work on the notebook document format....
Brian E. Granger -
Show More
@@ -0,0 +1,195 b''
1 #-----------------------------------------------------------------------------
2 # Copyright (C) 2011 The IPython Development Team
3 #
4 # Distributed under the terms of the BSD License. The full license is in
5 # the file COPYING.txt, distributed as part of this software.
6 #-----------------------------------------------------------------------------
7
8 #-----------------------------------------------------------------------------
9 # Imports
10 #-----------------------------------------------------------------------------
11
12 import datetime
13 import os
14 import uuid
15
16 from tornado import web
17
18 from IPython.config.configurable import Configurable
19 from IPython.nbformat import current
20 from IPython.utils.traitlets import Unicode, List, Dict
21
22
23 #-----------------------------------------------------------------------------
24 # Code
25 #-----------------------------------------------------------------------------
26
27
28 class NotebookManager(Configurable):
29
30 notebook_dir = Unicode(os.getcwd())
31 filename_ext = Unicode(u'.ipynb')
32 allowed_formats = List([u'json',u'xml',u'py'])
33
34 # Map notebook_ids to notebook names
35 mapping = Dict()
36 # Map notebook names to notebook_ids
37 rev_mapping = Dict()
38
39 def list_notebooks(self):
40 """List all notebooks in the notebook dir.
41
42 This returns a list of dicts of the form::
43
44 dict(notebook_id=notebook,name=name)
45 """
46 names = os.listdir(self.notebook_dir)
47 names = [name.strip(self.filename_ext)\
48 for name in names if name.endswith(self.filename_ext)]
49 data = []
50 for name in names:
51 if name not in self.rev_mapping:
52 notebook_id = self.new_notebook_id(name)
53 else:
54 notebook_id = self.rev_mapping[name]
55 data.append(dict(notebook_id=notebook_id,name=name))
56 return data
57
58 def new_notebook_id(self, name):
59 """Generate a new notebook_id for a name and store its mappings."""
60 notebook_id = unicode(uuid.uuid4())
61 self.mapping[notebook_id] = name
62 self.rev_mapping[name] = notebook_id
63 return notebook_id
64
65 def delete_notebook_id(self, notebook_id):
66 """Delete a notebook's id only. This doesn't delete the actual notebook."""
67 name = self.mapping[notebook_id]
68 del self.mapping[notebook_id]
69 del self.rev_mapping[name]
70
71 def notebook_exists(self, notebook_id):
72 """Does a notebook exist?"""
73 if notebook_id not in self.mapping:
74 return False
75 path = self.get_path_by_name(self.mapping[notebook_id])
76 if not os.path.isfile(path):
77 return False
78 return True
79
80 def find_path(self, notebook_id):
81 """Return a full path to a notebook given its notebook_id."""
82 try:
83 name = self.mapping[notebook_id]
84 except KeyError:
85 raise web.HTTPError(404)
86 return self.get_path_by_name(name)
87
88 def get_path_by_name(self, name):
89 """Return a full path to a notebook given its name."""
90 filename = name + self.filename_ext
91 path = os.path.join(self.notebook_dir, filename)
92 return path
93
94 def get_notebook(self, notebook_id, format=u'json'):
95 """Get the representation of a notebook in format by notebook_id."""
96 format = unicode(format)
97 if format not in self.allowed_formats:
98 raise web.HTTPError(415)
99 last_modified, nb = self.get_notebook_object(notebook_id)
100 data = current.writes(nb, format)
101 name = nb.get('name','notebook')
102 return last_modified, name, data
103
104 def get_notebook_object(self, notebook_id):
105 """Get the NotebookNode representation of a notebook by notebook_id."""
106 path = self.find_path(notebook_id)
107 if not os.path.isfile(path):
108 raise web.HTTPError(404)
109 info = os.stat(path)
110 last_modified = datetime.datetime.utcfromtimestamp(info.st_mtime)
111 try:
112 with open(path,'r') as f:
113 s = f.read()
114 try:
115 # v2 and later have xml in the .ipynb files
116 nb = current.reads(s, 'xml')
117 except:
118 # v1 had json in the .ipynb files
119 nb = current.reads(s, 'json')
120 except:
121 raise web.HTTPError(404)
122 return last_modified, nb
123
124 def save_new_notebook(self, data, format=u'json'):
125 """Save a new notebook and return its notebook_id."""
126 if format not in self.allowed_formats:
127 raise web.HTTPError(415)
128 try:
129 nb = current.reads(data, format)
130 except:
131 raise web.HTTPError(400)
132 try:
133 name = nb.name
134 except AttributeError:
135 raise web.HTTPError(400)
136 notebook_id = self.new_notebook_id(name)
137 self.save_notebook_object(notebook_id, nb)
138 return notebook_id
139
140 def save_notebook(self, notebook_id, data, format=u'json'):
141 """Save an existing notebook by notebook_id."""
142 if format not in self.allowed_formats:
143 raise web.HTTPError(415)
144 try:
145 nb = current.reads(data, format)
146 except:
147 raise web.HTTPError(400)
148 self.save_notebook_object(notebook_id, nb)
149
150 def save_notebook_object(self, notebook_id, nb):
151 """Save an existing notebook object by notebook_id."""
152 if notebook_id not in self.mapping:
153 raise web.HTTPError(404)
154 old_name = self.mapping[notebook_id]
155 try:
156 new_name = nb.name
157 except AttributeError:
158 raise web.HTTPError(400)
159 path = self.get_path_by_name(new_name)
160 try:
161 with open(path,'w') as f:
162 current.write(nb, f, u'xml')
163 except:
164 raise web.HTTPError(400)
165 if old_name != new_name:
166 old_path = self.get_path_by_name(old_name)
167 if os.path.isfile(old_path):
168 os.unlink(old_path)
169 self.mapping[notebook_id] = new_name
170 self.rev_mapping[new_name] = notebook_id
171
172 def delete_notebook(self, notebook_id):
173 """Delete notebook by notebook_id."""
174 path = self.find_path(notebook_id)
175 if not os.path.isfile(path):
176 raise web.HTTPError(404)
177 os.unlink(path)
178 self.delete_notebook_id(notebook_id)
179
180 def new_notebook(self):
181 """Create a new notebook and returns its notebook_id."""
182 i = 0
183 while True:
184 name = u'Untitled%i' % i
185 path = self.get_path_by_name(name)
186 if not os.path.isfile(path):
187 break
188 else:
189 i = i+1
190 notebook_id = self.new_notebook_id(name)
191 nb = current.new_notebook(name=name, id=notebook_id)
192 with open(path,'w') as f:
193 current.write(nb, f, u'xml')
194 return notebook_id
195
@@ -4,15 +4,14 b''
4 4 # Imports
5 5 #-----------------------------------------------------------------------------
6 6
7 import datetime
8 7 import json
9 8 import logging
10 import os
11 9 import urllib
12 10
13 11 from tornado import web
14 12 from tornado import websocket
15 13
14
16 15 #-----------------------------------------------------------------------------
17 16 # Handlers
18 17 #-----------------------------------------------------------------------------
@@ -20,7 +19,16 b' from tornado import websocket'
20 19
21 20 class MainHandler(web.RequestHandler):
22 21 def get(self):
23 self.render('notebook.html')
22 notebook_id = self.application.notebook_manager.new_notebook()
23 self.render('notebook.html', notebook_id=notebook_id)
24
25
26 class NamedNotebookHandler(web.RequestHandler):
27 def get(self, notebook_id):
28 nbm = self.application.notebook_manager
29 if not nbm.notebook_exists(notebook_id):
30 raise web.HTTPError(404)
31 self.render('notebook.html', notebook_id=notebook_id)
24 32
25 33
26 34 class KernelHandler(web.RequestHandler):
@@ -30,6 +38,7 b' class KernelHandler(web.RequestHandler):'
30 38
31 39 def post(self):
32 40 kernel_id = self.application.start_kernel()
41 self.set_header('Location', '/'+kernel_id)
33 42 self.write(json.dumps(kernel_id))
34 43
35 44
@@ -65,52 +74,52 b' class ZMQStreamHandler(websocket.WebSocketHandler):'
65 74 class NotebookRootHandler(web.RequestHandler):
66 75
67 76 def get(self):
68 files = os.listdir(os.getcwd())
69 files = [file for file in files if file.endswith(".ipynb")]
77 nbm = self.application.notebook_manager
78 files = nbm.list_notebooks()
70 79 self.write(json.dumps(files))
71 80
81 def post(self):
82 nbm = self.application.notebook_manager
83 body = self.request.body.strip()
84 format = self.get_argument('format', default='json')
85 if body:
86 notebook_id = nbm.save_new_notebook(body, format)
87 else:
88 notebook_id = nbm.new_notebook()
89 self.set_header('Location', '/'+notebook_id)
90 self.write(json.dumps(notebook_id))
72 91
73 class NotebookHandler(web.RequestHandler):
74
75 SUPPORTED_METHODS = ("GET", "DELETE", "PUT")
76 92
77 def find_path(self, filename):
78 filename = urllib.unquote(filename)
79 if not filename.endswith('.ipynb'):
80 raise web.HTTPError(400)
81 path = os.path.join(os.getcwd(), filename)
82 return path
93 class NotebookHandler(web.RequestHandler):
83 94
84 def get(self, filename):
85 path = self.find_path(filename)
86 if not os.path.isfile(path):
87 raise web.HTTPError(404)
88 info = os.stat(path)
89 self.set_header("Content-Type", "application/unknown")
90 self.set_header("Last-Modified", datetime.datetime.utcfromtimestamp(
91 info.st_mtime))
92 f = open(path, "r")
93 try:
94 self.finish(f.read())
95 finally:
96 f.close()
97
98 def put(self, filename):
99 path = self.find_path(filename)
100 f = open(path, "w")
101 f.write(self.request.body)
102 f.close()
95 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
96
97 def get(self, notebook_id):
98 nbm = self.application.notebook_manager
99 format = self.get_argument('format', default='json')
100 last_mod, name, data = nbm.get_notebook(notebook_id, format)
101 if format == u'json':
102 self.set_header('Content-Type', 'application/json')
103 self.set_header('Content-Disposition','attachment; filename=%s.json' % name)
104 elif format == u'xml':
105 self.set_header('Content-Type', 'text/xml')
106 self.set_header('Content-Disposition','attachment; filename=%s.ipynb' % name)
107 elif format == u'py':
108 self.set_header('Content-Type', 'text/plain')
109 self.set_header('Content-Disposition','attachment; filename=%s.py' % name)
110 self.set_header('Last-Modified', last_mod)
111 self.finish(data)
112
113 def put(self, notebook_id):
114 nbm = self.application.notebook_manager
115 format = self.get_argument('format', default='json')
116 nbm.save_notebook(notebook_id, self.request.body, format)
117 self.set_status(204)
103 118 self.finish()
104 119
105 def delete(self, filename):
106 path = self.find_path(filename)
107 if not os.path.isfile(path):
108 raise web.HTTPError(404)
109 os.unlink(path)
120 def delete(self, notebook_id):
121 nbm = self.application.notebook_manager
122 nbm.delete_notebook(notebook_id)
110 123 self.set_status(204)
111 124 self.finish()
112 125
113
114
115
116
@@ -27,13 +27,15 b' tornado.ioloop = ioloop'
27 27 from tornado import httpserver
28 28 from tornado import web
29 29
30 from kernelmanager import KernelManager
31 from sessionmanager import SessionManager
32 from handlers import (
33 MainHandler, KernelHandler, KernelActionHandler, ZMQStreamHandler,
30 from .kernelmanager import KernelManager
31 from .sessionmanager import SessionManager
32 from .handlers import (
33 MainHandler, NamedNotebookHandler,
34 KernelHandler, KernelActionHandler, ZMQStreamHandler,
34 35 NotebookRootHandler, NotebookHandler
35 36 )
36 from routers import IOPubStreamRouter, ShellStreamRouter
37 from .routers import IOPubStreamRouter, ShellStreamRouter
38 from .notebookmanager import NotebookManager
37 39
38 40 from IPython.core.application import BaseIPythonApplication
39 41 from IPython.core.profiledir import ProfileDir
@@ -53,6 +55,7 b' from IPython.utils.traitlets import Dict, Unicode, Int, Any, List, Enum'
53 55
54 56 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
55 57 _kernel_action_regex = r"(?P<action>restart|interrupt)"
58 _notebook_id_regex = r"(?P<notebook_id>\w+-\w+-\w+-\w+-\w+)"
56 59
57 60 LOCALHOST = '127.0.0.1'
58 61
@@ -65,12 +68,13 b' class NotebookWebApplication(web.Application):'
65 68 def __init__(self, kernel_manager, log, kernel_argv, config):
66 69 handlers = [
67 70 (r"/", MainHandler),
71 (r"/%s" % _notebook_id_regex, NamedNotebookHandler),
68 72 (r"/kernels", KernelHandler),
69 73 (r"/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
70 74 (r"/kernels/%s/iopub" % _kernel_id_regex, ZMQStreamHandler, dict(stream_name='iopub')),
71 75 (r"/kernels/%s/shell" % _kernel_id_regex, ZMQStreamHandler, dict(stream_name='shell')),
72 76 (r"/notebooks", NotebookRootHandler),
73 (r"/notebooks/([^/]+)", NotebookHandler)
77 (r"/notebooks/%s" % _notebook_id_regex, NotebookHandler)
74 78 ]
75 79 settings = dict(
76 80 template_path=os.path.join(os.path.dirname(__file__), "templates"),
@@ -84,6 +88,7 b' class NotebookWebApplication(web.Application):'
84 88 self.config = config
85 89 self._routers = {}
86 90 self._session_dict = {}
91 self.notebook_manager = NotebookManager(config=self.config)
87 92
88 93 #-------------------------------------------------------------------------
89 94 # Methods for managing kernels and sessions
@@ -112,7 +112,7 b' class ShellStreamRouter(ZMQStreamRouter):'
112 112 def forward_msg(self, client_id, msg):
113 113 if len(msg) < self.max_msg_size:
114 114 msg = json.loads(msg)
115 to_send = self.session.serialize(msg)
115 # to_send = self.session.serialize(msg)
116 116 self._request_queue.put(client_id)
117 117 self.session.send(self.zmq_stream, msg)
118 118
@@ -206,6 +206,13 b' span.button_label {'
206 206 font-size: 77%;
207 207 }
208 208
209 #download_format {
210 float: right;
211 font-size: 85%;
212 width: 60px;
213 margin: 1px 5px;
214 }
215
209 216 div#left_panel_splitter {
210 217 width: 8px;
211 218 top: 0px;
@@ -302,18 +302,28 b' var IPython = (function (IPython) {'
302 302
303 303 CodeCell.prototype.fromJSON = function (data) {
304 304 if (data.cell_type === 'code') {
305 this.set_code(data.code);
306 this.set_input_prompt(data.prompt_number);
305 if (data.input !== undefined) {
306 this.set_code(data.input);
307 }
308 if (data.prompt_number !== undefined) {
309 this.set_input_prompt(data.prompt_number);
310 } else {
311 this.set_input_prompt();
312 };
307 313 };
308 314 };
309 315
310 316
311 317 CodeCell.prototype.toJSON = function () {
312 return {
313 code : this.get_code(),
314 cell_type : 'code',
315 prompt_number : this.input_prompt_number
318 var data = {}
319 data.input = this.get_code();
320 data.cell_type = 'code';
321 if (this.input_prompt_number !== ' ') {
322 data.prompt_number = this.input_prompt_number
316 323 };
324 data.outputs = [];
325 data.language = 'python';
326 return data;
317 327 };
318 328
319 329 IPython.CodeCell = CodeCell;
@@ -14,14 +14,9 b' var IPython = (function (IPython) {'
14 14 this.next_prompt_number = 1;
15 15 this.kernel = null;
16 16 this.msg_cell_map = {};
17 this.filename = null;
18 this.notebook_load_re = /%notebook load/
19 this.notebook_save_re = /%notebook save/
20 this.notebook_filename_re = /(\w)+.ipynb/
21 17 this.style();
22 18 this.create_elements();
23 19 this.bind_events();
24 this.start_kernel();
25 20 };
26 21
27 22
@@ -473,24 +468,8 b' var IPython = (function (IPython) {'
473 468 if (cell instanceof IPython.CodeCell) {
474 469 cell.clear_output();
475 470 var code = cell.get_code();
476 if (that.notebook_load_re.test(code)) {
477 // %notebook load
478 var code_parts = code.split(' ');
479 if (code_parts.length === 3) {
480 that.load_notebook(code_parts[2]);
481 };
482 } else if (that.notebook_save_re.test(code)) {
483 // %notebook save
484 var code_parts = code.split(' ');
485 if (code_parts.length === 3) {
486 that.save_notebook(code_parts[2]);
487 } else {
488 that.save_notebook()
489 };
490 } else {
491 var msg_id = that.kernel.execute(cell.get_code());
492 that.msg_cell_map[msg_id] = cell.cell_id;
493 };
471 var msg_id = that.kernel.execute(cell.get_code());
472 that.msg_cell_map[msg_id] = cell.cell_id;
494 473 } else if (cell instanceof IPython.TextCell) {
495 474 cell.render();
496 475 }
@@ -532,18 +511,22 b' var IPython = (function (IPython) {'
532 511 // Always delete cell 0 as they get renumbered as they are deleted.
533 512 this.delete_cell(0);
534 513 };
535 var new_cells = data.cells;
536 ncells = new_cells.length;
537 var cell_data = null;
538 for (var i=0; i<ncells; i++) {
539 cell_data = new_cells[i];
540 if (cell_data.cell_type == 'code') {
541 this.insert_code_cell_after();
542 this.selected_cell().fromJSON(cell_data);
543 } else if (cell_data.cell_type === 'text') {
544 this.insert_text_cell_after();
545 this.selected_cell().fromJSON(cell_data);
546 };
514 // Only handle 1 worksheet for now.
515 var worksheet = data.worksheets[0];
516 if (worksheet !== undefined) {
517 var new_cells = worksheet.cells;
518 ncells = new_cells.length;
519 var cell_data = null;
520 for (var i=0; i<ncells; i++) {
521 cell_data = new_cells[i];
522 if (cell_data.cell_type == 'code') {
523 this.insert_code_cell_after();
524 this.selected_cell().fromJSON(cell_data);
525 } else if (cell_data.cell_type === 'text') {
526 this.insert_text_cell_after();
527 this.selected_cell().fromJSON(cell_data);
528 };
529 };
547 530 };
548 531 };
549 532
@@ -555,67 +538,66 b' var IPython = (function (IPython) {'
555 538 for (var i=0; i<ncells; i++) {
556 539 cell_array[i] = cells[i].toJSON();
557 540 };
558 json = {
559 cells : cell_array
541 data = {
542 // Only handle 1 worksheet for now.
543 worksheets : [{cells:cell_array}]
544 }
545 return data
546 };
547
548 Notebook.prototype.save_notebook = function () {
549 if (IPython.save_widget.test_notebook_name()) {
550 var notebook_id = IPython.save_widget.get_notebook_id();
551 var nbname = IPython.save_widget.get_notebook_name();
552 // We may want to move the name/id/nbformat logic inside toJSON?
553 var data = this.toJSON();
554 data.name = nbname;
555 data.nbformat = 2;
556 data.id = notebook_id
557 // We do the call with settings so we can set cache to false.
558 var settings = {
559 processData : false,
560 cache : false,
561 type : "PUT",
562 data : JSON.stringify(data),
563 success : $.proxy(this.notebook_saved,this)
564 };
565 IPython.save_widget.status_saving();
566 $.ajax("/notebooks/" + notebook_id, settings);
560 567 };
561 return json
562 568 };
563 569
564 570
565 Notebook.prototype.test_filename = function (filename) {
566 if (this.notebook_filename_re.test(filename)) {
567 return true;
568 } else {
569 var bad_filename = $('<div/>');
570 bad_filename.html(
571 "The filename you entered (" + filename + ") is not valid. Notebook filenames must have the following form: foo.ipynb"
572 );
573 bad_filename.dialog({title: 'Invalid filename', modal: true});
574 return false;
575 };
576 };
577
578 Notebook.prototype.save_notebook = function (filename) {
579 this.filename = filename || this.filename || '';
580 if (this.filename === '') {
581 var no_filename = $('<div/>');
582 no_filename.html(
583 "This notebook has no filename, please specify a filename of the form: foo.ipynb"
584 );
585 no_filename.dialog({title: 'Missing filename', modal: true});
586 return;
587 }
588 if (!this.test_filename(this.filename)) {return;}
589 var thedata = this.toJSON();
590 var settings = {
591 processData : false,
592 cache : false,
593 type : "PUT",
594 data : JSON.stringify(thedata),
595 success : function (data, status, xhr) {console.log(data);}
596 };
597 $.ajax("/notebooks/" + this.filename, settings);
598 };
571 Notebook.prototype.notebook_saved = function (data, status, xhr) {
572 IPython.save_widget.status_save();
573 }
599 574
600 575
601 Notebook.prototype.load_notebook = function (filename) {
602 if (!this.test_filename(filename)) {return;}
603 var that = this;
576 Notebook.prototype.load_notebook = function () {
577 var notebook_id = IPython.save_widget.get_notebook_id();
604 578 // We do the call with settings so we can set cache to false.
605 579 var settings = {
606 580 processData : false,
607 581 cache : false,
608 582 type : "GET",
609 583 dataType : "json",
610 success : function (data, status, xhr) {
611 that.fromJSON(data);
612 that.filename = filename;
613 that.kernel.restart();
614 }
584 success : $.proxy(this.notebook_loaded,this)
615 585 };
616 $.ajax("/notebooks/" + filename, settings);
586 IPython.save_widget.status_loading();
587 $.ajax("/notebooks/" + notebook_id, settings);
617 588 }
618 589
590
591 Notebook.prototype.notebook_loaded = function (data, status, xhr) {
592 this.fromJSON(data);
593 if (this.ncells() === 0) {
594 this.insert_code_cell_after();
595 };
596 IPython.save_widget.status_save();
597 IPython.save_widget.set_notebook_name(data.name);
598 this.start_kernel();
599 };
600
619 601 IPython.Notebook = Notebook;
620 602
621 603 return IPython;
@@ -30,14 +30,18 b' $(document).ready(function () {'
30 30 IPython.kernel_status_widget.status_idle();
31 31
32 32 IPython.layout_manager.do_resize();
33 IPython.notebook.insert_code_cell_after();
34 IPython.layout_manager.do_resize();
35 33
36 34 // These have display: none in the css file and are made visible here to prevent FLOUC.
37 35 $('div#header').css('display','block');
38 36 $('div#notebook_app').css('display','block');
39 IPython.layout_manager.do_resize();
40 IPython.pager.collapse();
41 IPython.layout_manager.do_resize();
37
38 IPython.notebook.load_notebook();
39
40 // Perform these actions after the notebook has been loaded.
41 setTimeout(function () {
42 IPython.save_widget.update_url();
43 IPython.layout_manager.do_resize();
44 IPython.pager.collapse();
45 }, 100);
42 46 });
43 47
@@ -82,20 +82,31 b' var IPython = (function (IPython) {'
82 82 this.content.addClass('ui-helper-clearfix');
83 83 this.content.find('div.section_row').addClass('ui-helper-clearfix');
84 84 this.content.find('#new_open').buttonset();
85 this.content.find('#download_notebook').button();
86 this.content.find('#upload_notebook').button();
87 this.content.find('#download_format').addClass('ui-widget ui-widget-content');
88 this.content.find('#download_format option').addClass('ui-widget ui-widget-content');
85 89 };
86 90
87 91
88 92 NotebookSection.prototype.bind_events = function () {
89 93 PanelSection.prototype.bind_events.apply(this);
94 var that = this;
90 95 this.content.find('#new_notebook').click(function () {
91 alert('Not Implemented');
96 console.log('click!')
97 window.open('/');
92 98 });
93 99 this.content.find('#open_notebook').click(function () {
94 100 alert('Not Implemented');
95 101 });
102 this.content.find('#download_notebook').click(function () {
103 var format = that.content.find('#download_format').val();
104 var notebook_id = IPython.save_widget.get_notebook_id();
105 var url = '/notebooks/' + notebook_id + '?format=' + format;
106 window.open(url,'_newtab');
107 });
96 108 };
97 109
98
99 110 // CellSection
100 111
101 112 var CellSection = function () {
@@ -9,6 +9,7 b' var IPython = (function (IPython) {'
9 9
10 10 var SaveWidget = function (selector) {
11 11 this.selector = selector;
12 this.notebook_name_re = /[^/\\]+/
12 13 if (this.selector !== undefined) {
13 14 this.element = $(selector);
14 15 this.style();
@@ -29,7 +30,7 b' var IPython = (function (IPython) {'
29 30 SaveWidget.prototype.bind_events = function () {
30 31 var that = this;
31 32 this.element.find('button#save_notebook').click(function () {
32 IPython.notebook.save_notebook(that.get_notebook_name());
33 IPython.notebook.save_notebook();
33 34 });
34 35 };
35 36
@@ -39,11 +40,59 b' var IPython = (function (IPython) {'
39 40 }
40 41
41 42
42 SaveWidget.prototype.set_notebook_name = function (name) {
43 this.element.find('input#notebook_name').attr('value',name);
43 SaveWidget.prototype.set_notebook_name = function (nbname) {
44 this.element.find('input#notebook_name').attr('value',nbname);
44 45 }
45 46
46 47
48 SaveWidget.prototype.get_notebook_id = function () {
49 return this.element.find('span#notebook_id').text()
50 };
51
52
53 SaveWidget.prototype.update_url = function () {
54 var notebook_id = this.get_notebook_id();
55 if (notebook_id !== '') {
56 window.history.replaceState({}, '', notebook_id);
57 };
58 };
59
60
61 SaveWidget.prototype.test_notebook_name = function () {
62 var nbname = this.get_notebook_name();
63 if (this.notebook_name_re.test(nbname)) {
64 return true;
65 } else {
66 var bad_name = $('<div/>');
67 bad_name.html(
68 "The notebook name you entered (" +
69 nbname +
70 ") is not valid. Notebook names can contain any characters except / and \\"
71 );
72 bad_name.dialog({title: 'Invalid name', modal: true});
73 return false;
74 };
75 };
76
77
78 SaveWidget.prototype.status_save = function () {
79 this.element.find('span.ui-button-text').text('Save');
80 this.element.find('button#save_notebook').button('enable');
81 };
82
83
84 SaveWidget.prototype.status_saving = function () {
85 this.element.find('span.ui-button-text').text('Saving');
86 this.element.find('button#save_notebook').button('disable');
87 };
88
89
90 SaveWidget.prototype.status_loading = function () {
91 this.element.find('span.ui-button-text').text('Loading');
92 this.element.find('button#save_notebook').button('disable');
93 };
94
95
47 96 IPython.SaveWidget = SaveWidget;
48 97
49 98 return IPython;
@@ -129,17 +129,19 b' var IPython = (function (IPython) {'
129 129
130 130 TextCell.prototype.fromJSON = function (data) {
131 131 if (data.cell_type === 'text') {
132 this.set_text(data.text);
133 this.grow(this.element.find("textarea.text_cell_input"));
132 if (data.text !== undefined) {
133 this.set_text(data.text);
134 this.grow(this.element.find("textarea.text_cell_input"));
135 };
134 136 };
135 137 }
136 138
137 139
138 140 TextCell.prototype.toJSON = function () {
139 return {
140 cell_type : 'text',
141 text : this.get_text(),
142 };
141 var data = {}
142 data.cell_type = 'text';
143 data.text = this.get_text();
144 return data;
143 145 };
144 146
145 147 IPython.TextCell = TextCell;
@@ -33,6 +33,7 b''
33 33 <span id="ipython_notebook"><h1>IPython Notebook</h1></span>
34 34 <span id="save_widget">
35 35 <input type="text" id="notebook_name" size="20"></textarea>
36 <span id="notebook_id" style="display:none">{{notebook_id}}</span>
36 37 <button id="save_notebook">Save</button>
37 38 </span>
38 39 <span id="kernel_status">Idle</span>
@@ -52,6 +53,18 b''
52 53 </span>
53 54 <span class="section_row_header">Actions</span>
54 55 </div>
56 <div class="section_row">
57 <span class="section_row_buttons">
58 <button id="download_notebook">Export</button>
59 </span>
60 <span>
61 <select id="download_format">
62 <option value="xml">xml</option>
63 <option value="json">json</option>
64 <option value="py">py</option>
65 </select>
66 </span>
67 </div>
55 68 </div>
56 69 </div>
57 70
@@ -5,6 +5,11 b' import re'
5 5 from IPython.nbformat import v2
6 6 from IPython.nbformat import v1
7 7
8 from IPython.nbformat.v2 import (
9 NotebookNode,
10 new_code_cell, new_text_cell, new_notebook, new_output, new_worksheet
11 )
12
8 13
9 14 current_nbformat = 2
10 15
@@ -36,10 +36,11 b' class PyWriter(NotebookWriter):'
36 36 for ws in nb.worksheets:
37 37 for cell in ws.cells:
38 38 if cell.cell_type == 'code':
39 input = cell.input
40 lines.extend([u'# <codecell>',u''])
41 lines.extend(input.splitlines())
42 lines.extend([u'',u'# </codecell>'])
39 input = cell.get('input')
40 if input is not None:
41 lines.extend([u'# <codecell>',u''])
42 lines.extend(input.splitlines())
43 lines.extend([u'',u'# </codecell>'])
43 44 lines.append('')
44 45 return unicode('\n'.join(lines))
45 46
General Comments 0
You need to be logged in to leave comments. Login now