Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open formgrader with a local configuration file #1859

Merged
merged 12 commits into from
Jun 20, 2024
Merged
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
Loading