"""Basic html-rendering handlers."""

# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.

from tornado import web, gen

from .. import orm
from ..utils import admin_only, url_path_join
from .base import BaseHandler
from .login import LoginHandler


class RootHandler(BaseHandler):
    """Render the Hub root page.
    
    If logged in, redirects to:
    
    - single-user server if running
    - hub home, otherwise
    
    Otherwise, renders login page.
    """
    def get(self):
        user = self.get_current_user()
        if user:
            if user.running:
                url = user.server.base_url
                self.log.debug("User is running: %s", url)
            else:
                url = url_path_join(self.hub.server.base_url, 'home')
                self.log.debug("User is not running: %s", url)
            self.redirect(url)
            return
        url = url_path_join(self.hub.server.base_url, 'login')
        self.redirect(url)


class HomeHandler(BaseHandler):
    """Render the user's home page."""

    @web.authenticated
    def get(self):
        html = self.render_template('home.html',
            user=self.get_current_user(),
        )
        self.finish(html)


class SpawnHandler(BaseHandler):
    """Handle spawning of single-user servers via form.
    
    GET renders the form, POST handles form submission.
    
    Only enabled when Spawner.options_form is defined.
    """
    @web.authenticated
    def get(self):
        """GET renders form for spawning with user-specified options"""
        user = self.get_current_user()
        if user.running:
            url = user.server.base_url
            self.log.debug("User is running: %s", url)
            self.redirect(url)
            return
        if user.spawner.options_form:
            html = self.render_template('spawn.html',
                user=self.get_current_user(),
                spawner_options_form=user.spawner.options_form,
            )
            self.finish(html)
        else:
            # not running, no form. Trigger spawn.
            url = url_path_join(self.base_url, 'users', user.name)
            self.redirect(url)
    
    @web.authenticated
    @gen.coroutine
    def post(self):
        """POST spawns with user-specified options"""
        user = self.get_current_user()
        if user.running:
            url = user.server.base_url
            self.log.warning("User is already running: %s", url)
            self.redirect(url)
            return
        form_options = {}
        for key, byte_list in self.request.body_arguments.items():
            form_options[key] = [ bs.decode('utf8') for bs in byte_list ]
        options = user.spawner.options_from_form(form_options)
        yield self.spawn_single_user(user, options=options)
        self.set_login_cookie(user)
        url = user.server.base_url
        self.redirect(url)

class AdminHandler(BaseHandler):
    """Render the admin page."""

    @admin_only
    def get(self):
        available = {'name', 'admin', 'running', 'last_activity'}
        default_sort = ['admin', 'name']
        mapping = {
            'running': '_server_id'
        }
        default_order = {
            'name': 'asc',
            'last_activity': 'desc',
            'admin': 'desc',
            'running': 'desc',
        }
        sorts = self.get_arguments('sort') or default_sort
        orders = self.get_arguments('order')
        
        for bad in set(sorts).difference(available):
            self.log.warn("ignoring invalid sort: %r", bad)
            sorts.remove(bad)
        for bad in set(orders).difference({'asc', 'desc'}):
            self.log.warn("ignoring invalid order: %r", bad)
            orders.remove(bad)
        
        # add default sort as secondary
        for s in default_sort:
            if s not in sorts:
                sorts.append(s)
        if len(orders) < len(sorts):
            for col in sorts[len(orders):]:
                orders.append(default_order[col])
        else:
            orders = orders[:len(sorts)]
        
        # this could be one incomprehensible nested list comprehension
        # get User columns
        cols = [ getattr(orm.User, mapping.get(c, c)) for c in sorts ]
        # get User.col.desc() order objects
        ordered = [ getattr(c, o)() for c, o in zip(cols, orders) ]
        
        users = self.db.query(orm.User).order_by(*ordered)
        users = [ self._user_from_orm(u) for u in users ]
        running = [ u for u in users if u.running ]
        
        html = self.render_template('admin.html',
            user=self.get_current_user(),
            admin_access=self.settings.get('admin_access', False),
            users=users,
            running=running,
            sort={s:o for s,o in zip(sorts, orders)},
        )
        self.finish(html)


