Student courses not appearing
When using the "multiple courses" setup with JupyterHub authentication, it does not seem that students can actually view assignments in the courses they are in.
diff --git a/nbgrader/auth/jupyterhub.py b/nbgrader/auth/jupyterhub.py
index a1164047..81db061f 100644
--- a/nbgrader/auth/jupyterhub.py
+++ b/nbgrader/auth/jupyterhub.py
@@ -99,6 +99,12 @@ class JupyterHubAuthPlugin(BaseAuthPlugin):
         return list(courses)
 
     def add_student_to_course(self, student_id, course_id):
+        if not course_id:
+            self.log.error(
+                "Could not add student to course because the course_id has not "
+                "been provided. Has it been set in the nbgrader_config.py?")
+            return
+
         try:
             group_name = "nbgrader-{}".format(course_id)
             jup_groups = _query_jupyterhub_api(
@@ -138,6 +144,12 @@ class JupyterHubAuthPlugin(BaseAuthPlugin):
             self.log.error("Make sure you set a valid admin_user 'api_token' in your config file before starting the service")
 
     def remove_student_from_course(self, student_id, course_id):
+        if not course_id:
+            self.log.error(
+                "Could not remove student from course because the course_id has "
+                "not been provided. Has it been set in the nbgrader_config.py?")
+            return
+
         try:
             group_name = "nbgrader-{}".format(course_id)
             _query_jupyterhub_api(
diff --git a/nbgrader/docs/source/configuration/jupyterhub_config.rst b/nbgrader/docs/source/configuration/jupyterhub_config.rst
index ff91de7d..8ffc1765 100644
--- a/nbgrader/docs/source/configuration/jupyterhub_config.rst
+++ b/nbgrader/docs/source/configuration/jupyterhub_config.rst
@@ -31,23 +31,13 @@ shared server setup like JupyterHub, the formgrader (see
 :doc:`/user_guide/creating_and_grading_assignments`) can be configured to
 integrate with JupyterHub so that all grading can occur on the same server.
 
-.. warning::
-
-    The way that the formgrader integrates with JupyterHub changed between
-    versions 0.4 and 0.5 in a backwards-incompatible way. However, this means
-    that the formgrader should be much easier to use with JupyterHub!
-
-.. warning::
-
-    Starting in version 0.5.0 of nbgrader, the formgrader is no longer a
-    standalone command. Rather, it is an extension of the Jupyter notebook.
-
 .. warning::
 
     When using nbgrader with JupyterHub, it is strongly recommended to set a
     logfile so that you can more easily debug problems. To do so, you can set
     a config option, for example ``NbGrader.logfile = "/usr/local/share/jupyter/nbgrader.log"``.
 
+Each of these use cases also has a corresponding demo in the `demos folder <https://github.com/jupyter/nbgrader/tree/master/demos>`_ of the GitHub repository.
 
 Example Use Case: One Class, One Grader
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -138,7 +128,8 @@ Your JupyterHub should look something like this:
     }
 
     # Start the notebook server as a service. The port can be whatever you want
-    # and the group has to match the name of the group defined above.
+    # and the group has to match the name of the group defined above. The name
+    # of the service MUST match the name of your course.
     c.JupyterHub.services = [
         {
             'name': 'course101',
@@ -160,6 +151,16 @@ Similarly to the use case with just a single grader, there needs to then be a ``
     c = get_config()
     c.CourseDirectory.root = '/home/grader-course101/course101'
 
+You will additionally need to add a global nbgrader config file (for example,
+in ``/etc/jupyter/nbgrader_config.py``) which specifies the course id:
+
+.. code:: python
+
+    c = get_config()
+    c.CourseDirectory.course_id = 'course101'
+
+This course id MUST match the name of the service that is running the formgrader.
+
 
 Example Use Case: Multiple Classes
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -179,82 +180,6 @@ combination of instructors then have access to:
 
 .. image:: images/multiple_classes.png
 
-To configure such a setup, your JupyterHub config should look something like this:
-
-.. code:: python
-
-    c = get_config()
-
-    # Our user list
-    c.Authenticator.whitelist = [
-        'instructor1',
-        'instructor2',
-        'student1',
-    ]
-
-    # instructor1 and instructor2 have access to different shared servers:
-    c.JupyterHub.load_groups = {
-        'formgrader-course101': [
-            'instructor1'
-        ],
-        'formgrader-course123': [
-            'instructor2'
-        ]
-    }
-
-    # Start the notebook server as a service. The port can be whatever you want
-    # and the group has to match the name of the group defined above.
-    c.JupyterHub.services = [
-        {
-            'name': 'course101',
-            'url': 'http://127.0.0.1:9999',
-            'command': [
-                'jupyterhub-singleuser',
-                '--group=formgrader-course101',
-                '--debug',
-            ],
-            'user': 'grader-course101',
-            'cwd': '/home/grader-course101'
-        },
-        {
-            'name': 'course123',
-            'url': 'http://127.0.0.1:9998',
-            'command': [
-                'jupyterhub-singleuser',
-                '--group=formgrader-course123',
-                '--debug',
-            ],
-            'user': 'grader-course123',
-            'cwd': '/home/grader-course123'
-        },
-    ]
-
-There also needs to be a ``nbgrader_config.py`` file in the root of each grader
-account, which points to the directory where the class files are, e.g. in
-``/home/grader-course101/.jupyter/nbgrader_config.py`` would be:
-
-.. code:: python
-
-    c = get_config()
-    c.CourseDirectory.root = '/home/grader-course101/course101'
-
-and ``/home/grader-course123/.jupyter/nbgrader_config.py`` would be:
-
-.. code:: python
-
-    c = get_config()
-    c.CourseDirectory.root = '/home/grader-course123/course123'
-
-You will also need to do some additional configuration on the student side. If
-each student is enrolled in exactly one course, then you will need to provide
-them a custom ``nbgrader_config.py`` which specifies that course. Alternately,
-if students may be enrolled in multiple courses, you need to provide them a
-custom ``nbgrader_config.py`` that will cause nbgrader to look for assignments
-in a subdirectory corresponding to the course name. See :ref:`multiple-classes`
-for details.
-
-Finally, you will need to enable and disable different combinations of the nbgrader extensions for different accounts. See the table in :ref:`multiple-graders` for details.
-
 .. _jupyterhub-auth:
 
 JupyterHub Authentication
@@ -262,22 +187,9 @@ JupyterHub Authentication
 
 .. versionadded:: 0.6.0
 
-With the advent of JupyterHubAuthPlugin students who don't have a specified course_id won't see all courses anymore, just the ones they have been added to. That means you can ommit the ``course_id`` from the students ``nbgrader_config.py``
-
-Requirements for using ``JupyterHubAuthPlugin``:
-
-* Jupyterhub > 0.8
-* Activating the JupyterHubAuthPlugin requires you to add it as an authentication plugin class into the ``nbgrader_config.py`` for both the formgrader services and instructor accounts.
-
-.. code:: python
-
-    from nbgrader.auth import JupyterHubAuthPlugin
-    c = get_config()
-    c.Authenticator.plugin_class = JupyterHubAuthPlugin
+With the advent of JupyterHubAuthPlugin, nbgrader will ask JupyterHub which students are enrolled in which courses and only show them assignments from those respective courses (note that the ``JupyterHubAuthPlugin`` requires JupyterHub version 0.8 or higher). Similarly, nbgrader will ask JupyterHub which instructors have access to which courses and only show them formgrader links for those courses.
 
-* To differentiate student from instructor, their groups need to be named ``formgrade-{course_id}`` for instructors and ``nbgrader-{course_id}`` for students.
-* The course service needs to have an api_token set that is from a jupyterhub admin see: Creating an `api token <https://jupyterhub.readthedocs.io/en/stable/reference/rest.html#create-an-api-token>`_
-* The service user also needs to be added to the formgrade group (see the jupyterhub_config example below)
+On the JupyterHub side of things, to differentiate student from instructor, groups need to be named ``formgrade-{course_id}`` for instructors and and grader accounts, and ``nbgrader-{course_id}`` for students. The course service additionally needs to have an API token set that is from a JupyterHub admin (see `JupyterHub documentation <https://jupyterhub.readthedocs.io/en/stable/reference/rest.html#create-an-api-token>`_).
 
 As in the case of multiple graders for a single class, if you have multiple
 classes on the same JupyterHub instance, then you will need to create multiple
@@ -349,17 +261,35 @@ this:
     ]
 
 
-Note: As you can see the ``nbgrader-{course_id}`` group is an empty list, adding students to the jupyterhub group is automatically done when the instructor adds them to the course database with the ``nbgrader db student add`` command or through the formgrader.
+Note: As you can see the ``nbgrader-{course_id}`` group is an empty list.
+Adding students to the JupyterHub group is automatically done when the
+instructor adds them to the course database with the ``nbgrader db student
+add`` command or through the formgrader.
 
+On the nbgrader side of things, activating the JupyterHubAuthPlugin requires
+you to add it as an authentication plugin class into the ``nbgrader_config.py``
+for all accounts. This is easiest to do by putting it in a global location such
+as ``/etc/jupyter/nbgrader_config.py``. You also need to configure nbgrader to
+look for assignments in a subdirectory corresponding to the course name (see
+:ref:`multiple-classes`). For example:
 
-There also needs to be a ``nbgrader_config.py`` file in the root of each grader
-account, which points to the directory where the class files are, e.g. in
+.. code:: python
+
+    from nbgrader.auth import JupyterHubAuthPlugin
+    c = get_config()
+    c.Exchange.path_includes_course = True
+    c.Authenticator.plugin_class = JupyterHubAuthPlugin
+
+There also needs to be a separate ``nbgrader_config.py`` file in the root of
+each grader account, which points to the directory where the class files are
+and which specifies what the course id is, e.g.
 ``/home/grader-course101/.jupyter/nbgrader_config.py`` would be:
 
 .. code:: python
 
     c = get_config()
     c.CourseDirectory.root = '/home/grader-course101/course101'
+    c.CourseDirectory.course_id = 'course101'
 
 and ``/home/grader-course123/.jupyter/nbgrader_config.py`` would be:
 
@@ -367,7 +297,11 @@ and ``/home/grader-course123/.jupyter/nbgrader_config.py`` would be:
 
     c = get_config()
     c.CourseDirectory.root = '/home/grader-course123/course123'
+    c.CourseDirectory.course_id = 'course123'
 
+Finally, you will again need to enable and disable different combinations of
+the nbgrader extensions for different accounts. See the table in
+:ref:`multiple-graders` for details.
 
 Custom Authentication
 ^^^^^^^^^^^^^^^^^^^^^
@@ -380,10 +314,8 @@ There are now four authentication classes:
 
 
 * ``BaseAuthPlugin``: Inherit this class when implementing your own plugin, thought of as a way to enable LTI use cases. This class is never called directly.
-* ``NoAuthPlugin``: The default old behaviour. Using this plugin will allow any user to any course if he does not have a course_id in his nbgrader_config. This is still the default behaviour so no need to specify it in ``etc/jupyter/nbgrader_config.py``
-
-* ``JupyterHubAuthPlugin``: Uses the Jupyterhub groups part of the Jupyterhub API for authentication.
-
+* ``NoAuthPlugin``: The default old behaviour. Using this plugin will allow any user to any course if they do not have a course_id in their nbgrader_config. This is still the default behaviour so no need to specify it in ``/etc/jupyter/nbgrader_config.py``
+* ``JupyterHubAuthPlugin``: Uses the Jupyterhub groups part of the JupyterHub API for authentication.
 * ``Authenticator``: Configurable for different plugins.
 
 
@@ -399,7 +331,3 @@ API
     .. automethod:: add_student_to_course
 
     .. automethod:: remove_student_from_course
-
-
-
-
diff --git a/nbgrader/docs/source/contributor_guide/releasing.rst b/nbgrader/docs/source/contributor_guide/releasing.rst
index 916390d9..f5f64c1d 100644
--- a/nbgrader/docs/source/contributor_guide/releasing.rst
+++ b/nbgrader/docs/source/contributor_guide/releasing.rst
@@ -41,6 +41,7 @@ The version number needs to be changed in the following files:
 
 - ``nbgrader/_version.py``
 - ``nbgrader/nbextensions/assignment_list/main.js``
+- ``nbgrader/nbextensions/course_list/main.js``
 - ``nbgrader/nbextensions/validate_assignment/main.js``
 
 Rebuild the documentation
@@ -141,4 +142,5 @@ was ``0.3.0``, then the new version should be ``0.4.0.dev0``. Remember that the
 
 - ``nbgrader/_version.py``
 - ``nbgrader/nbextensions/assignment_list/main.js``
+- ``nbgrader/nbextensions/course_list/main.js``
 - ``nbgrader/nbextensions/validate_assignment/main.js``
diff --git a/nbgrader/exchange/list.py b/nbgrader/exchange/list.py
index ccade378..1b07566e 100644
--- a/nbgrader/exchange/list.py
+++ b/nbgrader/exchange/list.py
@@ -99,7 +99,7 @@ class ExchangeList(Exchange):
                     notebookDir, notebookFilename = os.path.split(notebook)
                     notebookName, _ = os.path.splitext(notebookFilename)
                     feedbackpath = os.path.join(self.root, info['course_id'], 'feedback', '{0}.html'.format(nb_hash))
-                    # notebookDir should have the course_did in it if we have multiple courses ...
+                    # notebookDir should have the course_id in it if we have multiple courses ...
                     if self.path_includes_course:
                         nbdir = os.path.join(self.assignment_dir, info['course_id'], info['assignment_id'])
                     else:
diff --git a/nbgrader/server_extensions/course_list/handlers.py b/nbgrader/server_extensions/course_list/handlers.py
index 82f059f8..0fb97b7f 100644
--- a/nbgrader/server_extensions/course_list/handlers.py
+++ b/nbgrader/server_extensions/course_list/handlers.py
@@ -19,7 +19,8 @@ from jupyter_core.paths import jupyter_config_path
 from ...apps import NbGrader
 from ...auth import Authenticator
 from ...auth.jupyterhub import (JupyterhubEnvironmentError, get_jupyterhub_api_url,
-    get_jupyterhub_authorization, get_jupyterhub_url)
+    get_jupyterhub_authorization, get_jupyterhub_url, get_jupyterhub_user)
+from ...coursedir import CourseDirectory
 from ... import __version__ as nbgrader_version
 
 
@@ -89,6 +90,37 @@ class CourseListHandler(IPythonHandler):
         self.log.error("Local formgrader not accessible")
         raise gen.Return([])
 
+    @gen.coroutine
+    def check_for_noauth_jupyterhub_formgraders(self, config):
+        try:
+            get_jupyterhub_user()
+        except JupyterhubEnvironmentError:
+            # Not running on JupyterHub.
+            raise gen.Return([])
+
+        # We are running on JupyterHub, so maybe there's a formgrader
+        # service. Check if we have a course id and if so guess the path to the
+        # formgrader.
+        coursedir = CourseDirectory(config=config)
+        if not coursedir.course_id:
+            raise gen.Return([])
+
+        url = self.get_base_url() + "/services/" + coursedir.course_id + "/formgrader"
+        auth = get_jupyterhub_authorization()
+        http_client = AsyncHTTPClient()
+        try:
+            yield http_client.fetch(url, headers=auth)
+        except:
+            self.log.error("Formgrader not available at URL: %s", url)
+            raise gen.Return([])
+
+        courses = [{
+            'course_id': coursedir.course_id,
+            'url': url,
+            'kind': 'jupyterhub'
+        }]
+        raise gen.Return(courses)
+
     @gen.coroutine
     def check_for_jupyterhub_formgraders(self, config):
         # first get the list of courses from the authenticator
@@ -99,10 +131,12 @@ class CourseListHandler(IPythonHandler):
             # not running on JupyterHub, or otherwise don't have access
             raise gen.Return([])
 
-        # If course_names is None, that probably means we're not running with
-        # JupyterHub.
+        # If course_names is None, that means either we're not running with
+        # JupyterHub, or we just have a single class for all students and
+        # instructors.
         if course_names is None:
-            raise gen.Return([])
+            courses = yield self.check_for_noauth_jupyterhub_formgraders(config)
+            raise gen.Return(courses)
 
         base_url = get_jupyterhub_api_url()
         url = base_url + "/services"
['nbgrader/server_extensions/course_list/handlers.py', 'nbgrader/docs/source/configuration/jupyterhub_config.rst', 'nbgrader/auth/jupyterhub.py', 'nbgrader/exchange/list.py', 'nbgrader/docs/source/contributor_guide/releasing.rst']