Skip to content

Commit 3657ca5

Browse files
authored
Open formgrader with a local configuration file (#1859)
* 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
1 parent 6bfd5a4 commit 3657ca5

File tree

12 files changed

+382
-47
lines changed

12 files changed

+382
-47
lines changed

nbgrader/apps/baseapp.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ class NbGrader(JupyterApp):
6464
aliases = nbgrader_aliases
6565
flags = nbgrader_flags
6666

67+
load_cwd_config = True
68+
6769
_log_formatter_cls = LogFormatter
6870

6971
@default("log_level")
@@ -313,10 +315,13 @@ def excepthook(self, etype, evalue, tb):
313315
format_excepthook(etype, evalue, tb)
314316

315317
@catch_config_error
316-
def initialize(self, argv: TypingList[str] = None) -> None:
318+
def initialize(self, argv: TypingList[str] = None, root: str = '') -> None:
317319
self.update_config(self.build_extra_config())
318320
self.init_syspath()
319-
self.coursedir = CourseDirectory(parent=self)
321+
if root:
322+
self.coursedir = CourseDirectory(parent=self, root=root)
323+
else:
324+
self.coursedir = CourseDirectory(parent=self)
320325
super(NbGrader, self).initialize(argv)
321326

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

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

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

366-
# Load also config from current working directory
367-
super(JupyterApp, self).load_config_file(self.config_file_name, os.getcwd())
372+
if (self.load_cwd_config):
373+
# Load also config from current working directory
374+
super(JupyterApp, self).load_config_file(self.config_file_name, os.getcwd())
368375

369376
def start(self) -> None:
370377
super(NbGrader, self).start()

nbgrader/docs/source/build_docs.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import shutil
66
import sys
77
import nbgrader.apps
8+
import nbgrader.server_extensions.formgrader
89

910
from textwrap import dedent
1011
from clear_docs import run, clear_notebooks
@@ -92,6 +93,7 @@ def autogen_config(root):
9293

9394
print('Generating example configuration file')
9495
config = nbgrader.apps.NbGraderApp().document_config_options()
96+
config += nbgrader.server_extensions.formgrader.formgrader.FormgradeExtension().document_config_options()
9597
destination = os.path.join(root, 'configuration', 'config_options.rst')
9698
with open(destination, 'w') as f:
9799
f.write(header)

nbgrader/docs/source/configuration/nbgrader_config.rst

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,34 @@ For example, the ``nbgrader_config.py`` that the notebook knows about could be p
6565

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

68+
Use Case 3: using config from a specific sub directory
69+
------------------------------------------------------
6870

69-
Use Case 3: nbgrader and JupyterHub
71+
.. warning::
72+
73+
This option should not be used with a multiuser Jupyterlab instance, as it modifies
74+
certain objects in the running instance, and can probably prevent other users
75+
from using *formgrader* correctly. Also, if you have a JupyterHub installation,
76+
you should use the settings described in the following section.
77+
78+
You may need to use a dedicated configuration file for each course without configuring
79+
JupyterHub for all courses. In this case, the config file used will be the one from the
80+
current directory in the filebrowser panel, instead of the one from the directory where
81+
the jupyter server started.
82+
83+
This option is not enabled by default. It can be enabled by using the settings panel:
84+
*Nbgrader -> Formgrader* and check *Allow local nbgrader config file*.
85+
86+
A new item is displayed in the *nbgrader* menu (or in the command palette), to open
87+
formgrader from the local director: *Formgrader (local)*.
88+
89+
.. warning::
90+
91+
If paths are used in the configuration file, note that the root of the relative
92+
paths will always be the directory where the jupyter server was started, and not
93+
the directory containing the ``nbgrader_config.py`` file.
94+
95+
Use Case 4: nbgrader and JupyterHub
7096
-----------------------------------
7197

7298
.. seealso::

nbgrader/server_extensions/formgrader/base.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,15 @@ def base_url(self):
1616

1717
@property
1818
def db_url(self):
19-
return self.settings['nbgrader_coursedir'].db_url
19+
return self.coursedir.db_url
2020

2121
@property
2222
def url_prefix(self):
2323
return self.settings['nbgrader_formgrader'].url_prefix
2424

2525
@property
2626
def coursedir(self):
27-
return self.settings['nbgrader_coursedir']
27+
return self.settings['nbgrader_formgrader'].coursedir
2828

2929
@property
3030
def authenticator(self):

nbgrader/server_extensions/formgrader/formgrader.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
# coding: utf-8
22

33
import os
4+
from textwrap import dedent
45

56
from nbconvert.exporters import HTMLExporter
6-
from traitlets import default
7+
from traitlets import Bool, default
78
from tornado import web
89
from jinja2 import Environment, FileSystemLoader
910
from jupyter_server.utils import url_path_join as ujoin
10-
from jupyter_core.paths import jupyter_config_path
1111

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

21+
debug = Bool(
22+
True,
23+
help=dedent(
24+
"""
25+
Whether to display the loaded configuration in the 'Formgrader ->
26+
Manage Assignments' panel. This can help debugging some misconfiguration
27+
when using several files.
28+
"""
29+
)
30+
).tag(config=True)
31+
2132
@property
2233
def root_dir(self):
2334
return self._root_dir
@@ -33,10 +44,9 @@ def url_prefix(self):
3344
return relpath
3445

3546
def load_config(self):
36-
paths = jupyter_config_path()
37-
paths.insert(0, os.getcwd())
3847
app = NbGrader()
39-
app.config_file_paths.append(paths)
48+
app.load_cwd_config = self.load_cwd_config
49+
app.config_dir = self.config_dir
4050
app.load_config_file()
4151

4252
return app.config
@@ -72,13 +82,13 @@ def init_tornado_settings(self, webapp):
7282
# Configure the formgrader settings
7383
tornado_settings = dict(
7484
nbgrader_formgrader=self,
75-
nbgrader_coursedir=self.coursedir,
7685
nbgrader_authenticator=self.authenticator,
7786
nbgrader_exporter=HTMLExporter(config=self.config),
7887
nbgrader_gradebook=None,
7988
nbgrader_db_url=self.coursedir.db_url,
8089
nbgrader_jinja2_env=jinja_env,
81-
nbgrader_bad_setup=nbgrader_bad_setup
90+
nbgrader_bad_setup=nbgrader_bad_setup,
91+
initial_config=self.config
8292
)
8393

8494
webapp.settings.update(tornado_settings)

nbgrader/server_extensions/formgrader/handlers.py

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,62 @@
11
import os
22
import re
33
import sys
4+
import json
45

56
from tornado import web
7+
from jupyter_core.paths import jupyter_config_dir
8+
from traitlets.config.loader import Config
69

710
from .base import BaseHandler, check_xsrf, check_notebook_dir
811
from ...api import MissingEntry
912

1013

14+
class FormgraderHandler(BaseHandler):
15+
@web.authenticated
16+
@check_xsrf
17+
@check_notebook_dir
18+
def get(self):
19+
formgrader = self.settings['nbgrader_formgrader']
20+
path = self.get_argument('path', '')
21+
if path:
22+
path = os.path.abspath(path)
23+
formgrader.load_cwd_config = False
24+
formgrader.config = Config()
25+
formgrader.config_dir = path
26+
formgrader.initialize([], root=path)
27+
else:
28+
if formgrader.config != self.settings['initial_config']:
29+
formgrader.config = self.settings['initial_config']
30+
formgrader.config_dir = jupyter_config_dir()
31+
formgrader.initialize([])
32+
formgrader.load_cwd_config = True
33+
self.redirect(f"{self.base_url}/formgrader/manage_assignments")
34+
35+
1136
class ManageAssignmentsHandler(BaseHandler):
1237
@web.authenticated
1338
@check_xsrf
1439
@check_notebook_dir
1540
def get(self):
41+
formgrader = self.settings['nbgrader_formgrader']
42+
current_config = {}
43+
if formgrader.debug:
44+
try:
45+
current_config = json.dumps(formgrader.config, indent=2)
46+
except TypeError:
47+
current_config = formgrader.config
48+
self.log.warn("Formgrader config is not serializable")
49+
50+
api = self.api
1651
html = self.render(
1752
"manage_assignments.tpl",
1853
url_prefix=self.url_prefix,
1954
base_url=self.base_url,
2055
windows=(sys.prefix == 'win32'),
21-
course_id=self.api.course_id,
22-
exchange=self.api.exchange_root,
23-
exchange_missing=self.api.exchange_missing)
56+
course_id=api.course_id,
57+
exchange=api.exchange_root,
58+
exchange_missing=api.exchange_missing,
59+
current_config= current_config)
2460
self.write(html)
2561