default_handlers = [
    (r'/', RootHandler),
    (r'/home', HomeHandler),
    (r'/admin', AdminHandler),
    (r'/spawn', SpawnHandler),
]

jupyterhub/handlers/pages.py
"""Basic html-rendering handlers."""

# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.

from tornado import web, gen

from .. import orm
from ..utils import admin_only, url_path_join
from .base import BaseHandler
from .login import LoginHandler


class RootHandler(BaseHandler):
    """Render the Hub root page.
    
    If logged in, redirects to:
    
    - single-user server if running
    - hub home, otherwise
    
    Otherwise, renders login page.
    """
    def get(self):
        user = self.get_current_user()
        if user:
            if user.running:
                url = user.server.base_url
                self.log.debug("User is running: %s", url)
            else:
                url = url_path_join(self.hub.server.base_url, 'home')
                self.log.debug("User is not running: %s", url)
            self.redirect(url)
            return
        url = url_path_join(self.hub.server.base_url, 'login')
        self.redirect(url)


class HomeHandler(BaseHandler):
    """Render the user's home page."""

    @web.authenticated
    def get(self):
        html = self.render_template('home.html',
            user=self.get_current_user(),
        )
        self.finish(html)


class SpawnHandler(BaseHandler):
    """Handle spawning of single-user servers via form.
    
    GET renders the form, POST handles form submission.
    
    Only enabled when Spawner.options_form is defined.
    """
    @web.authenticated
    def get(self):
        """GET renders form for spawning with user-specified options"""
        user = self.get_current_user()
        if user.running:
            url = user.server.base_url
            self.log.debug("User is running: %s", url)
            self.redirect(url)
            return
        if user.spawner.options_form:
            html = self.render_template('spawn.html',
                user=self.get_current_user(),
                spawner_options_form=user.spawner.options_form,
            )
            self.finish(html)
        else:
            # not running, no form. Trigger spawn.
            url = url_path_join(self.base_url, 'user', user.name)
            self.redirect(url)
    
    @web.authenticated
    @gen.coroutine
    def post(self):
        """POST spawns with user-specified options"""
        user = self.get_current_user()
        if user.running:
            url = user.server.base_url
            self.log.warning("User is already running: %s", url)
            self.redirect(url)
            return
        form_options = {}
        for key, byte_list in self.request.body_arguments.items():
            form_options[key] = [ bs.decode('utf8') for bs in byte_list ]
        options = user.spawner.options_from_form(form_options)
        yield self.spawn_single_user(user, options=options)
        self.set_login_cookie(user)
        url = user.server.base_url
        self.redirect(url)

class AdminHandler(BaseHandler):
    """Render the admin page."""

    @admin_only
    def get(self):
        available = {'name', 'admin', 'running', 'last_activity'}
        default_sort = ['admin', 'name']
        mapping = {
            'running': '_server_id'
        }
        default_order = {
            'name': 'asc',
            'last_activity': 'desc',
            'admin': 'desc',
            'running': 'desc',
        }
        sorts = self.get_arguments('sort') or default_sort
        orders = self.get_arguments('order')
        
        for bad in set(sorts).difference(available):
            self.log.warn("ignoring invalid sort: %r", bad)
            sorts.remove(bad)
        for bad in set(orders).difference({'asc', 'desc'}):
            self.log.warn("ignoring invalid order: %r", bad)
            orders.remove(bad)
        
        # add default sort as secondary
        for s in default_sort:
            if s not in sorts:
                sorts.append(s)
        if len(orders) < len(sorts):
            for col in sorts[len(orders):]:
                orders.append(default_order[col])
        else:
            orders = orders[:len(sorts)]
        
        # this could be one incomprehensible nested list comprehension
        # get User columns
        cols = [ getattr(orm.User, mapping.get(c, c)) for c in sorts ]
        # get User.col.desc() order objects
        ordered = [ getattr(c, o)() for c, o in zip(cols, orders) ]
        
        users = self.db.query(orm.User).order_by(*ordered)
        users = [ self._user_from_orm(u) for u in users ]
        running = [ u for u in users if u.running ]
        
        html = self.render_template('admin.html',
            user=self.get_current_user(),
            admin_access=self.settings.get('admin_access', False),
            users=users,
            running=running,
            sort={s:o for s,o in zip(sorts, orders)},
        )
        self.finish(html)


