Skip to content

Commit

Permalink
Open formgrader with a local configuration file (#1859)
Browse files Browse the repository at this point in the history
* Allow formgrader to update its config from the current directory in lab

* Restore the config to avoid confusion

* Initialize configuration only if necessary

* Add a debuf flag to FormgraderExtension to display the current configuration in formgrader UI

* Add a setting to enable/disable the local formgrader

* Avoid loading CWD config when API is called too

* Fix UI tests

* Update documentation

* Add a warning about using it with several users on the same Jupyterlab instance, and show the configuration by default in formgrader

* Add tests on local formgrader and increase time for doc test

* Fix ui-tests on notebook

* Skip test on formgrader exchange on windows
  • Loading branch information
brichet committed Jun 20, 2024
1 parent 6bfd5a4 commit 3657ca5
Show file tree
Hide file tree
Showing 12 changed files with 382 additions and 47 deletions.
17 changes: 12 additions & 5 deletions nbgrader/apps/baseapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ class NbGrader(JupyterApp):
aliases = nbgrader_aliases
flags = nbgrader_flags

load_cwd_config = True

_log_formatter_cls = LogFormatter

@default("log_level")
Expand Down Expand Up @@ -313,10 +315,13 @@ def excepthook(self, etype, evalue, tb):
format_excepthook(etype, evalue, tb)

@catch_config_error
def initialize(self, argv: TypingList[str] = None) -> None:
def initialize(self, argv: TypingList[str] = None, root: str = '') -> None:
self.update_config(self.build_extra_config())
self.init_syspath()
self.coursedir = CourseDirectory(parent=self)
if root:
self.coursedir = CourseDirectory(parent=self, root=root)
else:
self.coursedir = CourseDirectory(parent=self)
super(NbGrader, self).initialize(argv)

# load config that is in the coursedir directory
Expand Down Expand Up @@ -355,16 +360,18 @@ def load_config_file(self, **kwargs: Any) -> None:
paths = [os.path.abspath("{}.py".format(self.config_file))]
else:
config_dir = self.config_file_paths.copy()
config_dir.insert(0, os.getcwd())
if self.load_cwd_config:
config_dir.insert(0, os.getcwd())
paths = [os.path.join(x, "{}.py".format(self.config_file_name)) for x in config_dir]

if not any(os.path.exists(x) for x in paths):
self.log.warning("No nbgrader_config.py file found (rerun with --debug to see where nbgrader is looking)")

super(NbGrader, self).load_config_file(**kwargs)

# Load also config from current working directory
super(JupyterApp, self).load_config_file(self.config_file_name, os.getcwd())
if (self.load_cwd_config):
# Load also config from current working directory
super(JupyterApp, self).load_config_file(self.config_file_name, os.getcwd())

def start(self) -> None:
super(NbGrader, self).start()
Expand Down
2 changes: 2 additions & 0 deletions nbgrader/docs/source/build_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import shutil
import sys
import nbgrader.apps
import nbgrader.server_extensions.formgrader

from textwrap import dedent
from clear_docs import run, clear_notebooks
Expand Down Expand Up @@ -92,6 +93,7 @@ def autogen_config(root):

print('Generating example configuration file')
config = nbgrader.apps.NbGraderApp().document_config_options()
config += nbgrader.server_extensions.formgrader.formgrader.FormgradeExtension().document_config_options()
destination = os.path.join(root, 'configuration', 'config_options.rst')
with open(destination, 'w') as f:
f.write(header)
Expand Down
28 changes: 27 additions & 1 deletion nbgrader/docs/source/configuration/nbgrader_config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,34 @@ For example, the ``nbgrader_config.py`` that the notebook knows about could be p

Then you would additionally have a config file at ``/path/to/course/directory/nbgrader_config.py``.

Use Case 3: using config from a specific sub directory
------------------------------------------------------

Use Case 3: nbgrader and JupyterHub
.. warning::

This option should not be used with a multiuser Jupyterlab instance, as it modifies
certain objects in the running instance, and can probably prevent other users
from using *formgrader* correctly. Also, if you have a JupyterHub installation,
you should use the settings described in the following section.

You may need to use a dedicated configuration file for each course without configuring
JupyterHub for all courses. In this case, the config file used will be the one from the
current directory in the filebrowser panel, instead of the one from the directory where
the jupyter server started.

This option is not enabled by default. It can be enabled by using the settings panel:
*Nbgrader -> Formgrader* and check *Allow local nbgrader config file*.

A new item is displayed in the *nbgrader* menu (or in the command palette), to open
formgrader from the local director: *Formgrader (local)*.

.. warning::

If paths are used in the configuration file, note that the root of the relative
paths will always be the directory where the jupyter server was started, and not
the directory containing the ``nbgrader_config.py`` file.

Use Case 4: nbgrader and JupyterHub
-----------------------------------

.. seealso::
Expand Down
4 changes: 2 additions & 2 deletions nbgrader/server_extensions/formgrader/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ def base_url(self):

@property
def db_url(self):
return self.settings['nbgrader_coursedir'].db_url
return self.coursedir.db_url

@property
def url_prefix(self):
return self.settings['nbgrader_formgrader'].url_prefix

@property
def coursedir(self):
return self.settings['nbgrader_coursedir']
return self.settings['nbgrader_formgrader'].coursedir

@property
def authenticator(self):
Expand Down
24 changes: 17 additions & 7 deletions nbgrader/server_extensions/formgrader/formgrader.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# coding: utf-8

import os
from textwrap import dedent

from nbconvert.exporters import HTMLExporter
from traitlets import default
from traitlets import Bool, default
from tornado import web
from jinja2 import Environment, FileSystemLoader
from jupyter_server.utils import url_path_join as ujoin
from jupyter_core.paths import jupyter_config_path

from . import handlers, apihandlers
from ...apps.baseapp import NbGrader
Expand All @@ -18,6 +18,17 @@ class FormgradeExtension(NbGrader):
name = u'formgrade'
description = u'Grade a notebook using an HTML form'

debug = Bool(
True,
help=dedent(
"""
Whether to display the loaded configuration in the 'Formgrader ->
Manage Assignments' panel. This can help debugging some misconfiguration
when using several files.
"""
)
).tag(config=True)

@property
def root_dir(self):
return self._root_dir
Expand All @@ -33,10 +44,9 @@ def url_prefix(self):
return relpath

def load_config(self):
paths = jupyter_config_path()
paths.insert(0, os.getcwd())
app = NbGrader()
app.config_file_paths.append(paths)
app.load_cwd_config = self.load_cwd_config
app.config_dir = self.config_dir
app.load_config_file()

return app.config
Expand Down Expand Up @@ -72,13 +82,13 @@ def init_tornado_settings(self, webapp):
# Configure the formgrader settings
tornado_settings = dict(
nbgrader_formgrader=self,
nbgrader_coursedir=self.coursedir,
nbgrader_authenticator=self.authenticator,
nbgrader_exporter=HTMLExporter(config=self.config),
nbgrader_gradebook=None,
nbgrader_db_url=self.coursedir.db_url,
nbgrader_jinja2_env=jinja_env,
nbgrader_bad_setup=nbgrader_bad_setup
nbgrader_bad_setup=nbgrader_bad_setup,
initial_config=self.config
)

webapp.settings.update(tornado_settings)
Expand Down
44 changes: 40 additions & 4 deletions nbgrader/server_extensions/formgrader/handlers.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,62 @@
import os
import re
import sys
import json

from tornado import web
from jupyter_core.paths import jupyter_config_dir
from traitlets.config.loader import Config

from .base import BaseHandler, check_xsrf, check_notebook_dir
from ...api import MissingEntry


class FormgraderHandler(BaseHandler):
@web.authenticated
@check_xsrf
@check_notebook_dir
def get(self):
formgrader = self.settings['nbgrader_formgrader']
path = self.get_argument('path', '')
if path:
path = os.path.abspath(path)
formgrader.load_cwd_config = False
formgrader.config = Config()
formgrader.config_dir = path
formgrader.initialize([], root=path)
else:
if formgrader.config != self.settings['initial_config']:
formgrader.config = self.settings['initial_config']
formgrader.config_dir = jupyter_config_dir()
formgrader.initialize([])
formgrader.load_cwd_config = True
self.redirect(f"{self.base_url}/formgrader/manage_assignments")


class ManageAssignmentsHandler(BaseHandler):
@web.authenticated
@check_xsrf
@check_notebook_dir
def get(self):
formgrader = self.settings['nbgrader_formgrader']
current_config = {}
if formgrader.debug:
try:
current_config = json.dumps(formgrader.config, indent=2)
except TypeError:
current_config = formgrader.config
self.log.warn("Formgrader config is not serializable")

api = self.api
html = self.render(
"manage_assignments.tpl",
url_prefix=self.url_prefix,
base_url=self.base_url,
windows=(sys.prefix == 'win32'),
course_id=self.api.course_id,
exchange=self.api.exchange_root,
exchange_missing=self.api.exchange_missing)
course_id=api.course_id,
exchange=api.exchange_root,
exchange_missing=api.exchange_missing,
current_config= current_config)
self.write(html)