2662

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

284320
default_handlers = [
285-
(r"/formgrader/?", ManageAssignmentsHandler),
321+
(r"/formgrader/?", FormgraderHandler),
286322
(r"/formgrader/manage_assignments/?", ManageAssignmentsHandler),
287323
(r"/formgrader/manage_submissions/([^/]+)/?", ManageSubmissionsHandler),
288324

nbgrader/server_extensions/formgrader/templates/manage_assignments.tpl

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,22 @@ Manage Assignments
5555
</div>
5656
</div>
5757
</div>
58+
{% if current_config %}
59+
<div class="panel-group" id="config" role="tablist" aria-multiselectable="true">
60+
<div class="panel panel-default">
61+
<div class="panel-heading" role="tab" id="headingConfig">
62+
<h4 class="panel-title">
63+
<a class="collapsed" role="button" data-toggle="collapse" data-parent="#accordion" href="#collapseConfig" aria-expanded="false" aria-controls="collapseConfig">
64+
Current configuration (click to expand)
65+
</a>
66+
</h4>
67+
</div>
68+
<div id="collapseConfig" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingConfig">
69+
<pre class="panel-body">{{ current_config }}</pre>
70+
</div>
71+
</div>
72+
</div>
73+
{% endif %}
5874
{% if windows %}
5975
<div class="alert alert-warning" id="warning-windows">
6076
Windows operating system detected. Please note that the "release" and "collect"

0 commit comments

Comments
 (0)