default_handlers = [
    (r'/', RootHandler),
    (r'/home', HomeHandler),
    (r'/admin', AdminHandler),
    (r'/spawn', SpawnHandler),
]

jupyterhub/handlers/pages.py
diff --git a/jupyterhub/handlers/pages.py b/jupyterhub/handlers/pages.py
index 83b46e6003..4d6aeb0600 100644
--- a/jupyterhub/handlers/pages.py
+++ b/jupyterhub/handlers/pages.py
@@ -71,7 +71,7 @@ def get(self):
             self.finish(html)
         else:
             # not running, no form. Trigger spawn.
-            url = url_path_join(self.base_url, 'users', user.name)
+            url = url_path_join(self.base_url, 'user', user.name)
             self.redirect(url)
     
     @web.authenticated
diff --git a/jupyterhub/tests/test_pages.py b/jupyterhub/tests/test_pages.py
index ff4566b827..9ea0807532 100644
--- a/jupyterhub/tests/test_pages.py
+++ b/jupyterhub/tests/test_pages.py
@@ -1,5 +1,7 @@
 """Tests for HTML pages"""
 
+from urllib.parse import urlparse
+
 import requests
 
 from ..utils import url_path_join as ujoin
@@ -7,6 +9,7 @@
 
 import mock
 from .mocking import FormSpawner
+from .test_api import api_request
 
 
 def get_page(path, app, **kw):
@@ -59,10 +62,34 @@ def test_admin(app):
     r.raise_for_status()
     assert r.url.endswith('/admin')
 
-def test_spawn_redirect(app):
-    cookies = app.login_user('wash')
+def test_spawn_redirect(app, io_loop):
+    name = 'wash'
+    cookies = app.login_user(name)
+    u = app.users[orm.User.find(app.db, name)]
+    
+    # ensure wash's server isn't running:
+    r = api_request(app, 'users', name, 'server', method='delete', cookies=cookies)
+    r.raise_for_status()
+    status = io_loop.run_sync(u.spawner.poll)
+    assert status is not None
+    
+    # test spawn page when no server is running
     r = get_page('spawn', app, cookies=cookies)
-    assert r.url.endswith('/wash')
+    r.raise_for_status()
+    print(urlparse(r.url))
+    path = urlparse(r.url).path
+    assert path == '/user/%s' % name
+    
+    # should have started server
+    status = io_loop.run_sync(u.spawner.poll)
+    assert status is None
+    
+    # test spawn page when server is already running (just redirect)
+    r = get_page('spawn', app, cookies=cookies)
+    r.raise_for_status()
+    print(urlparse(r.url))
+    path = urlparse(r.url).path
+    assert path == '/user/%s' % name
 
 def test_spawn_page(app):
     with mock.patch.dict(app.users.settings, {'spawner_class': FormSpawner}):

404 on single user notebook server anytime I restart jupyterhub
I just deployed jupyterhub from scratch using my ansible scripts this morning. The script are unchanged from 2-3 weeks ago when I did it previously and it was all working. I am running from latest master of jupyterhub.

Anytime I restart jupyterhub (hub+proxy) I see the following behavior:
- I can log in (using GitHub OAuth)
- Trying to start my server gives me a 404 on `/hub/users/ellisonbg`. I can tell from the server log that the single user server isn't being started.
- I can go to the Admin page and start my server from there.
- But then, I get a redirect loop when I try to go to my single user server.
- If I clear all my cookies while my single user server (started through the Admin page) is still running, it starts to work as expected. I can start and stop the single user server on the control panel page just fine.
- If a restart jupyterhub, the problems start all over again.

I am using a fixed proxy_auth_token and cookie secret.