Expand Down Expand Up @@ -282,7 +318,7 @@ def prepare(self):
_navigation_regex = r"(?P<action>next_incorrect|prev_incorrect|next|prev)"

default_handlers = [
(r"/formgrader/?", ManageAssignmentsHandler),
(r"/formgrader/?", FormgraderHandler),
(r"/formgrader/manage_assignments/?", ManageAssignmentsHandler),
(r"/formgrader/manage_submissions/([^/]+)/?", ManageSubmissionsHandler),

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,22 @@ Manage Assignments
</div>
</div>
</div>
{% if current_config %}
<div class="panel-group" id="config" role="tablist" aria-multiselectable="true">
<div class="panel panel-default">
<div class="panel-heading" role="tab" id="headingConfig">
<h4 class="panel-title">
<a class="collapsed" role="button" data-toggle="collapse" data-parent="#accordion" href="#collapseConfig" aria-expanded="false" aria-controls="collapseConfig">
Current configuration (click to expand)
</a>
</h4>
</div>
<div id="collapseConfig" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingConfig">
<pre class="panel-body">{{ current_config }}</pre>
</div>
</div>
</div>
{% endif %}
{% if windows %}
<div class="alert alert-warning" id="warning-windows">
Windows operating system detected. Please note that the "release" and "collect"
Expand Down
Loading

0 comments on commit 3657ca5

Please sign in to comment.