Merge pull request #6809 from minrk/rm-contents-path-name

remove separate 'path' and 'name' from ContentsManager
Thomas Kluyver 11 years ago
commit 28cc4e9c84

@ -298,7 +298,7 @@ class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
@web.authenticated
def get(self, path):
if os.path.splitext(path)[1] == '.ipynb':
name = os.path.basename(path)
name = path.rsplit('/', 1)[-1]
self.set_header('Content-Type', 'application/json')
self.set_header('Content-Disposition','attachment; filename="%s"' % name)
@ -418,43 +418,42 @@ class ApiVersionHandler(IPythonHandler):
# not authenticated, so give as few info as possible
self.finish(json.dumps({"version":IPython.__version__}))
class TrailingSlashHandler(web.RequestHandler):
"""Simple redirect handler that strips trailing slashes
This should be the first, highest priority handler.
"""
SUPPORTED_METHODS = ['GET']
def get(self):
self.redirect(self.request.uri.rstrip('/'))
post = put = get
class FilesRedirectHandler(IPythonHandler):
"""Handler for redirecting relative URLs to the /files/ handler"""
def get(self, path=''):
cm = self.contents_manager
if cm.path_exists(path):
if cm.dir_exists(path):
# it's a *directory*, redirect to /tree
url = url_path_join(self.base_url, 'tree', path)
else:
orig_path = path
# otherwise, redirect to /files
parts = path.split('/')
path = '/'.join(parts[:-1])
name = parts[-1]
if not cm.file_exists(name=name, path=path) and 'files' in parts:
if not cm.file_exists(path=path) and 'files' in parts:
# redirect without files/ iff it would 404
# this preserves pre-2.0-style 'files/' links
self.log.warn("Deprecated files/ URL: %s", orig_path)
parts.remove('files')
path = '/'.join(parts[:-1])
path = '/'.join(parts)
if not cm.file_exists(name=name, path=path):
if not cm.file_exists(path=path):
raise web.HTTPError(404)
url = url_path_join(self.base_url, 'files', path, name)
url = url_path_join(self.base_url, 'files', path)
url = url_escape(url)
self.log.debug("Redirecting %s to %s", self.request.path, url)
self.redirect(url)
@ -464,11 +463,9 @@ class FilesRedirectHandler(IPythonHandler):
# URL pattern fragments for re-use
#-----------------------------------------------------------------------------
path_regex = r"(?P<path>(?:/.*)*)"
notebook_name_regex = r"(?P<name>[^/]+\.ipynb)"
notebook_path_regex = "%s/%s" % (path_regex, notebook_name_regex)
file_name_regex = r"(?P<name>[^/]+)"
file_path_regex = "%s/%s" % (path_regex, file_name_regex)
# path matches any number of `/foo[/bar...]` or just `/` or ''
path_regex = r"(?P<path>(?:(?:/[^/]+)+|/?))"
notebook_path_regex = r"(?P<path>(?:/[^/]+)+\.ipynb)"
#-----------------------------------------------------------------------------
# URL to handler mappings

@ -17,13 +17,18 @@ class FilesHandler(IPythonHandler):
@web.authenticated
def get(self, path):
cm = self.settings['contents_manager']
cm = self.contents_manager
if cm.is_hidden(path):
self.log.info("Refusing to serve hidden file, via 404 Error")
raise web.HTTPError(404)
path, name = os.path.split(path)
model = cm.get_model(name, path)
path = path.strip('/')
if '/' in path:
_, name = path.rsplit('/', 1)
else:
name = path
model = cm.get_model(path)
if self.get_argument("download", False):
self.set_header('Content-Disposition','attachment; filename="%s"' % name)

@ -76,12 +76,13 @@ class NbconvertFileHandler(IPythonHandler):
SUPPORTED_METHODS = ('GET',)
@web.authenticated
def get(self, format, path='', name=None):
def get(self, format, path):
exporter = get_exporter(format, config=self.config, log=self.log)
path = path.strip('/')
model = self.contents_manager.get_model(name=name, path=path)
model = self.contents_manager.get_model(path=path)
name = model['name']
self.set_header('Last-Modified', model['last_modified'])
@ -109,7 +110,7 @@ class NbconvertFileHandler(IPythonHandler):
class NbconvertPostHandler(IPythonHandler):
SUPPORTED_METHODS = ('POST',)
@web.authenticated
@web.authenticated
def post(self, format):
exporter = get_exporter(format, config=self.config)

