Repository: jupyterhub/jupyterhub

Issue:
Can't share links to notebooks
I use the Docker spawner and have the following volume configuration:

```
c.DockerSpawner.volumes = {'/notebooks/Users/{username}': '/notebooks/Home'}
c.DockerSpawner.read_only_volumes = {'/notebooks/Users': '/notebooks/Users'}
```

I can create a notebook at `http://myserver/user/akaihola/notebooks/Users/akaihola/example.ipynb` and view it as another user at `http://myserver/user/friend/notebooks/Users/akaihola/example.ipynb`

But, if I send the link `http://myserver/user/akaihola/notebooks/Users/akaihola/example.ipynb` to a friend, he can't just log in to the server and click on the link since `/user/akaihola/` doesn't match his username.

It would be convenient if JupyterHub would redirect from `/user/akaihola/*` to `/user/friend/*` automatically when the username in the URL doesn't match the logged in user.



Discussion:
For sharing notebooks between users, I am trying out an `/examples` server extension, [nbexamples](https://github.com/danielballan/nbexamples). Maybe it will be of use to you.


Resolving patch:
diff --git a/jupyterhub/handlers/base.py b/jupyterhub/handlers/base.py
--- a/jupyterhub/handlers/base.py
+++ b/jupyterhub/handlers/base.py
@@ -454,22 +454,26 @@ def get(self):
 
 
 class UserSpawnHandler(BaseHandler):
-    """Requests to /user/name handled by the Hub
-    should result in spawning the single-user server and
-    being redirected to the original.
+    """Redirect requests to /user/name/* handled by the Hub.
+
+    If logged in, spawn a single-user server and redirect request.
+    If a user, alice, requests /user/bob/notebooks/mynotebook.ipynb,
+    redirect her to /user/alice/notebooks/mynotebook.ipynb, which should
+    in turn call this function.
     """
+
     @gen.coroutine
-    def get(self, name):
+    def get(self, name, user_path):
         current_user = self.get_current_user()
         if current_user and current_user.name == name:
-            # logged in, spawn the server
+            # logged in as correct user, spawn the server
             if current_user.spawner:
                 if current_user.spawn_pending:
                     # spawn has started, but not finished
                     html = self.render_template("spawn_pending.html", user=current_user)
                     self.finish(html)
                     return
-                
+
                 # spawn has supposedly finished, check on the status
                 status = yield current_user.spawner.poll()
                 if status is not None:
@@ -485,14 +489,19 @@ def get(self, name):
             if self.use_subdomains:
                 target = current_user.host + target
             self.redirect(target)
+        elif current_user:
+            # logged in as a different user, redirect
+            target = url_path_join(self.base_url, 'user', current_user.name,
+                                   user_path or '')
+            self.redirect(target)
         else:
-            # not logged in to the right user,
-            # clear any cookies and reload (will redirect to login)
+            # not logged in, clear any cookies and reload
             self.clear_login_cookie()
             self.redirect(url_concat(
                 self.settings['login_url'],
-                {'next': self.request.uri,
-            }))
+                {'next': self.request.uri},
+            ))
+
 
 class CSPReportHandler(BaseHandler):
     '''Accepts a content security policy violation report'''
@@ -503,6 +512,6 @@ def post(self):
                       self.request.body.decode('utf8', 'replace'))
 
 default_handlers = [
-    (r'/user/([^/]+)/?.*', UserSpawnHandler),
+    (r'/user/([^/]+)(/.*)?', UserSpawnHandler),
     (r'/security/csp-report', CSPReportHandler),
 ]


Test patch:
diff --git a/jupyterhub/tests/test_pages.py b/jupyterhub/tests/test_pages.py
--- a/jupyterhub/tests/test_pages.py
+++ b/jupyterhub/tests/test_pages.py
@@ -1,6 +1,6 @@
 """Tests for HTML pages"""
 
-from urllib.parse import urlparse
+from urllib.parse import urlencode, urlparse
 
 import requests
 
@@ -149,6 +149,31 @@ def test_spawn_form_with_file(app, io_loop):
         }
 
 
+def test_user_redirect(app):
+    name = 'wash'
+    cookies = app.login_user(name)
+
+    r = get_page('/user/baduser', app, cookies=cookies)
+    r.raise_for_status()
+    print(urlparse(r.url))
+    path = urlparse(r.url).path
+    assert path == '/user/%s' % name
+
+    r = get_page('/user/baduser/test.ipynb', app, cookies=cookies)
+    r.raise_for_status()
+    print(urlparse(r.url))
+    path = urlparse(r.url).path
+    assert path == '/user/%s/test.ipynb' % name
+
+    r = get_page('/user/baduser/test.ipynb', app)
+    r.raise_for_status()
+    print(urlparse(r.url))
+    path = urlparse(r.url).path
+    assert path == '/hub/login'
+    query = urlparse(r.url).query
+    assert query == urlencode({'next': '/hub/user/baduser/test.ipynb'})
+
+
 def test_static_files(app):
     base_url = ujoin(public_url(app), app.hub.server.base_url)
     print(base_url)