@ -17,18 +17,16 @@ from ..utils import url_escape
class NotebookHandler(IPythonHandler):
@web.authenticated
def get(self, path='', name=None):
def get(self, path):
"""get renders the notebook template if a name is given, or
redirects to the '/files/' handler if the name is not given."""
path = path.strip('/')
cm = self.contents_manager
if name is None:
raise web.HTTPError(500, "This shouldn't be accessible: %s" % self.request.uri)
# a .ipynb filename was given
if not cm.file_exists(name, path):
raise web.HTTPError(404, u'Notebook does not exist: %s/%s' % (path, name))
name = url_escape(name)
if not cm.file_exists(path):
raise web.HTTPError(404, u'Notebook does not exist: %s' % path)
name = url_escape(path.rsplit('/', 1)[-1])
path = url_escape(path)
self.write(self.render_template('notebook.html',
notebook_path=path,

@ -187,9 +187,10 @@ class NotebookWebApplication(web.Application):
return settings
def init_handlers(self, settings):
# Load the (URL pattern, handler) tuples for each component.
"""Load the (URL pattern, handler) tuples for each component."""
# Order matters. The first handler to match the URL will handle the request.
handlers = []
handlers.extend(load_handlers('base.handlers'))
handlers.extend(load_handlers('tree.handlers'))
handlers.extend(load_handlers('auth.login'))
handlers.extend(load_handlers('auth.logout'))
@ -206,6 +207,8 @@ class NotebookWebApplication(web.Application):
handlers.append(
(r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}),
)
# register base handlers last
handlers.extend(load_handlers('base.handlers'))
# set the URL that will be redirected from `/`
handlers.append(
(r'/?', web.RedirectHandler, {

@ -61,27 +61,22 @@ class FileContentsManager(ContentsManager):
except OSError as e:
self.log.debug("copystat on %s failed", dest, exc_info=True)
def _get_os_path(self, name=None, path=''):
"""Given a filename and API path, return its file system
path.
def _get_os_path(self, path):
"""Given an API path, return its file system path.
Parameters
----------
name : string
A filename
path : string
The relative API path to the named file.
Returns
-------
path : string
API path to be evaluated relative to root_dir.
Native, absolute OS path to for a file.
"""
if name is not None:
path = url_path_join(path, name)
return to_os_path(path, self.root_dir)
def path_exists(self, path):
def dir_exists(self, path):
"""Does the API-style path refer to an extant directory?
API-style wrapper for os.path.isdir
@ -112,25 +107,22 @@ class FileContentsManager(ContentsManager):
Returns
-------
exists : bool
Whether the path is hidden.
hidden : bool
Whether the path exists and is hidden.
"""
path = path.strip('/')
os_path = self._get_os_path(path=path)
return is_hidden(os_path, self.root_dir)
def file_exists(self, name, path=''):
def file_exists(self, path):
"""Returns True if the file exists, else returns False.
API-style wrapper for os.path.isfile
Parameters
----------
name : string
The name of the file you are checking.
path : string
The relative path to the file's directory (with '/' as separator)
The relative path to the file (with '/' as separator)
Returns
-------
@ -138,20 +130,18 @@ class FileContentsManager(ContentsManager):
Whether the file exists.
"""
path = path.strip('/')
nbpath = self._get_os_path(name, path=path)
return os.path.isfile(nbpath)
os_path = self._get_os_path(path)
return os.path.isfile(os_path)
def exists(self, name=None, path=''):
"""Returns True if the path [and name] exists, else returns False.
def exists(self, path):
"""Returns True if the path exists, else returns False.
API-style wrapper for os.path.exists
Parameters
----------
name : string
The name of the file you are checking.
path : string
The relative path to the file's directory (with '/' as separator)
The API path to the file (with '/' as separator)
Returns
-------
@ -159,32 +149,31 @@ class FileContentsManager(ContentsManager):
Whether the target exists.
"""
path = path.strip('/')
os_path = self._get_os_path(name, path=path)
os_path = self._get_os_path(path=path)
return os.path.exists(os_path)
def _base_model(self, name, path=''):
def _base_model(self, path):
"""Build the common base of a contents model"""
os_path = self._get_os_path(name, path)
os_path = self._get_os_path(path)
info = os.stat(os_path)
last_modified = tz.utcfromtimestamp(info.st_mtime)
created = tz.utcfromtimestamp(info.st_ctime)
# Create the base model.
model = {}
model['name'] = name
model['name'] = path.rsplit('/', 1)[-1]
model['path'] = path
model['last_modified'] = last_modified
model['created'] = created
model['content'] = None
model['format'] = None
model['message'] = None
return model
def _dir_model(self, name, path='', content=True):
def _dir_model(self, path, content=True):
"""Build a model for a directory
if content is requested, will include a listing of the directory
"""
os_path = self._get_os_path(name, path)
os_path = self._get_os_path(path)
four_o_four = u'directory does not exist: %r' % os_path
@ -196,39 +185,43 @@ class FileContentsManager(ContentsManager):
)
raise web.HTTPError(404, four_o_four)
if name is None:
if '/' in path:
path, name = path.rsplit('/', 1)
else:
name = ''
model = self._base_model(name, path)
model = self._base_model(path)
model['type'] = 'directory'
dir_path = u'{}/{}'.format(path, name)
if content:
model['content'] = contents = []
for os_path in glob.glob(self._get_os_path('*', dir_path)):
name = os.path.basename(os_path)
os_dir = self._get_os_path(path)
for name in os.listdir(os_dir):
os_path = os.path.join(os_dir, name)
# skip over broken symlinks in listing
if not os.path.exists(os_path):
self.log.warn("%s doesn't exist", os_path)
continue
elif not os.path.isfile(os_path) and not os.path.isdir(os_path):
self.log.debug("%s not a regular file", os_path)
continue
if self.should_list(name) and not is_hidden(os_path, self.root_dir):
contents.append(self.get_model(name=name, path=dir_path, content=False))
contents.append(self.get_model(
path='%s/%s' % (path, name),
content=False)
)
model['format'] = 'json'
return model
def _file_model(self, name, path='', content=True):
def _file_model(self, path, content=True):
"""Build a model for a file
if content is requested, include the file contents.
UTF-8 text files will be unicode, binary files will be base64-encoded.
"""
model = self._base_model(name, path)
model = self._base_model(path)
model['type'] = 'file'
if content:
os_path = self._get_os_path(name, path)
os_path = self._get_os_path(path)
if not os.path.isfile(os_path):
# could be FIFO
raise web.HTTPError(400, "Cannot get content of non-file %s" % os_path)
with io.open(os_path, 'rb') as f:
bcontent = f.read()
try:
@ -241,34 +234,32 @@ class FileContentsManager(ContentsManager):
return model
def _notebook_model(self, name, path='', content=True):
def _notebook_model(self, path, content=True):
"""Build a notebook model
if content is requested, the notebook content will be populated
as a JSON structure (not double-serialized)
"""
model = self._base_model(name, path)
model = self._base_model(path)
model['type'] = 'notebook'
if content:
os_path = self._get_os_path(name, path)
os_path = self._get_os_path(path)
with io.open(os_path, 'r', encoding='utf-8') as f:
try:
nb = nbformat.read(f, as_version=4)
except Exception as e:
raise web.HTTPError(400, u"Unreadable Notebook: %s %r" % (os_path, e))
self.mark_trusted_cells(nb, name, path)
self.mark_trusted_cells(nb, path)
model['content'] = nb
model['format'] = 'json'
self.validate_notebook_model(model)
return model
def get_model(self, name, path='', content=True):
""" Takes a path and name for an entity and returns its model
def get_model(self, path, content=True):
""" Takes a path for an entity and returns its model
Parameters
----------
name : str
the name of the target
path : str
the API path that describes the relative path for the target
@ -280,32 +271,29 @@ class FileContentsManager(ContentsManager):
"""
path = path.strip('/')
if not self.exists(name=name, path=path):
raise web.HTTPError(404, u'No such file or directory: %s/%s' % (path, name))
if not self.exists(path):
raise web.HTTPError(404, u'No such file or directory: %s' % path)
os_path = self._get_os_path(name, path)
os_path = self._get_os_path(path)
if os.path.isdir(os_path):
model = self._dir_model(name, path, content)
elif name.endswith('.ipynb'):
model = self._notebook_model(name, path, content)
model = self._dir_model(path, content=content)
elif path.endswith('.ipynb'):
model = self._notebook_model(path, content=content)
else:
model = self._file_model(name, path, content)
model = self._file_model(path, content=content)
return model
def _save_notebook(self, os_path, model, name='', path=''):
def _save_notebook(self, os_path, model, path=''):
"""save a notebook file"""
# Save the notebook file
nb = nbformat.from_dict(model['content'])
self.check_and_sign(nb, name, path)
if 'name' in nb['metadata']:
nb['metadata']['name'] = u''
self.check_and_sign(nb, path)
with atomic_writing(os_path, encoding='utf-8') as f:
nbformat.write(nb, f, version=nbformat.NO_CONVERT)
def _save_file(self, os_path, model, name='', path=''):
def _save_file(self, os_path, model, path=''):
"""save a non-notebook file"""
fmt = model.get('format', None)
if fmt not in {'text', 'base64'}:
@ -322,7 +310,7 @@ class FileContentsManager(ContentsManager):
with atomic_writing(os_path, text=False) as f:
f.write(bcontent)
def _save_directory(self, os_path, model, name='', path=''):
def _save_directory(self, os_path, model, path=''):
"""create a directory"""
if is_hidden(os_path, self.root_dir):
raise web.HTTPError(400, u'Cannot create hidden directory %r' % os_path)
@ -333,7 +321,7 @@ class FileContentsManager(ContentsManager):
else:
self.log.debug("Directory %r already exists", os_path)
def save(self, model, name='', path=''):
def save(self, model, path=''):
"""Save the file model and return the model with no content."""
path = path.strip('/')
@ -343,24 +331,18 @@ class FileContentsManager(ContentsManager):
raise web.HTTPError(400, u'No file content provided')
# One checkpoint should always exist
if self.file_exists(name, path) and not self.list_checkpoints(name, path):
self.create_checkpoint(name, path)
if self.file_exists(path) and not self.list_checkpoints(path):
self.create_checkpoint(path)
new_path = model.get('path', path).strip('/')
new_name = model.get('name', name)
if path != new_path or name != new_name:
self.rename(name, path, new_name, new_path)
os_path = self._get_os_path(new_name, new_path)
os_path = self._get_os_path(path)
self.log.debug("Saving %s", os_path)
try:
if model['type'] == 'notebook':
self._save_notebook(os_path, model, new_name, new_path)
self._save_notebook(os_path, model, path)
elif model['type'] == 'file':
self._save_file(os_path, model, new_name, new_path)
self._save_file(os_path, model, path)
elif model['type'] == 'directory':
self._save_directory(os_path, model, new_name, new_path)
self._save_directory(os_path, model, path)
else:
raise web.HTTPError(400, "Unhandled contents type: %s" % model['type'])
except web.HTTPError:
@ -373,29 +355,28 @@ class FileContentsManager(ContentsManager):
self.validate_notebook_model(model)
validation_message = model.get('message', None)
model = self.get_model(new_name, new_path, content=False)
model = self.get_model(path, content=False)
if validation_message:
model['message'] = validation_message
return model
def update(self, model, name, path=''):
"""Update the file's path and/or name
def update(self, model, path):
"""Update the file's path
For use in PATCH requests, to enable renaming a file without
re-uploading its contents. Only used for renaming at the moment.
"""
path = path.strip('/')
new_name = model.get('name', name)
new_path = model.get('path', path).strip('/')
if path != new_path or name != new_name:
self.rename(name, path, new_name, new_path)
model = self.get_model(new_name, new_path, content=False)
if path != new_path:
self.rename(path, new_path)
model = self.get_model(new_path, content=False)
return model
def delete(self, name, path=''):
"""Delete file by name and path."""
def delete(self, path):
"""Delete file at path."""
path = path.strip('/')
os_path = self._get_os_path(name, path)
os_path = self._get_os_path(path)
rm = os.unlink
if os.path.isdir(os_path):
listing = os.listdir(os_path)
@ -406,9 +387,9 @@ class FileContentsManager(ContentsManager):
raise web.HTTPError(404, u'File does not exist: %s' % os_path)
# clear checkpoints
for checkpoint in self.list_checkpoints(name, path):
for checkpoint in self.list_checkpoints(path):
checkpoint_id = checkpoint['id']
cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
cp_path = self.get_checkpoint_path(checkpoint_id, path)
if os.path.isfile(cp_path):
self.log.debug("Unlinking checkpoint %s", cp_path)
os.unlink(cp_path)
@ -420,57 +401,59 @@ class FileContentsManager(ContentsManager):
self.log.debug("Unlinking file %s", os_path)
rm(os_path)
def rename(self, old_name, old_path, new_name, new_path):
def rename(self, old_path, new_path):
"""Rename a file."""
old_path = old_path.strip('/')
new_path = new_path.strip('/')
if new_name == old_name and new_path == old_path:
if new_path == old_path:
return
new_os_path = self._get_os_path(new_name, new_path)
old_os_path = self._get_os_path(old_name, old_path)
new_os_path = self._get_os_path(new_path)
old_os_path = self._get_os_path(old_path)
# Should we proceed with the move?
if os.path.isfile(new_os_path):
raise web.HTTPError(409, u'File with name already exists: %s' % new_os_path)
if os.path.exists(new_os_path):
raise web.HTTPError(409, u'File already exists: %s' % new_path)
# Move the file
try:
shutil.move(old_os_path, new_os_path)
except Exception as e:
raise web.HTTPError(500, u'Unknown error renaming file: %s %s' % (old_os_path, e))
raise web.HTTPError(500, u'Unknown error renaming file: %s %s' % (old_path, e))
# Move the checkpoints
old_checkpoints = self.list_checkpoints(old_name, old_path)
old_checkpoints = self.list_checkpoints(old_path)
for cp in old_checkpoints:
checkpoint_id = cp['id']
old_cp_path = self.get_checkpoint_path(checkpoint_id, old_name, old_path)
new_cp_path = self.get_checkpoint_path(checkpoint_id, new_name, new_path)
old_cp_path = self.get_checkpoint_path(checkpoint_id, old_path)
new_cp_path = self.get_checkpoint_path(checkpoint_id, new_path)
if os.path.isfile(old_cp_path):
self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
shutil.move(old_cp_path, new_cp_path)
# Checkpoint-related utilities
def get_checkpoint_path(self, checkpoint_id, name, path=''):
def get_checkpoint_path(self, checkpoint_id, path):
"""find the path to a checkpoint"""
path = path.strip('/')
parent, name = ('/' + path).rsplit('/', 1)
parent = parent.strip('/')
basename, ext = os.path.splitext(name)
filename = u"{name}-{checkpoint_id}{ext}".format(
name=basename,
checkpoint_id=checkpoint_id,
ext=ext,
)
os_path = self._get_os_path(path=path)
os_path = self._get_os_path(path=parent)
cp_dir = os.path.join(os_path, self.checkpoint_dir)
ensure_dir_exists(cp_dir)
cp_path = os.path.join(cp_dir, filename)
return cp_path
def get_checkpoint_model(self, checkpoint_id, name, path=''):
def get_checkpoint_model(self, checkpoint_id, path):
"""construct the info dict for a given checkpoint"""
path = path.strip('/')
cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
cp_path = self.get_checkpoint_path(checkpoint_id, path)
stats = os.stat(cp_path)
last_modified = tz.utcfromtimestamp(stats.st_mtime)
info = dict(
@ -481,43 +464,45 @@ class FileContentsManager(ContentsManager):
# public checkpoint API
def create_checkpoint(self, name, path=''):
def create_checkpoint(self, path):
"""Create a checkpoint from the current state of a file"""
path = path.strip('/')
src_path = self._get_os_path(name, path)
if not self.file_exists(path):
raise web.HTTPError(404)
src_path = self._get_os_path(path)
# only the one checkpoint ID:
checkpoint_id = u"checkpoint"
cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
self.log.debug("creating checkpoint for %s", name)
cp_path = self.get_checkpoint_path(checkpoint_id, path)
self.log.debug("creating checkpoint for %s", path)
self._copy(src_path, cp_path)
# return the checkpoint info
return self.get_checkpoint_model(checkpoint_id, name, path)
return self.get_checkpoint_model(checkpoint_id, path)
def list_checkpoints(self, name, path=''):
def list_checkpoints(self, path):
"""list the checkpoints for a given file
This contents manager currently only supports one checkpoint per file.
"""
path = path.strip('/')
checkpoint_id = "checkpoint"
os_path = self.get_checkpoint_path(checkpoint_id, name, path)
os_path = self.get_checkpoint_path(checkpoint_id, path)
if not os.path.exists(os_path):
return []
else:
return [self.get_checkpoint_model(checkpoint_id, name, path)]
return [self.get_checkpoint_model(checkpoint_id, path)]
def restore_checkpoint(self, checkpoint_id, name, path=''):
def restore_checkpoint(self, checkpoint_id, path):
"""restore a file to a checkpointed state"""
path = path.strip('/')
self.log.info("restoring %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)
self.log.info("restoring %s from checkpoint %s", path, checkpoint_id)
nb_path = self._get_os_path(path)
cp_path = self.get_checkpoint_path(checkpoint_id, path)
if not os.path.isfile(cp_path):
self.log.debug("checkpoint file does not exist: %s", cp_path)
raise web.HTTPError(404,
u'checkpoint does not exist: %s-%s' % (name, checkpoint_id)
u'checkpoint does not exist: %s@%s' % (path, checkpoint_id)
)
# ensure notebook is readable (never restore from an unreadable notebook)
if cp_path.endswith('.ipynb'):
@ -526,13 +511,13 @@ class FileContentsManager(ContentsManager):
self._copy(cp_path, nb_path)
self.log.debug("copying %s -> %s", cp_path, nb_path)
def delete_checkpoint(self, checkpoint_id, name, path=''):
def delete_checkpoint(self, checkpoint_id, path):
"""delete a file's checkpoint"""
path = path.strip('/')
cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
cp_path = self.get_checkpoint_path(checkpoint_id, path)
if not os.path.isfile(cp_path):
raise web.HTTPError(404,
u'Checkpoint does not exist: %s%s-%s' % (path, name, checkpoint_id)
u'Checkpoint does not exist: %s@%s' % (path, checkpoint_id)
)
self.log.debug("unlinking %s", cp_path)
os.unlink(cp_path)
@ -540,6 +525,10 @@ class FileContentsManager(ContentsManager):
def info_string(self):
return "Serving notebooks from local directory: %s" % self.root_dir
def get_kernel_path(self, name, path='', model=None):
def get_kernel_path(self, path, model=None):
"""Return the initial working dir a kernel associated with a given notebook"""
return os.path.join(self.root_dir, path)
if '/' in path:
parent_dir = path.rsplit('/', 1)[0]
else:
parent_dir = ''
return self._get_os_path(parent_dir)

@ -10,9 +10,9 @@ from tornado import web
from IPython.html.utils import url_path_join, url_escape
from IPython.utils.jsonutil import date_default
from IPython.html.base.handlers import (IPythonHandler, json_errors,
file_path_regex, path_regex,
file_name_regex)
from IPython.html.base.handlers import (
IPythonHandler, json_errors, path_regex,
)
def sort_key(model):
@ -29,38 +29,36 @@ class ContentsHandler(IPythonHandler):
SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE')
def location_url(self, name, path):
def location_url(self, path):
"""Return the full URL location of a file.
Parameters
----------
name : unicode
The base name of the file, such as "foo.ipynb".
path : unicode
The API path of the file, such as "foo/bar".
The API path of the file, such as "foo/bar.txt".
"""
return url_escape(url_path_join(
self.base_url, 'api', 'contents', path, name
self.base_url, 'api', 'contents', path
))
def _finish_model(self, model, location=True):
"""Finish a JSON request with a model, setting relevant headers, etc."""
if location:
location = self.location_url(model['name'], model['path'])
location = self.location_url(model['path'])
self.set_header('Location', location)
self.set_header('Last-Modified', model['last_modified'])
self.finish(json.dumps(model, default=date_default))
@web.authenticated
@json_errors
def get(self, path='', name=None):
def get(self, path=''):
"""Return a model for a file or directory.
A directory model contains a list of models (without content)
of the files and directories it contains.
"""
path = path or ''
model = self.contents_manager.get_model(name=name, path=path)
model = self.contents_manager.get_model(path=path)
if model['type'] == 'directory':
# group listing by type, then by name (case-insensitive)
# FIXME: sorting should be done in the frontends
@ -69,112 +67,83 @@ class ContentsHandler(IPythonHandler):
@web.authenticated
@json_errors
def patch(self, path='', name=None):
"""PATCH renames a notebook without re-uploading content."""
def patch(self, path=''):
"""PATCH renames a file or directory without re-uploading content."""
cm = self.contents_manager
if name is None:
raise web.HTTPError(400, u'Filename missing')
model = self.get_json_body()
if model is None:
raise web.HTTPError(400, u'JSON body missing')
model = cm.update(model, name, path)
model = cm.update(model, path)
self._finish_model(model)
def _copy(self, copy_from, path, copy_to=None):
"""Copy a file, optionally specifying the new name.
"""
self.log.info(u"Copying {copy_from} to {path}/{copy_to}".format(
def _copy(self, copy_from, copy_to=None):
"""Copy a file, optionally specifying a target directory."""
self.log.info(u"Copying {copy_from} to {copy_to}".format(
copy_from=copy_from,
path=path,
copy_to=copy_to or '',
))
model = self.contents_manager.copy(copy_from, copy_to, path)
model = self.contents_manager.copy(copy_from, copy_to)
self.set_status(201)
self._finish_model(model)
def _upload(self, model, path, name=None):
"""Handle upload of a new file
If name specified, create it in path/name,
otherwise create a new untitled file in path.
"""
self.log.info(u"Uploading file to %s/%s", path, name or '')
if name:
model['name'] = name
model = self.contents_manager.create_file(model, path)
def _upload(self, model, path):
"""Handle upload of a new file to path"""
self.log.info(u"Uploading file to %s", path)
model = self.contents_manager.new(model, path)
self.set_status(201)
self._finish_model(model)
def _create_empty_file(self, path, name=None, ext='.ipynb'):
"""Create an empty file in path
If name specified, create it in path/name.
"""
self.log.info(u"Creating new file in %s/%s", path, name or '')
model = {}
if name:
model['name'] = name
model = self.contents_manager.create_file(model, path=path, ext=ext)
def _new_untitled(self, path, type='', ext=''):
"""Create a new, empty untitled entity"""
self.log.info(u"Creating new %s in %s", type or 'file', path)
model = self.contents_manager.new_untitled(path=path, type=type, ext=ext)
self.set_status(201)
self._finish_model(model)
def _save(self, model, path, name):
def _save(self, model, path):
"""Save an existing file."""
self.log.info(u"Saving file at %s/%s", path, name)
model = self.contents_manager.save(model, name, path)
if model['path'] != path.strip('/') or model['name'] != name:
# a rename happened, set Location header
location = True
else:
location = False
self._finish_model(model, location)
self.log.info(u"Saving file at %s", path)
model = self.contents_manager.save(model, path)
self._finish_model(model)
@web.authenticated
@json_errors
def post(self, path='', name=None):
"""Create a new file or directory in the specified path.
def post(self, path=''):
"""Create a new file in the specified path.
POST creates new files or directories. The server always decides on the name.
POST creates new files. The server always decides on the name.
POST /api/contents/path
New untitled notebook in path. If content specified, upload a
notebook, otherwise start empty.
New untitled, empty file or directory.
POST /api/contents/path
with body {"copy_from" : "OtherNotebook.ipynb"}
with body {"copy_from" : "/path/to/OtherNotebook.ipynb"}
New copy of OtherNotebook in path
"""
if name is not None:
path = u'{}/{}'.format(path, name)
cm = self.contents_manager
if cm.file_exists(path):
raise web.HTTPError(400, "Cannot POST to existing files, use PUT instead.")
raise web.HTTPError(400, "Cannot POST to files, use PUT instead.")
if not cm.path_exists(path):
if not cm.dir_exists(path):
raise web.HTTPError(404, "No such directory: %s" % path)
model = self.get_json_body()
if model is not None:
copy_from = model.get('copy_from')
ext = model.get('ext', '.ipynb')
if model.get('content') is not None:
if copy_from:
raise web.HTTPError(400, "Can't upload and copy at the same time.")
self._upload(model, path)
elif copy_from:
ext = model.get('ext', '')
type = model.get('type', '')
if copy_from:
self._copy(copy_from, path)
else:
self._create_empty_file(path, ext=ext)
self._new_untitled(path, type=type, ext=ext)
else:
self._create_empty_file(path)
self._new_untitled(path)
@web.authenticated
@json_errors
def put(self, path='', name=None):
def put(self, path=''):
"""Saves the file in the location specified by name and path.
PUT is very similar to POST, but the requester specifies the name,
@ -184,39 +153,25 @@ class ContentsHandler(IPythonHandler):
Save notebook at ``path/Name.ipynb``. Notebook structure is specified
in `content` key of JSON request body. If content is not specified,
create a new empty notebook.
PUT /api/contents/path/Name.ipynb
with JSON body::
{
"copy_from" : "[path/to/]OtherNotebook.ipynb"
}
Copy OtherNotebook to Name
"""
if name is None:
raise web.HTTPError(400, "name must be specified with PUT.")
model = self.get_json_body()
if model:
copy_from = model.get('copy_from')
if copy_from:
if model.get('content'):
raise web.HTTPError(400, "Can't upload and copy at the same time.")
self._copy(copy_from, path, name)
elif self.contents_manager.file_exists(name, path):
self._save(model, path, name)
if model.get('copy_from'):
raise web.HTTPError(400, "Cannot copy with PUT, only POST")
if self.contents_manager.file_exists(path):
self._save(model, path)
else:
self._upload(model, path, name)
self._upload(model, path)
else:
self._create_empty_file(path, name)
self._new_untitled(path)
@web.authenticated
@json_errors
def delete(self, path='', name=None):
def delete(self, path=''):
"""delete a file in the given path"""
cm = self.contents_manager
self.log.warn('delete %s:%s', path, name)
cm.delete(name, path)
self.log.warn('delete %s', path)
cm.delete(path)
self.set_status(204)
self.finish()
@ -227,22 +182,22 @@ class CheckpointsHandler(IPythonHandler):
@web.authenticated
@json_errors
def get(self, path='', name=None):
def get(self, path=''):
"""get lists checkpoints for a file"""
cm = self.contents_manager
checkpoints = cm.list_checkpoints(name, path)
checkpoints = cm.list_checkpoints(path)
data = json.dumps(checkpoints, default=date_default)
self.finish(data)
@web.authenticated
@json_errors
def post(self, path='', name=None):
def post(self, path=''):
"""post creates a new checkpoint"""
cm = self.contents_manager
checkpoint = cm.create_checkpoint(name, path)
checkpoint = cm.create_checkpoint(path)
data = json.dumps(checkpoint, default=date_default)
location = url_path_join(self.base_url, 'api/contents',
path, name, 'checkpoints', checkpoint['id'])
path, 'checkpoints', checkpoint['id'])
self.set_header('Location', url_escape(location))
self.set_status(201)
self.finish(data)
@ -254,19 +209,19 @@ class ModifyCheckpointsHandler(IPythonHandler):
@web.authenticated
@json_errors
def post(self, path, name, checkpoint_id):
def post(self, path, checkpoint_id):
"""post restores a file from a checkpoint"""
cm = self.contents_manager
cm.restore_checkpoint(checkpoint_id, name, path)
cm.restore_checkpoint(checkpoint_id, path)
self.set_status(204)
self.finish()
@web.authenticated
@json_errors
def delete(self, path, name, checkpoint_id):
def delete(self, path, checkpoint_id):
"""delete clears a checkpoint for a given file"""
cm = self.contents_manager
cm.delete_checkpoint(checkpoint_id, name, path)
cm.delete_checkpoint(checkpoint_id, path)
self.set_status(204)
self.finish()
@ -294,10 +249,9 @@ class NotebooksRedirectHandler(IPythonHandler):
_checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
default_handlers = [
(r"/api/contents%s/checkpoints" % file_path_regex, CheckpointsHandler),
(r"/api/contents%s/checkpoints/%s" % (file_path_regex, _checkpoint_id_regex),
(r"/api/contents%s/checkpoints" % path_regex, CheckpointsHandler),
(r"/api/contents%s/checkpoints/%s" % (path_regex, _checkpoint_id_regex),
ModifyCheckpointsHandler),
(r"/api/contents%s" % file_path_regex, ContentsHandler),
(r"/api/contents%s" % path_regex, ContentsHandler),
(r"/api/notebooks/?(.*)", NotebooksRedirectHandler),
]

@ -33,14 +33,6 @@ class ContentsManager(LoggingConfigurable):
- if unspecified, path defaults to '',
indicating the root path.
name is also unicode, and refers to a specfic target:
- unicode, not url-escaped
- must not contain '/'
- It refers to an individual filename
- It may refer to a directory name,
in the case of listing or creating directories.
"""
notary = Instance(sign.NotebookNotary)
@ -69,7 +61,7 @@ class ContentsManager(LoggingConfigurable):
# ContentsManager API part 1: methods that must be
# implemented in subclasses.
def path_exists(self, path):
def dir_exists(self, path):
"""Does the API-style path (directory) actually exist?
Like os.path.isdir
@ -105,8 +97,8 @@ class ContentsManager(LoggingConfigurable):
"""
raise NotImplementedError
def file_exists(self, name, path=''):
"""Does a file exist at the given name and path?
def file_exists(self, path=''):
"""Does a file exist at the given path?
Like os.path.isfile
@ -126,15 +118,13 @@ class ContentsManager(LoggingConfigurable):
"""
raise NotImplementedError('must be implemented in a subclass')
def exists(self, name, path=''):
"""Does a file or directory exist at the given name and path?
def exists(self, path):
"""Does a file or directory exist at the given path?
Like os.path.exists
Parameters
----------
name : string
The name of the file you are checking.
path : string
The relative path to the file's directory (with '/' as separator)
@ -143,17 +133,17 @@ class ContentsManager(LoggingConfigurable):
exists : bool
Whether the target exists.
"""
return self.file_exists(name, path) or self.path_exists("%s/%s" % (path, name))
return self.file_exists(path) or self.dir_exists(path)
def get_model(self, name, path='', content=True):
def get_model(self, path, content=True):
"""Get the model of a file or directory with or without content."""
raise NotImplementedError('must be implemented in a subclass')
def save(self, model, name, path=''):
def save(self, model, path):
"""Save the file or directory and return the model with no content."""
raise NotImplementedError('must be implemented in a subclass')
def update(self, model, name, path=''):
def update(self, model, path):
"""Update the file or directory and return the model with no content.
For use in PATCH requests, to enable renaming a file without
@ -161,26 +151,26 @@ class ContentsManager(LoggingConfigurable):
"""
raise NotImplementedError('must be implemented in a subclass')
def delete(self, name, path=''):
"""Delete file or directory by name and path."""
def delete(self, path):
"""Delete file or directory by path."""
raise NotImplementedError('must be implemented in a subclass')
def create_checkpoint(self, name, path=''):
def create_checkpoint(self, path):
"""Create a checkpoint of the current state of a file
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, path):
"""Return a list of checkpoints for a given file"""
return []
def restore_checkpoint(self, checkpoint_id, name, path=''):
def restore_checkpoint(self, checkpoint_id, path):
"""Restore a file 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, path):
"""delete a checkpoint for a file"""
raise NotImplementedError("must be implemented in a subclass")
@ -190,8 +180,12 @@ class ContentsManager(LoggingConfigurable):
def info_string(self):
return "Serving contents"
def get_kernel_path(self, name, path='', model=None):
""" Return the path to start kernel in """
def get_kernel_path(self, path, model=None):
"""Return the API path for the kernel
KernelManagers can turn this value into a filesystem path,
or ignore it altogether.
"""
return path
def increment_filename(self, filename, path=''):
@ -214,7 +208,7 @@ class ContentsManager(LoggingConfigurable):
for i in itertools.count():
name = u'{basename}{i}{ext}'.format(basename=basename, i=i,
ext=ext)
if not self.file_exists(name, path):
if not self.exists(u'{}/{}'.format(path, name)):
break
return name
@ -223,85 +217,124 @@ class ContentsManager(LoggingConfigurable):
try:
validate(model['content'])
except ValidationError as e:
model['message'] = 'Notebook Validation failed: {}:\n{}'.format(
model['message'] = u'Notebook Validation failed: {}:\n{}'.format(
e.message, json.dumps(e.instance, indent=1, default=lambda obj: '<UNKNOWN>'),
)
return model
def create_file(self, model=None, path='', ext='.ipynb'):
"""Create a new file or directory and return its model with no content."""
def new_untitled(self, path='', type='', ext=''):
"""Create a new untitled file or directory in path
path must be a directory
File extension can be specified.
Use `new` to create files with a fully specified path (including filename).
"""
path = path.strip('/')
if not self.dir_exists(path):
raise HTTPError(404, 'No such directory: %s' % path)
model = {}
if type:
model['type'] = type
if ext == '.ipynb':
model.setdefault('type', 'notebook')
else:
model.setdefault('type', 'file')
if model['type'] == 'directory':
untitled = self.untitled_directory
elif model['type'] == 'notebook':
untitled = self.untitled_notebook
ext = '.ipynb'
elif model['type'] == 'file':
untitled = self.untitled_file
else:
raise HTTPError(400, "Unexpected model type: %r" % model['type'])
name = self.increment_filename(untitled + ext, path)
path = u'{0}/{1}'.format(path, name)
return self.new(model, path)
def new(self, model=None, path=''):
"""Create a new file or directory and return its model with no content.
To create a new untitled entity in a directory, use `new_untitled`.
"""
path = path.strip('/')
if model is None:
model = {}
if 'content' not in model and model.get('type', None) != 'directory':
if ext == '.ipynb':
if path.endswith('.ipynb'):
model.setdefault('type', 'notebook')
else:
model.setdefault('type', 'file')
# no content, not a directory, so fill out new-file model
if 'content' not in model and model['type'] != 'directory':
if model['type'] == 'notebook':
model['content'] = new_notebook()
model['type'] = 'notebook'
model['format'] = 'json'
else:
model['content'] = ''
model['type'] = 'file'
model['format'] = 'text'
if 'name' not in model:
if model['type'] == 'directory':
untitled = self.untitled_directory
elif model['type'] == 'notebook':
untitled = self.untitled_notebook
elif model['type'] == 'file':
untitled = self.untitled_file
else:
raise HTTPError(400, "Unexpected model type: %r" % model['type'])
model['name'] = self.increment_filename(untitled + ext, path)
model['path'] = path
model = self.save(model, model['name'], model['path'])
model = self.save(model, path)
return model
def copy(self, from_name, to_name=None, path=''):
def copy(self, from_path, to_path=None):
"""Copy an existing file and return its new model.
If to_name not specified, increment `from_name-Copy#.ext`.
If to_path not specified, it will be the parent directory of from_path.
If to_path is a directory, filename will increment `from_path-Copy#.ext`.
copy_from can be a full path to a file,
or just a base name. If a base name, `path` is used.
from_path must be a full path to a file.
"""
path = path.strip('/')
if '/' in from_name:
from_path, from_name = from_name.rsplit('/', 1)
path = from_path.strip('/')
if '/' in path:
from_dir, from_name = path.rsplit('/', 1)
else:
from_path = path
model = self.get_model(from_name, from_path)
from_dir = ''
from_name = path
model = self.get_model(path)
model.pop('path', None)
model.pop('name', None)
if model['type'] == 'directory':
raise HTTPError(400, "Can't copy directories")
if not to_name:
if not to_path:
to_path = from_dir
if self.dir_exists(to_path):
base, ext = os.path.splitext(from_name)
copy_name = u'{0}-Copy{1}'.format(base, ext)
to_name = self.increment_filename(copy_name, path)
model['name'] = to_name
model['path'] = path
model = self.save(model, to_name, path)
to_name = self.increment_filename(copy_name, to_path)
to_path = u'{0}/{1}'.format(to_path, to_name)
model = self.save(model, to_path)
return model
def log_info(self):
self.log.info(self.info_string())
def trust_notebook(self, name, path=''):
def trust_notebook(self, path):
"""Explicitly trust a notebook
Parameters
----------
name : string
The filename of the notebook
path : string
The notebook's directory
The path of a notebook
"""
model = self.get_model(name, path)
model = self.get_model(path)
nb = model['content']
self.log.warn("Trusting notebook %s/%s", path, name)
self.log.warn("Trusting notebook %s", path)
self.notary.mark_cells(nb, True)
self.save(model, name, path)
self.save(model, path)
def check_and_sign(self, nb, name='', path=''):
def check_and_sign(self, nb, path=''):
"""Check for trusted cells, and sign the notebook.
Called as a part of saving notebooks.
@ -310,17 +343,15 @@ class ContentsManager(LoggingConfigurable):
----------
nb : dict
The notebook dict
name : string
The filename of the notebook (for logging)
path : string
The notebook's directory (for logging)
The notebook's path (for logging)
"""
if self.notary.check_cells(nb):
self.notary.sign(nb)
else:
self.log.warn("Saving untrusted notebook %s/%s", path, name)
self.log.warn("Saving untrusted notebook %s", path)
def mark_trusted_cells(self, nb, name='', path=''):
def mark_trusted_cells(self, nb, path=''):
"""Mark cells as trusted if the notebook signature matches.
Called as a part of loading notebooks.
@ -329,14 +360,12 @@ class ContentsManager(LoggingConfigurable):
----------
nb : dict
The notebook object (in current nbformat)
name : string
The filename of the notebook (for logging)
path : string
The notebook's directory (for logging)
The notebook's path (for logging)
"""
trusted = self.notary.check_signature(nb)
if not trusted:
self.log.warn("Notebook %s/%s is not trusted", path, name)
self.log.warn("Notebook %s is not trusted", path)
self.notary.mark_cells(nb, trusted)
def should_list(self, name):

@ -46,56 +46,59 @@ class API(object):
def list(self, path='/'):
return self._req('GET', path)
def read(self, name, path='/'):
return self._req('GET', url_path_join(path, name))
def read(self, path):
return self._req('GET', path)
def create_untitled(self, path='/', ext=None):
def create_untitled(self, path='/', ext='.ipynb'):
body = None
if ext:
body = json.dumps({'ext': ext})
return self._req('POST', path, body)
def upload_untitled(self, body, path='/'):
return self._req('POST', path, body)
def mkdir_untitled(self, path='/'):
return self._req('POST', path, json.dumps({'type': 'directory'}))
def copy_untitled(self, copy_from, path='/'):
def copy(self, copy_from, path='/'):
body = json.dumps({'copy_from':copy_from})
return self._req('POST', path, body)
def create(self, name, path='/'):
return self._req('PUT', url_path_join(path, name))
def create(self, path='/'):
return self._req('PUT', path)
def upload(self, path, body):
return self._req('PUT', path, body)
def upload(self, name, body, path='/'):
return self._req('PUT', url_path_join(path, name), body)
def mkdir_untitled(self, path='/'):
return self._req('POST', path, json.dumps({'type': 'directory'}))
def mkdir(self, name, path='/'):
return self._req('PUT', url_path_join(path, name), json.dumps({'type': 'directory'}))
def mkdir(self, path='/'):
return self._req('PUT', path, json.dumps({'type': 'directory'}))
def copy(self, copy_from, copy_to, path='/'):
def copy_put(self, copy_from, path='/'):
body = json.dumps({'copy_from':copy_from})
return self._req('PUT', url_path_join(path, copy_to), body)
return self._req('PUT', path, body)
def save(self, name, body, path='/'):
return self._req('PUT', url_path_join(path, name), body)
def save(self, path, body):
return self._req('PUT', path, body)
def delete(self, name, path='/'):
return self._req('DELETE', url_path_join(path, name))
def delete(self, path='/'):
return self._req('DELETE', path)
def rename(self, name, path, new_name):
body = json.dumps({'name': new_name})
return self._req('PATCH', url_path_join(path, name), body)
def rename(self, path, new_path):
body = json.dumps({'path': new_path})
return self._req('PATCH', path, body)
def get_checkpoints(self, name, path):
return self._req('GET', url_path_join(path, name, 'checkpoints'))
def get_checkpoints(self, path):
return self._req('GET', url_path_join(path, 'checkpoints'))
def new_checkpoint(self, name, path):
return self._req('POST', url_path_join(path, name, 'checkpoints'))
def new_checkpoint(self, path):
return self._req('POST', url_path_join(path, 'checkpoints'))
def restore_checkpoint(self, name, path, checkpoint_id):
return self._req('POST', url_path_join(path, name, 'checkpoints', checkpoint_id))
def restore_checkpoint(self, path, checkpoint_id):
return self._req('POST', url_path_join(path, 'checkpoints', checkpoint_id))
def delete_checkpoint(self, name, path, checkpoint_id):
return self._req('DELETE', url_path_join(path, name, 'checkpoints', checkpoint_id))
def delete_checkpoint(self, path, checkpoint_id):
return self._req('DELETE', url_path_join(path, 'checkpoints', checkpoint_id))
class APITest(NotebookTestBase):
"""Test the kernels web service API"""
@ -131,8 +134,6 @@ class APITest(NotebookTestBase):
self.blob = os.urandom(100)
self.b64_blob = base64.encodestring(self.blob).decode('ascii')
for d in (self.dirs + self.hidden_dirs):
d.replace('/', os.sep)
if not os.path.isdir(pjoin(nbdir, d)):
@ -178,12 +179,12 @@ class APITest(NotebookTestBase):
nbs = notebooks_only(self.api.list(u'/unicodé/').json())
self.assertEqual(len(nbs), 1)
self.assertEqual(nbs[0]['name'], 'innonascii.ipynb')
self.assertEqual(nbs[0]['path'], u'unicodé')
self.assertEqual(nbs[0]['path'], u'unicodé/innonascii.ipynb')
nbs = notebooks_only(self.api.list('/foo/bar/').json())
self.assertEqual(len(nbs), 1)
self.assertEqual(nbs[0]['name'], 'baz.ipynb')
self.assertEqual(nbs[0]['path'], 'foo/bar')
self.assertEqual(nbs[0]['path'], 'foo/bar/baz.ipynb')
nbs = notebooks_only(self.api.list('foo').json())
self.assertEqual(len(nbs), 4)
@ -198,8 +199,11 @@ class APITest(NotebookTestBase):
self.assertEqual(nbnames, expected)
def test_list_dirs(self):
print(self.api.list().json())
dirs = dirs_only(self.api.list().json())
dir_names = {normalize('NFC', d['name']) for d in dirs}
print(dir_names)
print(self.top_level_dirs)
self.assertEqual(dir_names, self.top_level_dirs) # Excluding hidden dirs
def test_list_nonexistant_dir(self):
@ -208,8 +212,10 @@ class APITest(NotebookTestBase):
def test_get_nb_contents(self):
for d, name in self.dirs_nbs:
nb = self.api.read('%s.ipynb' % name, d+'/').json()
path = url_path_join(d, name + '.ipynb')
nb = self.api.read(path).json()
self.assertEqual(nb['name'], u'%s.ipynb' % name)
self.assertEqual(nb['path'], path)
self.assertEqual(nb['type'], 'notebook')
self.assertIn('content', nb)
self.assertEqual(nb['format'], 'json')
@ -220,12 +226,14 @@ class APITest(NotebookTestBase):
def test_get_contents_no_such_file(self):
# Name that doesn't exist - should be a 404
with assert_http_error(404):
self.api.read('q.ipynb', 'foo')
self.api.read('foo/q.ipynb')
def test_get_text_file_contents(self):
for d, name in self.dirs_nbs:
model = self.api.read(u'%s.txt' % name, d+'/').json()
path = url_path_join(d, name + '.txt')
model = self.api.read(path).json()
self.assertEqual(model['name'], u'%s.txt' % name)
self.assertEqual(model['path'], path)
self.assertIn('content', model)
self.assertEqual(model['format'], 'text')
self.assertEqual(model['type'], 'file')
@ -233,12 +241,14 @@ class APITest(NotebookTestBase):
# Name that doesn't exist - should be a 404
with assert_http_error(404):
self.api.read('q.txt', 'foo')
self.api.read('foo/q.txt')
def test_get_binary_file_contents(self):
for d, name in self.dirs_nbs:
model = self.api.read(u'%s.blob' % name, d+'/').json()
path = url_path_join(d, name + '.blob')
model = self.api.read(path).json()
self.assertEqual(model['name'], u'%s.blob' % name)
self.assertEqual(model['path'], path)
self.assertIn('content', model)
self.assertEqual(model['format'], 'base64')
self.assertEqual(model['type'], 'file')
@ -247,66 +257,71 @@ class APITest(NotebookTestBase):
# Name that doesn't exist - should be a 404
with assert_http_error(404):
self.api.read('q.txt', 'foo')
self.api.read('foo/q.txt')
def _check_created(self, resp, name, path, type='notebook'):
def _check_created(self, resp, path, type='notebook'):
self.assertEqual(resp.status_code, 201)
location_header = py3compat.str_to_unicode(resp.headers['Location'])
self.assertEqual(location_header, url_escape(url_path_join(u'/api/contents', path, name)))
self.assertEqual(location_header, url_escape(url_path_join(u'/api/contents', path)))
rjson = resp.json()
self.assertEqual(rjson['name'], name)
self.assertEqual(rjson['name'], path.rsplit('/', 1)[-1])
self.assertEqual(rjson['path'], path)
self.assertEqual(rjson['type'], type)
isright = os.path.isdir if type == 'directory' else os.path.isfile
assert isright(pjoin(
self.notebook_dir.name,
path.replace('/', os.sep),
name,
))
def test_create_untitled(self):
resp = self.api.create_untitled(path=u'å b')
self._check_created(resp, 'Untitled0.ipynb', u'å b')
self._check_created(resp, u'å b/Untitled0.ipynb')
# Second time
resp = self.api.create_untitled(path=u'å b')
self._check_created(resp, 'Untitled1.ipynb', u'å b')
self._check_created(resp, u'å b/Untitled1.ipynb')
# And two directories down
resp = self.api.create_untitled(path='foo/bar')
self._check_created(resp, 'Untitled0.ipynb', 'foo/bar')
self._check_created(resp, 'foo/bar/Untitled0.ipynb')
def test_create_untitled_txt(self):
resp = self.api.create_untitled(path='foo/bar', ext='.txt')
self._check_created(resp, 'untitled0.txt', 'foo/bar', type='file')
self._check_created(resp, 'foo/bar/untitled0.txt', type='file')
resp = self.api.read(path='foo/bar', name='untitled0.txt')
resp = self.api.read(path='foo/bar/untitled0.txt')
model = resp.json()
self.assertEqual(model['type'], 'file')
self.assertEqual(model['format'], 'text')
self.assertEqual(model['content'], '')
def test_upload_untitled(self):
nb = new_notebook()
nbmodel = {'content': nb, 'type': 'notebook'}
resp = self.api.upload_untitled(path=u'å b',
body=json.dumps(nbmodel))
self._check_created(resp, 'Untitled0.ipynb', u'å b')
def test_upload(self):
nb = new_notebook()
nbmodel = {'content': nb, 'type': 'notebook'}
resp = self.api.upload(u'Upload tést.ipynb', path=u'å b',
body=json.dumps(nbmodel))
self._check_created(resp, u'Upload tést.ipynb', u'å b')
path = u'å b/Upload tést.ipynb'
resp = self.api.upload(path, body=json.dumps(nbmodel))
self._check_created(resp, path)
def test_mkdir_untitled(self):
resp = self.api.mkdir_untitled(path=u'å b')
self._check_created(resp, u'å b/Untitled Folder0', type='directory')
# Second time
resp = self.api.mkdir_untitled(path=u'å b')
self._check_created(resp, u'å b/Untitled Folder1', type='directory')
# And two directories down
resp = self.api.mkdir_untitled(path='foo/bar')
self._check_created(resp, 'foo/bar/Untitled Folder0', type='directory')
def test_mkdir(self):
resp = self.api.mkdir(u'New ∂ir', path=u'å b')
self._check_created(resp, u'New ∂ir', u'å b', type='directory')
path = u'å b/New ∂ir'
resp = self.api.mkdir(path)
self._check_created(resp, path, type='directory')
def test_mkdir_hidden_400(self):
with assert_http_error(400):
resp = self.api.mkdir(u'.hidden', path=u'å b')
resp = self.api.mkdir(u'å b/.hidden')
def test_upload_txt(self):
body = u'ünicode téxt'
@ -315,11 +330,11 @@ class APITest(NotebookTestBase):
'format' : 'text',
'type' : 'file',
}
resp = self.api.upload(u'Upload tést.txt', path=u'å b',
body=json.dumps(model))
path = u'å b/Upload tést.txt'
resp = self.api.upload(path, body=json.dumps(model))
# check roundtrip
resp = self.api.read(path=u'å b', name=u'Upload tést.txt')
resp = self.api.read(path)
model = resp.json()
self.assertEqual(model['type'], 'file')
self.assertEqual(model['format'], 'text')
@ -333,13 +348,14 @@ class APITest(NotebookTestBase):
'format' : 'base64',
'type' : 'file',
}
resp = self.api.upload(u'Upload tést.blob', path=u'å b',
body=json.dumps(model))
path = u'å b/Upload tést.blob'
resp = self.api.upload(path, body=json.dumps(model))
# check roundtrip
resp = self.api.read(path=u'å b', name=u'Upload tést.blob')
resp = self.api.read(path)
model = resp.json()
self.assertEqual(model['type'], 'file')
self.assertEqual(model['path'], path)
self.assertEqual(model['format'], 'base64')
decoded = base64.decodestring(model['content'].encode('ascii'))
self.assertEqual(decoded, body)
@ -350,45 +366,52 @@ class APITest(NotebookTestBase):
nb.worksheets.append(ws)
ws.cells.append(v2.new_code_cell(input='print("hi")'))
nbmodel = {'content': nb, 'type': 'notebook'}
resp = self.api.upload(u'Upload tést.ipynb', path=u'å b',
body=json.dumps(nbmodel))
self._check_created(resp, u'Upload tést.ipynb', u'å b')
resp = self.api.read(u'Upload tést.ipynb', u'å b')
path = u'å b/Upload tést.ipynb'
resp = self.api.upload(path, body=json.dumps(nbmodel))
self._check_created(resp, path)
resp = self.api.read(path)
data = resp.json()
self.assertEqual(data['content']['nbformat'], 4)
def test_copy_untitled(self):
resp = self.api.copy_untitled(u'ç d.ipynb', path=u'å b')
self._check_created(resp, u'ç d-Copy0.ipynb', u'å b')
def test_copy(self):
resp = self.api.copy(u'ç d.ipynb', u'cøpy.ipynb', path=u'å b')
self._check_created(resp, u'cøpy.ipynb', u'å b')
resp = self.api.copy(u'å b/ç d.ipynb', u'unicodé')
self._check_created(resp, u'unicodé/ç d-Copy0.ipynb')
resp = self.api.copy(u'å b/ç d.ipynb', u'å b')
self._check_created(resp, u'å b/ç d-Copy0.ipynb')
def test_copy_path(self):
resp = self.api.copy(u'foo/a.ipynb', u'cøpyfoo.ipynb', path=u'å b')
self._check_created(resp, u'cøpyfoo.ipynb', u'å b')
resp = self.api.copy(u'foo/a.ipynb', u'å b')
self._check_created(resp, u'å b/a-Copy0.ipynb')
def test_copy_put_400(self):
with assert_http_error(400):
resp = self.api.copy_put(u'å b/ç d.ipynb', u'å b/cøpy.ipynb')
def test_copy_dir_400(self):
# can't copy directories
with assert_http_error(400):
resp = self.api.copy(u'å b', u'å c')
resp = self.api.copy(u'å b', u'foo')
def test_delete(self):
for d, name in self.dirs_nbs:
resp = self.api.delete('%s.ipynb' % name, d)
print('%r, %r' % (d, name))
resp = self.api.delete(url_path_join(d, name + '.ipynb'))
self.assertEqual(resp.status_code, 204)
for d in self.dirs + ['/']:
nbs = notebooks_only(self.api.list(d).json())
self.assertEqual(len(nbs), 0)
print('------')
print(d)
print(nbs)
self.assertEqual(nbs, [])
def test_delete_dirs(self):
# depth-first delete everything, so we don't try to delete empty directories
for name in sorted(self.dirs + ['/'], key=len, reverse=True):
listing = self.api.list(name).json()['content']
for model in listing:
self.api.delete(model['name'], model['path'])
self.api.delete(model['path'])
listing = self.api.list('/').json()['content']
self.assertEqual(listing, [])
@ -398,9 +421,10 @@ class APITest(NotebookTestBase):
self.api.delete(u'å b')
def test_rename(self):
resp = self.api.rename('a.ipynb', 'foo', 'z.ipynb')
resp = self.api.rename('foo/a.ipynb', 'foo/z.ipynb')
self.assertEqual(resp.headers['Location'].split('/')[-1], 'z.ipynb')
self.assertEqual(resp.json()['name'], 'z.ipynb')
self.assertEqual(resp.json()['path'], 'foo/z.ipynb')
assert os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'z.ipynb'))
nbs = notebooks_only(self.api.list('foo').json())
@ -410,41 +434,31 @@ class APITest(NotebookTestBase):
def test_rename_existing(self):
with assert_http_error(409):
self.api.rename('a.ipynb', 'foo', 'b.ipynb')
self.api.rename('foo/a.ipynb', 'foo/b.ipynb')
def test_save(self):
resp = self.api.read('a.ipynb', 'foo')
resp = self.api.read('foo/a.ipynb')
nbcontent = json.loads(resp.text)['content']
nb = from_dict(nbcontent)
nb.cells.append(new_markdown_cell(u'Created by test ³'))
nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb, 'type': 'notebook'}
resp = self.api.save('a.ipynb', path='foo', body=json.dumps(nbmodel))
nbmodel= {'content': nb, 'type': 'notebook'}
resp = self.api.save('foo/a.ipynb', body=json.dumps(nbmodel))
nbfile = pjoin(self.notebook_dir.name, 'foo', 'a.ipynb')
with io.open(nbfile, 'r', encoding='utf-8') as f:
newnb = read(f, as_version=4)
self.assertEqual(newnb.cells[0].source,
u'Created by test ³')
nbcontent = self.api.read('a.ipynb', 'foo').json()['content']
nbcontent = self.api.read('foo/a.ipynb').json()['content']
newnb = from_dict(nbcontent)
self.assertEqual(newnb.cells[0].source,
u'Created by test ³')
# Save and rename
nbmodel= {'name': 'a2.ipynb', 'path':'foo/bar', 'content': nb, 'type': 'notebook'}
resp = self.api.save('a.ipynb', path='foo', body=json.dumps(nbmodel))
saved = resp.json()
self.assertEqual(saved['name'], 'a2.ipynb')
self.assertEqual(saved['path'], 'foo/bar')
assert os.path.isfile(pjoin(self.notebook_dir.name,'foo','bar','a2.ipynb'))
assert not os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'a.ipynb'))
with assert_http_error(404):
self.api.read('a.ipynb', 'foo')
def test_checkpoints(self):
resp = self.api.read('a.ipynb', 'foo')
r = self.api.new_checkpoint('a.ipynb', 'foo')
resp = self.api.read('foo/a.ipynb')
r = self.api.new_checkpoint('foo/a.ipynb')
self.assertEqual(r.status_code, 201)
cp1 = r.json()
self.assertEqual(set(cp1), {'id', 'last_modified'})
@ -456,26 +470,26 @@ class APITest(NotebookTestBase):
hcell = new_markdown_cell('Created by test')
nb.cells.append(hcell)
# Save
nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb, 'type': 'notebook'}
resp = self.api.save('a.ipynb', path='foo', body=json.dumps(nbmodel))
nbmodel= {'content': nb, 'type': 'notebook'}
resp = self.api.save('foo/a.ipynb', body=json.dumps(nbmodel))
# List checkpoints
cps = self.api.get_checkpoints('a.ipynb', 'foo').json()
cps = self.api.get_checkpoints('foo/a.ipynb').json()
self.assertEqual(cps, [cp1])
nbcontent = self.api.read('a.ipynb', 'foo').json()['content']
nbcontent = self.api.read('foo/a.ipynb').json()['content']
nb = from_dict(nbcontent)
self.assertEqual(nb.cells[0].source, 'Created by test')
# Restore cp1
r = self.api.restore_checkpoint('a.ipynb', 'foo', cp1['id'])
r = self.api.restore_checkpoint('foo/a.ipynb', cp1['id'])
self.assertEqual(r.status_code, 204)
nbcontent = self.api.read('a.ipynb', 'foo').json()['content']
nbcontent = self.api.read('foo/a.ipynb').json()['content']
nb = from_dict(nbcontent)
self.assertEqual(nb.cells, [])
# Delete cp1
r = self.api.delete_checkpoint('a.ipynb', 'foo', cp1['id'])
r = self.api.delete_checkpoint('foo/a.ipynb', cp1['id'])
self.assertEqual(r.status_code, 204)
cps = self.api.get_checkpoints('a.ipynb', 'foo').json()
cps = self.api.get_checkpoints('foo/a.ipynb').json()
self.assertEqual(cps, [])

@ -42,7 +42,7 @@ class TestFileContentsManager(TestCase):
with TemporaryDirectory() as td:
root = td
fm = FileContentsManager(root_dir=root)
path = fm._get_os_path('test.ipynb', '/path/to/notebook/')
path = fm._get_os_path('/path/to/notebook/test.ipynb')
rel_path_list = '/path/to/notebook/test.ipynb'.split('/')
fs_path = os.path.join(fm.root_dir, *rel_path_list)
self.assertEqual(path, fs_path)
@ -53,7 +53,7 @@ class TestFileContentsManager(TestCase):
self.assertEqual(path, fs_path)
fm = FileContentsManager(root_dir=root)
path = fm._get_os_path('test.ipynb', '////')
path = fm._get_os_path('////test.ipynb')
fs_path = os.path.join(fm.root_dir, 'test.ipynb')
self.assertEqual(path, fs_path)
@ -64,8 +64,8 @@ class TestFileContentsManager(TestCase):
root = td
os.mkdir(os.path.join(td, subd))
fm = FileContentsManager(root_dir=root)
cp_dir = fm.get_checkpoint_path('cp', 'test.ipynb', '/')
cp_subdir = fm.get_checkpoint_path('cp', 'test.ipynb', '/%s/' % subd)
cp_dir = fm.get_checkpoint_path('cp', 'test.ipynb')
cp_subdir = fm.get_checkpoint_path('cp', '/%s/test.ipynb' % subd)
self.assertNotEqual(cp_dir, cp_subdir)
self.assertEqual(cp_dir, os.path.join(root, fm.checkpoint_dir, cp_name))
self.assertEqual(cp_subdir, os.path.join(root, subd, fm.checkpoint_dir, cp_name))
@ -101,46 +101,58 @@ class TestContentsManager(TestCase):
def new_notebook(self):
cm = self.contents_manager
model = cm.create_file()
model = cm.new_untitled(type='notebook')
name = model['name']
path = model['path']
full_model = cm.get_model(name, path)
full_model = cm.get_model(path)
nb = full_model['content']
self.add_code_cell(nb)
cm.save(full_model, name, path)
cm.save(full_model, path)
return nb, name, path
def test_create_file(self):
def test_new_untitled(self):
cm = self.contents_manager
# Test in root directory
model = cm.create_file()
model = cm.new_untitled(type='notebook')
assert isinstance(model, dict)
self.assertIn('name', model)
self.assertIn('path', model)
self.assertIn('type', model)
self.assertEqual(model['type'], 'notebook')
self.assertEqual(model['name'], 'Untitled0.ipynb')
self.assertEqual(model['path'], '')
self.assertEqual(model['path'], 'Untitled0.ipynb')
# Test in sub-directory
sub_dir = '/foo/'
self.make_dir(cm.root_dir, 'foo')
model = cm.create_file(None, sub_dir)
model = cm.new_untitled(type='directory')
assert isinstance(model, dict)
self.assertIn('name', model)
self.assertIn('path', model)
self.assertEqual(model['name'], 'Untitled0.ipynb')
self.assertEqual(model['path'], sub_dir.strip('/'))
self.assertIn('type', model)
self.assertEqual(model['type'], 'directory')
self.assertEqual(model['name'], 'Untitled Folder0')
self.assertEqual(model['path'], 'Untitled Folder0')
sub_dir = model['path']
model = cm.new_untitled(path=sub_dir)
assert isinstance(model, dict)
self.assertIn('name', model)
self.assertIn('path', model)
self.assertIn('type', model)
self.assertEqual(model['type'], 'file')
self.assertEqual(model['name'], 'untitled0')
self.assertEqual(model['path'], '%s/untitled0' % sub_dir)
def test_get(self):
cm = self.contents_manager
# Create a notebook
model = cm.create_file()
model = cm.new_untitled(type='notebook')
name = model['name']
path = model['path']
# Check that we 'get' on the notebook we just created
model2 = cm.get_model(name, path)
model2 = cm.get_model(path)
assert isinstance(model2, dict)
self.assertIn('name', model2)
self.assertIn('path', model2)
@ -150,14 +162,14 @@ class TestContentsManager(TestCase):
# Test in sub-directory
sub_dir = '/foo/'
self.make_dir(cm.root_dir, 'foo')
model = cm.create_file(None, sub_dir)
model2 = cm.get_model(name, sub_dir)
model = cm.new_untitled(path=sub_dir, ext='.ipynb')
model2 = cm.get_model(sub_dir + name)
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.strip('/'))
self.assertEqual(model2['path'], '{0}/{1}'.format(sub_dir.strip('/'), name))
@dec.skip_win32
def test_bad_symlink(self):
@ -165,7 +177,7 @@ class TestContentsManager(TestCase):
path = 'test bad symlink'
os_path = self.make_dir(cm.root_dir, path)
file_model = cm.create_file(path=path, ext='.txt')
file_model = cm.new_untitled(path=path, ext='.txt')
# create a broken symlink
os.symlink("target", os.path.join(os_path, "bad symlink"))
@ -175,16 +187,17 @@ class TestContentsManager(TestCase):
@dec.skip_win32
def test_good_symlink(self):
cm = self.contents_manager
path = 'test good symlink'
os_path = self.make_dir(cm.root_dir, path)
parent = 'test good symlink'
name = 'good symlink'
path = '{0}/{1}'.format(parent, name)
os_path = self.make_dir(cm.root_dir, parent)
file_model = cm.create_file(path=path, ext='.txt')
file_model = cm.new(path=parent + '/zfoo.txt')
# create a good symlink
os.symlink(file_model['name'], os.path.join(os_path, "good symlink"))
symlink_model = cm.get_model(name="good symlink", path=path, content=False)
dir_model = cm.get_model(path)
os.symlink(file_model['name'], os.path.join(os_path, name))
symlink_model = cm.get_model(path, content=False)
dir_model = cm.get_model(parent)
self.assertEqual(
sorted(dir_model['content'], key=lambda x: x['name']),
[symlink_model, file_model],
@ -193,53 +206,54 @@ class TestContentsManager(TestCase):
def test_update(self):
cm = self.contents_manager
# Create a notebook
model = cm.create_file()
model = cm.new_untitled(type='notebook')
name = model['name']
path = model['path']
# Change the name in the model for rename
model['name'] = 'test.ipynb'
model = cm.update(model, name, path)
model['path'] = 'test.ipynb'
model = cm.update(model, 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, cm.get_model, name, path)
self.assertRaises(HTTPError, cm.get_model, path)
# Test in sub-directory
# Create a directory and notebook in that directory
sub_dir = '/foo/'
self.make_dir(cm.root_dir, 'foo')
model = cm.create_file(None, sub_dir)
model = cm.new_untitled(path=sub_dir, type='notebook')
name = model['name']
path = model['path']
# Change the name in the model for rename
model['name'] = 'test_in_sub.ipynb'
model = cm.update(model, name, path)
d = path.rsplit('/', 1)[0]
new_path = model['path'] = d + '/test_in_sub.ipynb'
model = cm.update(model, 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.strip('/'))
self.assertEqual(model['path'], new_path)
# Make sure the old name is gone
self.assertRaises(HTTPError, cm.get_model, name, path)
self.assertRaises(HTTPError, cm.get_model, path)
def test_save(self):
cm = self.contents_manager
# Create a notebook
model = cm.create_file()
model = cm.new_untitled(type='notebook')
name = model['name']
path = model['path']
# Get the model with 'content'
full_model = cm.get_model(name, path)
full_model = cm.get_model(path)
# Save the notebook
model = cm.save(full_model, name, path)
model = cm.save(full_model, path)
assert isinstance(model, dict)
self.assertIn('name', model)
self.assertIn('path', model)
@ -250,18 +264,18 @@ class TestContentsManager(TestCase):
# Create a directory and notebook in that directory
sub_dir = '/foo/'
self.make_dir(cm.root_dir, 'foo')
model = cm.create_file(None, sub_dir)
model = cm.new_untitled(path=sub_dir, type='notebook')
name = model['name']
path = model['path']
model = cm.get_model(name, path)
model = cm.get_model(path)
# Change the name in the model for rename
model = cm.save(model, name, path)
model = cm.save(model, 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.strip('/'))
self.assertEqual(model['path'], 'foo/Untitled0.ipynb')
def test_delete(self):
cm = self.contents_manager
@ -269,36 +283,38 @@ class TestContentsManager(TestCase):
nb, name, path = self.new_notebook()
# Delete the notebook
cm.delete(name, path)
cm.delete(path)
# Check that a 'get' on the deleted notebook raises and error
self.assertRaises(HTTPError, cm.get_model, name, path)
self.assertRaises(HTTPError, cm.get_model, path)
def test_copy(self):
cm = self.contents_manager
path = u'å b'
parent = u'å b'
name = u'nb √.ipynb'
os.mkdir(os.path.join(cm.root_dir, path))
orig = cm.create_file({'name' : name}, path=path)
path = u'{0}/{1}'.format(parent, name)
os.mkdir(os.path.join(cm.root_dir, parent))
orig = cm.new(path=path)
# copy with unspecified name
copy = cm.copy(name, path=path)
copy = cm.copy(path)
self.assertEqual(copy['name'], orig['name'].replace('.ipynb', '-Copy0.ipynb'))
# copy with specified name
copy2 = cm.copy(name, u'copy 2.ipynb', path=path)
copy2 = cm.copy(path, u'å b/copy 2.ipynb')
self.assertEqual(copy2['name'], u'copy 2.ipynb')
self.assertEqual(copy2['path'], u'å b/copy 2.ipynb')
def test_trust_notebook(self):
cm = self.contents_manager
nb, name, path = self.new_notebook()
untrusted = cm.get_model(name, path)['content']
untrusted = cm.get_model(path)['content']
assert not cm.notary.check_cells(untrusted)
# print(untrusted)
cm.trust_notebook(name, path)
trusted = cm.get_model(name, path)['content']
cm.trust_notebook(path)
trusted = cm.get_model(path)['content']
# print(trusted)
assert cm.notary.check_cells(trusted)
@ -306,13 +322,13 @@ class TestContentsManager(TestCase):
cm = self.contents_manager
nb, name, path = self.new_notebook()
cm.mark_trusted_cells(nb, name, path)
cm.mark_trusted_cells(nb, path)
for cell in nb.cells:
if cell.cell_type == 'code':
assert not cell.metadata.trusted
cm.trust_notebook(name, path)
nb = cm.get_model(name, path)['content']
cm.trust_notebook(path)
nb = cm.get_model(path)['content']
for cell in nb.cells:
if cell.cell_type == 'code':
assert cell.metadata.trusted
@ -321,12 +337,12 @@ class TestContentsManager(TestCase):
cm = self.contents_manager
nb, name, path = self.new_notebook()
cm.mark_trusted_cells(nb, name, path)
cm.check_and_sign(nb, name, path)
cm.mark_trusted_cells(nb, path)
cm.check_and_sign(nb, path)
assert not cm.notary.check_signature(nb)
cm.trust_notebook(name, path)
nb = cm.get_model(name, path)['content']
cm.mark_trusted_cells(nb, name, path)
cm.check_and_sign(nb, name, path)
cm.trust_notebook(path)
nb = cm.get_model(path)['content']
cm.mark_trusted_cells(nb, path)
cm.check_and_sign(nb, path)
assert cm.notary.check_signature(nb)

@ -35,10 +35,6 @@ class SessionRootHandler(IPythonHandler):
model = self.get_json_body()
if model is None:
raise web.HTTPError(400, "No JSON data provided")
try:
name = model['notebook']['name']
except KeyError:
raise web.HTTPError(400, "Missing field in JSON data: notebook.name")
try:
path = model['notebook']['path']
except KeyError:
@ -50,11 +46,11 @@ class SessionRootHandler(IPythonHandler):
kernel_name = None
# Check to see if session exists
if sm.session_exists(name=name, path=path):
model = sm.get_session(name=name, path=path)
if sm.session_exists(path=path):
model = sm.get_session(path=path)
else:
try:
model = sm.create_session(name=name, path=path, kernel_name=kernel_name)
model = sm.create_session(path=path, kernel_name=kernel_name)
except NoSuchKernel:
msg = ("The '%s' kernel is not available. Please pick another "
"suitable kernel instead, or install that kernel." % kernel_name)
@ -92,8 +88,6 @@ class SessionHandler(IPythonHandler):
changes = {}
if 'notebook' in model:
notebook = model['notebook']
if 'name' in notebook:
changes['name'] = notebook['name']
if 'path' in notebook:
changes['path'] = notebook['path']

@ -21,7 +21,7 @@ class SessionManager(LoggingConfigurable):
# Session database initialized below
_cursor = None
_connection = None
_columns = {'session_id', 'name', 'path', 'kernel_id'}
_columns = {'session_id', 'path', 'kernel_id'}
@property
def cursor(self):
@ -29,7 +29,7 @@ class SessionManager(LoggingConfigurable):
if self._cursor is None:
self._cursor = self.connection.cursor()
self._cursor.execute("""CREATE TABLE session
(session_id, name, path, kernel_id)""")
(session_id, path, kernel_id)""")
return self._cursor
@property
@ -44,9 +44,9 @@ class SessionManager(LoggingConfigurable):
"""Close connection once SessionManager closes"""
self.cursor.close()
def session_exists(self, name, path):
def session_exists(self, path):
"""Check to see if the session for a given notebook exists"""
self.cursor.execute("SELECT * FROM session WHERE name=? AND path=?", (name, path))
self.cursor.execute("SELECT * FROM session WHERE path=?", (path,))
reply = self.cursor.fetchone()
if reply is None:
return False
@ -57,17 +57,17 @@ class SessionManager(LoggingConfigurable):
"Create a uuid for a new session"
return unicode_type(uuid.uuid4())
def create_session(self, name=None, path=None, kernel_name=None):
def create_session(self, path=None, kernel_name=None):
"""Creates a session and returns its model"""
session_id = self.new_session_id()
# allow nbm to specify kernels cwd
kernel_path = self.contents_manager.get_kernel_path(name=name, path=path)
kernel_path = self.contents_manager.get_kernel_path(path=path)
kernel_id = self.kernel_manager.start_kernel(path=kernel_path,
kernel_name=kernel_name)
return self.save_session(session_id, name=name, path=path,
return self.save_session(session_id, path=path,
kernel_id=kernel_id)
def save_session(self, session_id, name=None, path=None, kernel_id=None):
def save_session(self, session_id, path=None, kernel_id=None):
"""Saves the items for the session with the given session_id
Given a session_id (and any other of the arguments), this method
@ -78,10 +78,8 @@ class SessionManager(LoggingConfigurable):
----------
session_id : str
uuid for the session; this method must be given a session_id
name : str
the .ipynb notebook name that started the session
path : str
the path to the named notebook
the path for the given notebook
kernel_id : str
a uuid for the kernel associated with this session
@ -90,8 +88,8 @@ class SessionManager(LoggingConfigurable):
model : dict
a dictionary of the session model
"""
self.cursor.execute("INSERT INTO session VALUES (?,?,?,?)",
(session_id, name, path, kernel_id)
self.cursor.execute("INSERT INTO session VALUES (?,?,?)",
(session_id, path, kernel_id)
)
return self.get_session(session_id=session_id)
@ -105,7 +103,7 @@ class SessionManager(LoggingConfigurable):
----------
**kwargs : keyword argument
must be given one of the keywords and values from the session database
(i.e. session_id, name, path, kernel_id)
(i.e. session_id, path, kernel_id)
Returns
-------
@ -182,7 +180,6 @@ class SessionManager(LoggingConfigurable):
model = {
'id': row['session_id'],
'notebook': {
'name': row['name'],
'path': row['path']
},
'kernel': self.kernel_manager.kernel_model(row['kernel_id'])

@ -32,24 +32,24 @@ class TestSessionManager(TestCase):
def test_get_session(self):
sm = SessionManager(kernel_manager=DummyMKM())
session_id = sm.create_session(name='test.ipynb', path='/path/to/',
session_id = sm.create_session(path='/path/to/test.ipynb',
kernel_name='bar')['id']
model = sm.get_session(session_id=session_id)
expected = {'id':session_id,
'notebook':{'name':u'test.ipynb', 'path': u'/path/to/'},
'notebook':{'path': u'/path/to/test.ipynb'},
'kernel': {'id':u'A', 'name': 'bar'}}
self.assertEqual(model, expected)
def test_bad_get_session(self):
# Should raise error if a bad key is passed to the database.
sm = SessionManager(kernel_manager=DummyMKM())
session_id = sm.create_session(name='test.ipynb', path='/path/to/',
session_id = sm.create_session(path='/path/to/test.ipynb',
kernel_name='foo')['id']
self.assertRaises(TypeError, sm.get_session, bad_id=session_id) # Bad keyword
def test_get_session_dead_kernel(self):
sm = SessionManager(kernel_manager=DummyMKM())
session = sm.create_session(name='test1.ipynb', path='/path/to/1/', kernel_name='python')
session = sm.create_session(path='/path/to/1/test1.ipynb', kernel_name='python')
# kill the kernel
sm.kernel_manager.shutdown_kernel(session['kernel']['id'])
with self.assertRaises(KeyError):
@ -61,24 +61,33 @@ class TestSessionManager(TestCase):
def test_list_sessions(self):
sm = SessionManager(kernel_manager=DummyMKM())
sessions = [
sm.create_session(name='test1.ipynb', path='/path/to/1/', kernel_name='python'),
sm.create_session(name='test2.ipynb', path='/path/to/2/', kernel_name='python'),
sm.create_session(name='test3.ipynb', path='/path/to/3/', kernel_name='python'),
sm.create_session(path='/path/to/1/test1.ipynb', kernel_name='python'),
sm.create_session(path='/path/to/2/test2.ipynb', kernel_name='python'),
sm.create_session(path='/path/to/3/test3.ipynb', kernel_name='python'),
]
sessions = sm.list_sessions()
expected = [{'id':sessions[0]['id'], 'notebook':{'name':u'test1.ipynb',
'path': u'/path/to/1/'}, 'kernel':{'id':u'A', 'name':'python'}},
{'id':sessions[1]['id'], 'notebook': {'name':u'test2.ipynb',
'path': u'/path/to/2/'}, 'kernel':{'id':u'B', 'name':'python'}},
{'id':sessions[2]['id'], 'notebook':{'name':u'test3.ipynb',
'path': u'/path/to/3/'}, 'kernel':{'id':u'C', 'name':'python'}}]
expected = [
{
'id':sessions[0]['id'],
'notebook':{'path': u'/path/to/1/test1.ipynb'},
'kernel':{'id':u'A', 'name':'python'}
}, {
'id':sessions[1]['id'],
'notebook': {'path': u'/path/to/2/test2.ipynb'},
'kernel':{'id':u'B', 'name':'python'}
}, {
'id':sessions[2]['id'],
'notebook':{'path': u'/path/to/3/test3.ipynb'},
'kernel':{'id':u'C', 'name':'python'}
}
]
self.assertEqual(sessions, expected)
def test_list_sessions_dead_kernel(self):
sm = SessionManager(kernel_manager=DummyMKM())
sessions = [
sm.create_session(name='test1.ipynb', path='/path/to/1/', kernel_name='python'),
sm.create_session(name='test2.ipynb', path='/path/to/2/', kernel_name='python'),
sm.create_session(path='/path/to/1/test1.ipynb', kernel_name='python'),
sm.create_session(path='/path/to/2/test2.ipynb', kernel_name='python'),
]
# kill one of the kernels
sm.kernel_manager.shutdown_kernel(sessions[0]['kernel']['id'])
@ -87,8 +96,7 @@ class TestSessionManager(TestCase):
{
'id': sessions[1]['id'],
'notebook': {
'name': u'test2.ipynb',
'path': u'/path/to/2/',
'path': u'/path/to/2/test2.ipynb',
},
'kernel': {
'id': u'B',
@ -100,41 +108,47 @@ class TestSessionManager(TestCase):
def test_update_session(self):
sm = SessionManager(kernel_manager=DummyMKM())
session_id = sm.create_session(name='test.ipynb', path='/path/to/',
session_id = sm.create_session(path='/path/to/test.ipynb',
kernel_name='julia')['id']
sm.update_session(session_id, name='new_name.ipynb')
sm.update_session(session_id, path='/path/to/new_name.ipynb')
model = sm.get_session(session_id=session_id)
expected = {'id':session_id,
'notebook':{'name':u'new_name.ipynb', 'path': u'/path/to/'},
'notebook':{'path': u'/path/to/new_name.ipynb'},
'kernel':{'id':u'A', 'name':'julia'}}
self.assertEqual(model, expected)
def test_bad_update_session(self):
# try to update a session with a bad keyword ~ raise error
sm = SessionManager(kernel_manager=DummyMKM())
session_id = sm.create_session(name='test.ipynb', path='/path/to/',
session_id = sm.create_session(path='/path/to/test.ipynb',
kernel_name='ir')['id']
self.assertRaises(TypeError, sm.update_session, session_id=session_id, bad_kw='test.ipynb') # Bad keyword
def test_delete_session(self):
sm = SessionManager(kernel_manager=DummyMKM())
sessions = [
sm.create_session(name='test1.ipynb', path='/path/to/1/', kernel_name='python'),
sm.create_session(name='test2.ipynb', path='/path/to/2/', kernel_name='python'),
sm.create_session(name='test3.ipynb', path='/path/to/3/', kernel_name='python'),
sm.create_session(path='/path/to/1/test1.ipynb', kernel_name='python'),
sm.create_session(path='/path/to/2/test2.ipynb', kernel_name='python'),
sm.create_session(path='/path/to/3/test3.ipynb', kernel_name='python'),
]
sm.delete_session(sessions[1]['id'])
new_sessions = sm.list_sessions()
expected = [{'id':sessions[0]['id'], 'notebook':{'name':u'test1.ipynb',
'path': u'/path/to/1/'}, 'kernel':{'id':u'A', 'name':'python'}},
{'id':sessions[2]['id'], 'notebook':{'name':u'test3.ipynb',
'path': u'/path/to/3/'}, 'kernel':{'id':u'C', 'name':'python'}}]
expected = [{
'id': sessions[0]['id'],
'notebook': {'path': u'/path/to/1/test1.ipynb'},
'kernel': {'id':u'A', 'name':'python'}
}, {
'id': sessions[2]['id'],
'notebook': {'path': u'/path/to/3/test3.ipynb'},
'kernel': {'id':u'C', 'name':'python'}
}
]
self.assertEqual(new_sessions, expected)
def test_bad_delete_session(self):
# try to delete a session that doesn't exist ~ raise error
sm = SessionManager(kernel_manager=DummyMKM())
sm.create_session(name='test.ipynb', path='/path/to/', kernel_name='python')
sm.create_session(path='/path/to/test.ipynb', kernel_name='python')
self.assertRaises(TypeError, sm.delete_session, bad_kwarg='23424') # Bad keyword
self.assertRaises(web.HTTPError, sm.delete_session, session_id='23424') # nonexistant

@ -38,13 +38,13 @@ class SessionAPI(object):
def get(self, id):
return self._req('GET', id)
def create(self, name, path, kernel_name='python'):
body = json.dumps({'notebook': {'name':name, 'path':path},
def create(self, path, kernel_name='python'):
body = json.dumps({'notebook': {'path':path},
'kernel': {'name': kernel_name}})
return self._req('POST', '', body)
def modify(self, id, name, path):
body = json.dumps({'notebook': {'name':name, 'path':path}})
def modify(self, id, path):
body = json.dumps({'notebook': {'path':path}})
return self._req('PATCH', id, body)
def delete(self, id):
@ -78,12 +78,11 @@ class SessionAPITest(NotebookTestBase):
sessions = self.sess_api.list().json()
self.assertEqual(len(sessions), 0)
resp = self.sess_api.create('nb1.ipynb', 'foo')
resp = self.sess_api.create('foo/nb1.ipynb')
self.assertEqual(resp.status_code, 201)
newsession = resp.json()
self.assertIn('id', newsession)
self.assertEqual(newsession['notebook']['name'], 'nb1.ipynb')
self.assertEqual(newsession['notebook']['path'], 'foo')
self.assertEqual(newsession['notebook']['path'], 'foo/nb1.ipynb')
self.assertEqual(resp.headers['Location'], '/api/sessions/{0}'.format(newsession['id']))
sessions = self.sess_api.list().json()
@ -95,7 +94,7 @@ class SessionAPITest(NotebookTestBase):
self.assertEqual(got, newsession)
def test_delete(self):
newsession = self.sess_api.create('nb1.ipynb', 'foo').json()
newsession = self.sess_api.create('foo/nb1.ipynb').json()
sid = newsession['id']
resp = self.sess_api.delete(sid)
@ -108,10 +107,9 @@ class SessionAPITest(NotebookTestBase):
self.sess_api.get(sid)
def test_modify(self):
newsession = self.sess_api.create('nb1.ipynb', 'foo').json()
newsession = self.sess_api.create('foo/nb1.ipynb').json()
sid = newsession['id']
changed = self.sess_api.modify(sid, 'nb2.ipynb', '').json()
changed = self.sess_api.modify(sid, 'nb2.ipynb').json()
self.assertEqual(changed['id'], sid)
self.assertEqual(changed['notebook']['name'], 'nb2.ipynb')
self.assertEqual(changed['notebook']['path'], '')
self.assertEqual(changed['notebook']['path'], 'nb2.ipynb')

@ -272,11 +272,11 @@ define([
} else {
line = "background-color: ";
}
line = line + "rgb(" + r + "," + g + "," + b + ");"
if ( !attrs["style"] ) {
attrs["style"] = line;
line = line + "rgb(" + r + "," + g + "," + b + ");";
if ( !attrs.style ) {
attrs.style = line;
} else {
attrs["style"] += " " + line;
attrs.style += " " + line;
}
}
}
@ -285,7 +285,7 @@ define([
function ansispan(str) {
// ansispan function adapted from github.com/mmalecki/ansispan (MIT License)
// regular ansi escapes (using the table above)
var is_open = false
var is_open = false;
return str.replace(/\033\[(0?[01]|22|39)?([;\d]+)?m/g, function(match, prefix, pattern) {
if (!pattern) {
// [(01|22|39|)m close spans
@ -313,7 +313,7 @@ define([
return span + ">";
}
});
};
}
// Transform ANSI color escape codes into HTML <span> tags with css
// classes listed in the above ansi_colormap object. The actual color used
@ -392,6 +392,18 @@ define([
return url;
};
var url_path_split = function (path) {
// Like os.path.split for URLs.
// Always returns two strings, the directory path and the base filename
var idx = path.lastIndexOf('/');
if (idx === -1) {
return ['', path];
} else {
return [ path.slice(0, idx), path.slice(idx + 1) ];
}
};
var parse_url = function (url) {
// an `a` element with an href allows attr-access to the parsed segments of a URL
// a = parse_url("http://localhost:8888/path/name#hash")
@ -577,7 +589,7 @@ define([
wrapped_error.xhr_status = status;
wrapped_error.xhr_error = error;
return wrapped_error;
}
};
var utils = {
regex_split : regex_split,
@ -588,6 +600,7 @@ define([
points_to_pixels : points_to_pixels,
get_body_data : get_body_data,
parse_url : parse_url,
url_path_split : url_path_split,
url_path_join : url_path_join,
url_join_encode : url_join_encode,
encode_uri_components : encode_uri_components,

@ -151,6 +151,6 @@ require([
IPython.tooltip = notebook.tooltip;
events.trigger('app_initialized.NotebookApp');
notebook.load_notebook(common_options.notebook_name, common_options.notebook_path);
notebook.load_notebook(common_options.notebook_path);
});

@ -2,13 +2,14 @@
// Distributed under the terms of the Modified BSD License.
define([
'base/js/namespace',
'jquery',
'base/js/namespace',
'base/js/dialog',
'base/js/utils',
'notebook/js/tour',
'bootstrap',
'moment',
], function(IPython, $, utils, tour, bootstrap, moment) {
], function($, IPython, dialog, utils, tour, bootstrap, moment) {
"use strict";
var MenuBar = function (selector, options) {
@ -89,14 +90,14 @@ define([
this.element.find('#new_notebook').click(function () {
// Create a new notebook in the same path as the current
// notebook's path.
that.contents.new(that.notebook.notebook_path, null, {
ext: ".ipynb",
var parent = utils.url_path_split(that.notebook.notebook_path)[0];
that.contents.new_untitled(parent, {
type: "notebook",
extra_settings: {async: false}, // So we can open a new window afterwards
success: function (data) {
window.open(
utils.url_join_encode(
that.base_url, 'notebooks',
data.path, data.name
that.base_url, 'notebooks', data.path
), '_blank');
},
error: function(error) {

@ -212,13 +212,13 @@ define([
});
this.events.on('kernel_ready.Kernel', function(event, data) {
var kinfo = data.kernel.info_reply
var kinfo = data.kernel.info_reply;
var langinfo = kinfo.language_info || {};
if (!langinfo.name) langinfo.name = kinfo.language;
that.metadata.language_info = langinfo;
// Mode 'null' should be plain, unhighlighted text.
var cm_mode = langinfo.codemirror_mode || langinfo.language || 'null'
var cm_mode = langinfo.codemirror_mode || langinfo.language || 'null';
that.set_codemirror_mode(cm_mode);
});
@ -1029,7 +1029,7 @@ define([
text = '';
}
// metadata
target_cell.metadata = source_cell.metadata
target_cell.metadata = source_cell.metadata;
// We must show the editor before setting its contents
target_cell.unrender();
target_cell.set_text(text);
@ -1231,8 +1231,6 @@ define([
* @method split_cell
*/
Notebook.prototype.split_cell = function () {
var mdc = textcell.MarkdownCell;
var rc = textcell.RawCell;
var cell = this.get_selected_cell();
if (cell.is_splittable()) {
var texta = cell.get_pre_cursor();
@ -1251,8 +1249,6 @@ define([
* @method merge_cell_above
*/
Notebook.prototype.merge_cell_above = function () {
var mdc = textcell.MarkdownCell;
var rc = textcell.RawCell;
var index = this.get_selected_index();
var cell = this.get_cell(index);
var render = cell.rendered;
@ -1288,8 +1284,6 @@ define([
* @method merge_cell_below
*/
Notebook.prototype.merge_cell_below = function () {
var mdc = textcell.MarkdownCell;
var rc = textcell.RawCell;
var index = this.get_selected_index();
var cell = this.get_cell(index);
var render = cell.rendered;
@ -1523,9 +1517,9 @@ define([
}
this.codemirror_mode = newmode;
codecell.CodeCell.options_default.cm_config.mode = newmode;
modename = newmode.mode || newmode.name || newmode;
var modename = newmode.mode || newmode.name || newmode;
that = this;
var that = this;
utils.requireCodeMirrorMode(modename, function () {
$.map(that.get_cells(), function(cell, i) {
if (cell.cell_type === 'code'){
@ -1547,7 +1541,6 @@ define([
* @method start_session
*/
Notebook.prototype.start_session = function (kernel_name) {
var that = this;
if (this._session_starting) {
throw new session.SessionAlreadyStarting();
}
@ -1629,7 +1622,6 @@ define([
Notebook.prototype.execute_cell = function () {
// mode = shift, ctrl, alt
var cell = this.get_selected_cell();
var cell_index = this.find_cell_index(cell);
cell.execute();
this.command_mode();
@ -1758,7 +1750,9 @@ define([
* @param {String} name A new name for this notebook
*/
Notebook.prototype.set_notebook_name = function (name) {
var parent = utils.url_path_split(this.notebook_path)[0];
this.notebook_name = name;
this.notebook_path = utils.url_path_join(parent, name);
};
/**
@ -1795,6 +1789,7 @@ define([
// Save the metadata and name.
this.metadata = content.metadata;
this.notebook_name = data.name;
this.notebook_path = data.path;
var trusted = true;
// Trigger an event changing the kernel spec - this will set the default
@ -1807,7 +1802,7 @@ define([
if (this.metadata.language_info !== undefined) {
var langinfo = this.metadata.language_info;
// Mode 'null' should be plain, unhighlighted text.
var cm_mode = langinfo.codemirror_mode || langinfo.language || 'null'
var cm_mode = langinfo.codemirror_mode || langinfo.language || 'null';
this.set_codemirror_mode(cm_mode);
}
@ -1900,8 +1895,6 @@ define([
Notebook.prototype.save_notebook = function (extra_settings) {
// Create a JSON model to be sent to the server.
var model = {
name : this.notebook_name,
path : this.notebook_path,
type : "notebook",
content : this.toJSON()
};
@ -1909,11 +1902,11 @@ define([
var start = new Date().getTime();
var that = this;
this.contents.save(this.notebook_path, this.notebook_name, model, {
this.contents.save(this.notebook_path, model, {
extra_settings: extra_settings,
success: $.proxy(this.save_notebook_success, this, start),
error: function (error) {
that.events.trigger('notebook_save_failed.Notebook');
that.events.trigger('notebook_save_failed.Notebook', error);
}
});
};
@ -2031,15 +2024,15 @@ define([
Notebook.prototype.copy_notebook = function(){
var base_url = this.base_url;
this.contents.copy(this.notebook_path, null, this.notebook_name, {
var parent = utils.url_path_split(this.notebook_path)[0];
this.contents.copy(this.notebook_path, parent, {
// synchronous so we can open a new window on success
extra_settings: {async: false},
success: function (data) {
window.open(utils.url_join_encode(
base_url, 'notebooks', data.path, data.name
base_url, 'notebooks', data.path
), '_blank');
},
error : utils.log_ajax_error
}
});
};
@ -2049,11 +2042,13 @@ define([
}
var that = this;
this.contents.rename(this.notebook_path, this.notebook_name,
this.notebook_path, new_name, {
var parent = utils.url_path_split(this.notebook_path)[0];
var new_path = utils.url_path_join(parent, new_name);
this.contents.rename(this.notebook_path, new_path, {
success: function (json) {
var name = that.notebook_name = json.name;
that.session.rename_notebook(name, json.path);
that.notebook_name = json.name;
that.notebook_path = json.path;
that.session.rename_notebook(json.path);
that.events.trigger('notebook_renamed.Notebook', json);
},
error: $.proxy(this.rename_error, this)
@ -2061,7 +2056,7 @@ define([
};
Notebook.prototype.delete = function () {
this.contents.delete(this.notebook_name, this.notebook_path);
this.contents.delete(this.notebook_path);
};
Notebook.prototype.rename_error = function (error) {
@ -2100,13 +2095,13 @@ define([
* Request a notebook's data from the server.
*
* @method load_notebook
* @param {String} notebook_name and path A notebook to load
* @param {String} notebook_path A notebook to load
*/
Notebook.prototype.load_notebook = function (notebook_name, notebook_path) {
this.notebook_name = notebook_name;
Notebook.prototype.load_notebook = function (notebook_path) {
this.notebook_path = notebook_path;
this.notebook_name = utils.url_path_split(this.notebook_path)[1];
this.events.trigger('notebook_loading.Notebook');
this.contents.load(notebook_path, notebook_name, {
this.contents.get(notebook_path, {
success: $.proxy(this.load_notebook_success, this),
error: $.proxy(this.load_notebook_error, this)
});
@ -2121,7 +2116,7 @@ define([
* @param {Object} data JSON representation of a notebook
*/
Notebook.prototype.load_notebook_success = function (data) {
var failed;
var failed, msg;
try {
this.fromJSON(data);
} catch (e) {
@ -2146,12 +2141,11 @@ define([
}
if (data.message) {
var msg;
if (failed) {
msg = "The notebook also failed validation:"
msg = "The notebook also failed validation:";
} else {
msg = "An invalid notebook may not function properly." +
" The validation error was:"
" The validation error was:";
}
body.append($("<p>").text(
msg
@ -2192,7 +2186,7 @@ define([
src = " a newer notebook format ";
}
var msg = "This notebook has been converted from" + src +
msg = "This notebook has been converted from" + src +
"(v"+orig_nbformat+") to the current notebook " +
"format (v"+nbmodel.nbformat+"). The next time you save this notebook, the " +
"current notebook format will be used.";
@ -2219,7 +2213,7 @@ define([
var that = this;
var orig_vs = 'v' + nbmodel.nbformat + '.' + orig_nbformat_minor;
var this_vs = 'v' + nbmodel.nbformat + '.' + this.nbformat_minor;
var msg = "This notebook is version " + orig_vs + ", but we only fully support up to " +
msg = "This notebook is version " + orig_vs + ", but we only fully support up to " +
this_vs + ". You can still work with this notebook, but some features " +
"introduced in later notebook versions may not be available.";
@ -2270,7 +2264,7 @@ define([
Notebook.prototype.load_notebook_error = function (error) {
this.events.trigger('notebook_load_failed.Notebook', error);
var msg;
if (error.name = utils.XHR_ERROR && error.xhr.status === 500) {
if (error.name === utils.XHR_ERROR && error.xhr.status === 500) {
utils.log_ajax_error(error.xhr, error.xhr_status, error.xhr_error);
msg = "An unknown error occurred while loading this notebook. " +
"This version can load notebook formats " +
@ -2330,10 +2324,10 @@ define([
*/
Notebook.prototype.list_checkpoints = function () {
var that = this;
this.contents.list_checkpoints(this.notebook_path, this.notebook_name, {
this.contents.list_checkpoints(this.notebook_path, {
success: $.proxy(this.list_checkpoints_success, this),
error: function(error) {
that.events.trigger('list_checkpoints_failed.Notebook');
that.events.trigger('list_checkpoints_failed.Notebook', error);
}
});
};
@ -2362,10 +2356,10 @@ define([
*/
Notebook.prototype.create_checkpoint = function () {
var that = this;
this.contents.create_checkpoint(this.notebook_path, this.notebook_name, {
this.contents.create_checkpoint(this.notebook_path, {
success: $.proxy(this.create_checkpoint_success, this),
error: function (error) {
that.events.trigger('checkpoint_failed.Notebook');
that.events.trigger('checkpoint_failed.Notebook', error);
}
});
};
@ -2432,11 +2426,11 @@ define([
Notebook.prototype.restore_checkpoint = function (checkpoint) {
this.events.trigger('notebook_restoring.Notebook', checkpoint);
var that = this;
this.contents.restore_checkpoint(this.notebook_path, this.notebook_name,
this.contents.restore_checkpoint(this.notebook_path,
checkpoint, {
success: $.proxy(this.restore_checkpoint_success, this),
error: function (error) {
that.events.trigger('checkpoint_restore_failed.Notebook');
that.events.trigger('checkpoint_restore_failed.Notebook', error);
}
});
};
@ -2448,7 +2442,7 @@ define([
*/
Notebook.prototype.restore_checkpoint_success = function () {
this.events.trigger('checkpoint_restored.Notebook');
this.load_notebook(this.notebook_name, this.notebook_path);
this.load_notebook(this.notebook_path);
};
/**
@ -2460,7 +2454,7 @@ define([
Notebook.prototype.delete_checkpoint = function (checkpoint) {
this.events.trigger('notebook_restoring.Notebook', checkpoint);
var that = this;
this.contents.delete_checkpoint(this.notebook_path, this.notebook_name,
this.contents.delete_checkpoint(this.notebook_path,
checkpoint, {
success: $.proxy(this.delete_checkpoint_success, this),
error: function (error) {
@ -2476,7 +2470,7 @@ define([
*/
Notebook.prototype.delete_checkpoint_success = function () {
this.events.trigger('checkpoint_deleted.Notebook');
this.load_notebook(this.notebook_name, this.notebook_path);
this.load_notebook(this.notebook_path);
};

@ -122,14 +122,12 @@ define([
SaveWidget.prototype.update_address_bar = function(){
var base_url = this.notebook.base_url;
var nbname = this.notebook.notebook_name;
var path = this.notebook.notebook_path;
var state = {path : path, name: nbname};
var state = {path : path};
window.history.replaceState(state, "", utils.url_join_encode(
base_url,
"notebooks",
path,
nbname)
path)
);
};
@ -199,7 +197,7 @@ define([
$.proxy(that._regularly_update_checkpoint_date, that),
t + 1000
);
}
};
var tdelta = Math.ceil(new Date()-this._checkpoint_date);
// update regularly for the first 6hours and show

@ -29,8 +29,9 @@ define([
// An error representing the result of attempting to delete a non-empty
// directory.
this.message = 'A directory must be empty before being deleted.';
}
Contents.DirectoryNotEmptyError.prototype = new Error;
};
Contents.DirectoryNotEmptyError.prototype = Object.create(Error.prototype);
Contents.DirectoryNotEmptyError.prototype.name =
Contents.DIRECTORY_NOT_EMPTY_ERROR;
@ -54,29 +55,28 @@ define([
*/
Contents.prototype.create_basic_error_handler = function(callback) {
if (!callback) {
return function(xhr, status, error) { };
return utils.log_ajax_error;
}
return function(xhr, status, error) {
callback(utils.wrap_ajax_error(xhr, status, error));
};
}
};
/**
* File Functions (including notebook operations)
*/
/**
* Load a file.
* Get a file.
*
* Calls success with file JSON model, or error with error.
*
* @method load_notebook
* @method get
* @param {String} path
* @param {String} name
* @param {Function} success
* @param {Function} error
*/
Contents.prototype.load = function (path, name, options) {
Contents.prototype.get = function (path, options) {
// We do the call with settings so we can set cache to false.
var settings = {
processData : false,
@ -86,32 +86,29 @@ define([
success : options.success,
error : this.create_basic_error_handler(options.error)
};
var url = this.api_url(path, name);
var url = this.api_url(path);
$.ajax(url, settings);
};
/**
* Creates a new notebook file at the specified directory path.
* Creates a new untitled file or directory in the specified directory path.
*
* @method scroll_to_cell
* @param {String} path The path to create the new notebook at
* @param {String} name Name for new file. Chosen by server if unspecified.
* @method new
* @param {String} path: the directory in which to create the new file/directory
* @param {Object} options:
* ext: file extension to use if name unspecified
* ext: file extension to use
* type: model type to create ('notebook', 'file', or 'directory')
*/
Contents.prototype.new = function(path, name, options) {
var method, data;
if (name) {
method = "PUT";
} else {
method = "POST";
data = JSON.stringify({ext: options.ext || ".ipynb"});
}
Contents.prototype.new_untitled = function(path, options) {
var data = JSON.stringify({
ext: options.ext,
type: options.type
});
var settings = {
processData : false,
type : method,
type : "POST",
data: data,
dataType : "json",
success : options.success || function() {},
@ -123,9 +120,8 @@ define([
$.ajax(this.api_url(path), settings);
};
Contents.prototype.delete = function(name, path, options) {
Contents.prototype.delete = function(path, options) {
var error_callback = options.error || function() {};
var that = this;
var settings = {
processData : false,
type : "DELETE",
@ -140,12 +136,12 @@ define([
error_callback(utils.wrap_ajax_error(xhr, status, error));
}
};
var url = this.api_url(path, name);
var url = this.api_url(path);
$.ajax(url, settings);
};
Contents.prototype.rename = function(path, name, new_path, new_name, options) {
var data = {name: new_name, path: new_path};
Contents.prototype.rename = function(path, new_path, options) {
var data = {path: new_path};
var settings = {
processData : false,
type : "PATCH",
@ -155,11 +151,11 @@ define([
success : options.success || function() {},
error : this.create_basic_error_handler(options.error)
};
var url = this.api_url(path, name);
var url = this.api_url(path);
$.ajax(url, settings);
};
Contents.prototype.save = function(path, name, model, options) {
Contents.prototype.save = function(path, model, options) {
// We do the call with settings so we can set cache to false.
var settings = {
processData : false,
@ -172,24 +168,19 @@ define([
if (options.extra_settings) {
$.extend(settings, options.extra_settings);
}
var url = this.api_url(path, name);
var url = this.api_url(path);
$.ajax(url, settings);
};
Contents.prototype.copy = function(to_path, to_name, from, options) {
var url, method;
if (to_name) {
url = this.api_url(to_path, to_name);
method = "PUT";
} else {
url = this.api_url(to_path);
method = "POST";
}
Contents.prototype.copy = function(from_file, to_dir, options) {
// Copy a file into a given directory via POST
// The server will select the name of the copied file
var url = this.api_url(to_dir);
var settings = {
processData : false,
type: method,
data: JSON.stringify({copy_from: from}),
type: "POST",
data: JSON.stringify({copy_from: from_file}),
dataType : "json",
success: options.success || function() {},
error: this.create_basic_error_handler(options.error)
@ -204,8 +195,8 @@ define([
* Checkpointing Functions
*/
Contents.prototype.create_checkpoint = function(path, name, options) {
var url = this.api_url(path, name, 'checkpoints');
Contents.prototype.create_checkpoint = function(path, options) {
var url = this.api_url(path, 'checkpoints');
var settings = {
type : "POST",
success: options.success || function() {},
@ -214,8 +205,8 @@ define([
$.ajax(url, settings);
};
Contents.prototype.list_checkpoints = function(path, name, options) {
var url = this.api_url(path, name, 'checkpoints');
Contents.prototype.list_checkpoints = function(path, options) {
var url = this.api_url(path, 'checkpoints');
var settings = {
type : "GET",
success: options.success,
@ -224,8 +215,8 @@ define([
$.ajax(url, settings);
};
Contents.prototype.restore_checkpoint = function(path, name, checkpoint_id, options) {
var url = this.api_url(path, name, 'checkpoints', checkpoint_id);
Contents.prototype.restore_checkpoint = function(path, checkpoint_id, options) {
var url = this.api_url(path, 'checkpoints', checkpoint_id);
var settings = {
type : "POST",
success: options.success || function() {},
@ -234,8 +225,8 @@ define([
$.ajax(url, settings);
};
Contents.prototype.delete_checkpoint = function(path, name, checkpoint_id, options) {
var url = this.api_url(path, name, 'checkpoints', checkpoint_id);
Contents.prototype.delete_checkpoint = function(path, checkpoint_id, options) {
var url = this.api_url(path, 'checkpoints', checkpoint_id);
var settings = {
type : "DELETE",
success: options.success || function() {},
@ -255,10 +246,8 @@ define([
* representing individual files or directories. Each dictionary has
* the keys:
* type: "notebook" or "directory"
* name: the name of the file or directory
* created: created date
* last_modified: last modified dat
* path: the path
* @method list_notebooks
* @param {String} path The path to list notebooks in
* @param {Function} load_callback called with list of notebooks on success

@ -15,7 +15,6 @@ define([
* all other operations, the kernel object should be used.
*
* Options should include:
* - notebook_name: the notebook name
* - notebook_path: the path (not including name) to the notebook
* - kernel_name: the type of kernel (e.g. python3)
* - base_url: the root url of the notebook server
@ -28,7 +27,6 @@ define([
var Session = function (options) {
this.id = null;
this.notebook_model = {
name: options.notebook_name,
path: options.notebook_path
};
this.kernel_model = {
@ -154,15 +152,11 @@ define([
* undefined, then they will not be changed.
*
* @function rename_notebook
* @param {string} [name] - new notebook name
* @param {string} [path] - new path to notebook
* @param {string} [path] - new notebook path
* @param {function} [success] - function executed on ajax success
* @param {function} [error] - functon executed on ajax error
*/
Session.prototype.rename_notebook = function (name, path, success, error) {
if (name !== undefined) {
this.notebook_model.name = name;
}
Session.prototype.rename_notebook = function (path, success, error) {
if (path !== undefined) {
this.notebook_model.path = path;
}
@ -208,7 +202,6 @@ define([
* fresh. If options are given, they can include any of the
* following:
*
* - notebook_name - the name of the notebook
* - notebook_path - the path to the notebook
* - kernel_name - the name (type) of the kernel
*
@ -220,9 +213,6 @@ define([
Session.prototype.restart = function (options, success, error) {
var that = this;
var start = function () {
if (options && options.notebook_name) {
that.notebook_model.name = options.notebook_name;
}
if (options && options.notebook_path) {
that.notebook_model.path = options.notebook_path;
}
@ -238,8 +228,8 @@ define([
// Helper functions
/**
* Get the data model for the session, which includes the notebook
* (name and path) and kernel (name and id).
* Get the data model for the session, which includes the notebook path
* and kernel (name and id).
*
* @function _get_model
* @returns {Object} - the data model
@ -266,7 +256,6 @@ define([
this.session_url = utils.url_join_encode(this.session_service_url, this.id);
}
if (data && data.notebook) {
this.notebook_model.name = data.notebook.name;
this.notebook_model.path = data.notebook.path;
}
if (data && data.kernel) {

@ -2,8 +2,9 @@
// Distributed under the terms of the Modified BSD License.
require([
'base/js/namespace',
'jquery',
'base/js/namespace',
'base/js/dialog',
'base/js/events',
'base/js/page',
'base/js/utils',
@ -19,18 +20,20 @@ require([
'bootstrap',
'custom/custom',
], function(
IPython,
$,
$,
IPython,
dialog,
events,
page,
utils,
contents,
page,
utils,
contents_service,
notebooklist,
clusterlist,
sesssionlist,
kernellist,
terminallist,
loginwidget){
"use strict";
page = new page.Page();
@ -38,36 +41,37 @@ require([
base_url: utils.get_body_data("baseUrl"),
notebook_path: utils.get_body_data("notebookPath"),
};
session_list = new sesssionlist.SesssionList($.extend({
var session_list = new sesssionlist.SesssionList($.extend({
events: events},
common_options));
contents = new contents.Contents($.extend({
var contents = new contents_service.Contents($.extend({
events: events},
common_options));
notebook_list = new notebooklist.NotebookList('#notebook_list', $.extend({
var notebook_list = new notebooklist.NotebookList('#notebook_list', $.extend({
contents: contents,
session_list: session_list},
common_options));
cluster_list = new clusterlist.ClusterList('#cluster_list', common_options);
kernel_list = new kernellist.KernelList('#running_list', $.extend({
var cluster_list = new clusterlist.ClusterList('#cluster_list', common_options);
var kernel_list = new kernellist.KernelList('#running_list', $.extend({
session_list: session_list},
common_options));
var terminal_list;
if (utils.get_body_data("terminalsAvailable") === "True") {
terminal_list = new terminallist.TerminalList('#terminal_list', common_options);
}
login_widget = new loginwidget.LoginWidget('#login_widget', common_options);
var login_widget = new loginwidget.LoginWidget('#login_widget', common_options);
$('#new_notebook').click(function (e) {
contents.new(common_options.notebook_path, null, {
ext: ".ipynb",
contents.new_untitled(common_options.notebook_path, {
type: "notebook",
extra_settings: {async: false}, // So we can open a new window afterwards
success: function (data) {
window.open(
utils.url_join_encode(
common_options.base_url, 'notebooks',
data.path, data.name
data.path
), '_blank');
},
error: function(error) {

@ -100,7 +100,7 @@ define([
};
reader.onerror = function (event) {
var item = $(event.target).data('item');
var name = item.data('name')
var name = item.data('name');
item.remove();
dialog.modal({
title : 'Failed to read file',
@ -141,7 +141,7 @@ define([
};
NotebookList.prototype.load_list = function () {
var that = this
var that = this;
this.contents.list_contents(that.notebook_path, {
success: $.proxy(this.draw_notebook_list, this),
error: function(error) {
@ -177,7 +177,7 @@ define([
model = {
type: 'directory',
name: '..',
path: path,
path: utils.url_path_split(path)[0],
};
this.add_link(model, item);
offset += 1;
@ -240,8 +240,7 @@ define([
utils.url_join_encode(
this.base_url,
uri_prefix,
path,
name
path
)
);
// directory nav doesn't open new tabs
@ -311,7 +310,6 @@ define([
};
NotebookList.prototype.add_delete_button = function (item) {
var new_buttons = $('<span/>').addClass("btn-group pull-right");
var notebooklist = this;
var delete_button = $("<button/>").text("Delete").addClass("btn btn-default btn-xs").
click(function (e) {
@ -322,7 +320,7 @@ define([
var parent_item = that.parents('div.list_item');
var name = parent_item.data('nbname');
var path = parent_item.data('path');
var message = 'Are you sure you want to permanently delete the file: ' + nbname + '?';
var message = 'Are you sure you want to permanently delete the file: ' + name + '?';
dialog.modal({
title : "Delete file",
body : message,
@ -330,9 +328,9 @@ define([
Delete : {
class: "btn-danger",
click: function() {
notebooklist.contents.delete(name, path, {
notebooklist.contents.delete(path, {
success: function() {
notebooklist.notebook_deleted(path, name);
notebooklist.notebook_deleted(path);
}
});
}
@ -345,25 +343,24 @@ define([
item.find(".item_buttons").text("").append(delete_button);
};
NotebookList.prototype.notebook_deleted = function(path, name) {
NotebookList.prototype.notebook_deleted = function(path) {
// Remove the deleted notebook.
$( ":data(nbname)" ).each(function() {
var element = $( this );
if (element.data( "nbname" ) == d.name &&
element.data( "path" ) == d.path) {
var element = $(this);
if (element.data("path") == path) {
element.remove();
}
});
}
};
NotebookList.prototype.add_upload_button = function (item, type) {
NotebookList.prototype.add_upload_button = function (item) {
var that = this;
var upload_button = $('<button/>').text("Upload")
.addClass('btn btn-primary btn-xs upload_button')
.click(function (e) {
var path = that.notebook_path;
var filename = item.find('.item_name > input').val();
var path = utils.url_path_join(that.notebook_path, filename);
var filedata = item.data('filedata');
var format = 'text';
if (filename.length === 0 || filename[0] === '.') {
@ -385,10 +382,7 @@ define([
filedata = btoa(bytes);
format = 'base64';
}
var model = {
path: path,
name: filename
};
var model = {};
var name_and_ext = utils.splitext(filename);
var file_ext = name_and_ext[1];
@ -418,34 +412,22 @@ define([
model.content = filedata;
content_type = 'application/octet-stream';
}
var filedata = item.data('filedata');
filedata = item.data('filedata');
var settings = {
processData : false,
cache : false,
type : 'PUT',
data : JSON.stringify(model),
contentType: content_type,
success : function (data, status, xhr) {
success : function () {
item.removeClass('new-file');
that.add_link(model, item);
that.add_delete_button(item);
that.session_list.load_sessions();
},
error : utils.log_ajax_error,
};
var url = utils.url_join_encode(
that.base_url,
'api/contents',
that.notebook_path,
filename
);
var exists = false;
$.each(that.element.find('.list_item:not(.new-file)'), function(k,v){
if ($(v).data('name') === filename) { exists = true; return false; }
});
if (exists) {
dialog.modal({
title : "Replace file",
@ -453,7 +435,9 @@ define([
buttons : {
Overwrite : {
class: "btn-danger",
click: function() { $.ajax(url, settings); }
click: function () {
that.contents.save(path, model, settings);
}
},
Cancel : {
click: function() { item.remove(); }
@ -461,7 +445,7 @@ define([
}
});
} else {
$.ajax(url, settings);
that.contents.save(path, model, settings);
}
return false;
@ -478,7 +462,7 @@ define([
};
// Backwards compatability.
// Backwards compatability.
IPython.NotebookList = NotebookList;
return {'NotebookList': NotebookList};

@ -12,14 +12,14 @@ casper.notebook_test(function () {
this.thenEvaluate(function (nbname) {
require(['base/js/events'], function (events) {
IPython.notebook.notebook_name = nbname;
IPython.notebook.set_notebook_name(nbname);
IPython._save_success = IPython._save_failed = false;
events.on('notebook_saved.Notebook', function () {
IPython._save_success = true;
});
events.on('notebook_save_failed.Notebook',
function (event, xhr, status, error) {
IPython._save_failed = "save failed with " + xhr.status + xhr.responseText;
function (event, error) {
IPython._save_failed = "save failed with " + error;
});
IPython.notebook.save_notebook();
});
@ -42,6 +42,10 @@ casper.notebook_test(function () {
return IPython.notebook.notebook_name;
});
this.test.assertEquals(current_name, nbname, "Save with complicated name");
var current_path = this.evaluate(function(){
return IPython.notebook.notebook_path;
});
this.test.assertEquals(current_path, nbname, "path OK");
});
this.thenEvaluate(function(){
@ -68,11 +72,8 @@ casper.notebook_test(function () {
});
this.then(function(){
var baseUrl = this.get_notebook_server();
this.open(baseUrl);
this.open_dashboard();
});
this.waitForSelector('.list_item');
this.then(function(){
var notebook_url = this.evaluate(function(nbname){
@ -92,11 +93,11 @@ casper.notebook_test(function () {
});
// wait for the notebook
this.waitForSelector("#notebook");
this.waitFor(this.kernel_running);
this.waitFor(function(){
return this.evaluate(function(){
return IPython.notebook || false;
this.waitFor(function() {
return this.evaluate(function () {
return IPython && IPython.notebook && true;
});
});

@ -75,22 +75,22 @@ class TestInstallNBExtension(TestCase):
td.cleanup()
nbextensions.get_ipython_dir = self.save_get_ipython_dir
def assert_path_exists(self, path):
def assert_dir_exists(self, path):
if not os.path.exists(path):
do_exist = os.listdir(os.path.dirname(path))
self.fail(u"%s should exist (found %s)" % (path, do_exist))
def assert_not_path_exists(self, path):
def assert_not_dir_exists(self, path):
if os.path.exists(path):
self.fail(u"%s should not exist" % path)
def assert_installed(self, relative_path, ipdir=None):
self.assert_path_exists(
self.assert_dir_exists(
pjoin(ipdir or self.ipdir, u'nbextensions', relative_path)
)
def assert_not_installed(self, relative_path, ipdir=None):
self.assert_not_path_exists(
self.assert_not_dir_exists(
pjoin(ipdir or self.ipdir, u'nbextensions', relative_path)
)
@ -99,7 +99,7 @@ class TestInstallNBExtension(TestCase):
with TemporaryDirectory() as td:
ipdir = pjoin(td, u'ipython')
install_nbextension(self.src, ipython_dir=ipdir)
self.assert_path_exists(ipdir)
self.assert_dir_exists(ipdir)
for file in self.files:
self.assert_installed(
pjoin(basename(self.src), file),

@ -0,0 +1,61 @@
import re
import nose.tools as nt
from IPython.html.base.handlers import path_regex, notebook_path_regex
try: # py3
assert_regex = nt.assert_regex
assert_not_regex = nt.assert_not_regex
except AttributeError: # py2
assert_regex = nt.assert_regexp_matches
assert_not_regex = nt.assert_not_regexp_matches
# build regexps that tornado uses:
path_pat = re.compile('^' + '/x%s' % path_regex + '$')
nb_path_pat = re.compile('^' + '/y%s' % notebook_path_regex + '$')
def test_path_regex():
for path in (
'/x',
'/x/',
'/x/foo',
'/x/foo.ipynb',
'/x/foo/bar',
'/x/foo/bar.txt',
):
assert_regex(path, path_pat)
def test_path_regex_bad():
for path in (
'/xfoo',
'/xfoo/',
'/xfoo/bar',
'/xfoo/bar/',
'/x/foo/bar/',
'/x//foo',
'/y',
'/y/x/foo',
):
assert_not_regex(path, path_pat)
def test_notebook_path_regex():
for path in (
'/y/asdf.ipynb',
'/y/foo/bar.ipynb',
'/y/a/b/c/d/e.ipynb',
):
assert_regex(path, nb_path_pat)
def test_notebook_path_regex_bad():
for path in (
'/y',
'/y/',
'/y/.ipynb',
'/y/foo/.ipynb',
'/y/foo/bar',
'/yfoo.ipynb',
'/yfoo/bar.ipynb',
):
assert_not_regex(path, nb_path_pat)

@ -11,13 +11,15 @@ casper.get_list_items = function () {
});
};
casper.test_items = function (baseUrl) {
casper.test_items = function (baseUrl, visited) {
visited = visited || {};
casper.then(function () {
var items = casper.get_list_items();
casper.each(items, function (self, item) {
if (!item.label.match(/\.ipynb$/)) {
if (item.link.match(/^\/tree\//)) {
var followed_url = baseUrl+item.link;
if (!followed_url.match(/\/\.\.$/)) {
if (!visited[followed_url]) {
visited[followed_url] = true;
casper.thenOpen(followed_url, function () {
this.waitFor(this.page_loaded);
casper.wait_for_dashboard();
@ -25,7 +27,7 @@ casper.test_items = function (baseUrl) {
// but item.link is without host, and url-encoded
var expected = baseUrl + decodeURIComponent(item.link);
this.test.assertEquals(this.getCurrentUrl(), expected, 'Testing dashboard link: ' + expected);
casper.test_items(baseUrl);
casper.test_items(baseUrl, visited);
this.back();
});
}

@ -4,7 +4,7 @@
# Distributed under the terms of the Modified BSD License.
from tornado import web
from ..base.handlers import IPythonHandler, notebook_path_regex, path_regex
from ..base.handlers import IPythonHandler, path_regex
from ..utils import url_path_join, url_escape
@ -33,18 +33,21 @@ class TreeHandler(IPythonHandler):
return 'Home'
@web.authenticated
def get(self, path='', name=None):
def get(self, path=''):
path = path.strip('/')
cm = self.contents_manager
if name is not None:
# is a notebook, redirect to notebook handler
if cm.file_exists(path):
# it's not a directory, we have redirecting to do
model = cm.get(path, content=False)
# redirect to /api/notebooks if it's a notebook, otherwise /api/files
service = 'notebooks' if model['type'] == 'notebook' else 'files'
url = url_escape(url_path_join(
self.base_url, 'notebooks', path, name
self.base_url, service, path,
))
self.log.debug("Redirecting %s to %s", self.request.path, url)
self.redirect(url)
else:
if not cm.path_exists(path=path):
if not cm.dir_exists(path=path):
# Directory is hidden or does not exist.
raise web.HTTPError(404)
elif cm.is_hidden(path):
@ -66,7 +69,6 @@ class TreeHandler(IPythonHandler):
default_handlers = [
(r"/tree%s" % notebook_path_regex, TreeHandler),
(r"/tree%s" % path_regex, TreeHandler),
(r"/tree", TreeHandler),
]

Loading…
Cancel
Save